Spring之AOP(二)
1、代理模式
为什么要学代理模式?因为这就是SpringAOP的底层!
代理模式的分类:
- 静态代理
- 动态代理
![](https://gitee.com/yyb1024/myimage/raw/weiyi_work_img/img/20210306171228.png)
1、1静态代理
角色分析:
- 抽象角色:一般会使用接口或者抽象类来解决
- 真实角色:被代理的角色
- 代理角色:代理真实角色,代理真实角色后,我们一般会做一些附属操作
- 客户:访问代理对象的人。
代码步骤:
-
1、接口:
package com.study.proxy.demo1; //出租 public interface Rent { public void rent(); }
-
2、真实角色
package com.study.proxy.demo1; //房东 public class Host implements Rent { @Override public void rent() { System.out.println("我要出租房子!"); } }
-
3、代理角色
package com.study.proxy.demo1; //代理 public class Proxy implements Rent { //将房东作为属性引入 private Host host; //有参构造 public Proxy(Host host) { this.host = host; } @Override public void rent() { //代理房东出租 host.rent(); SeeHome(); free(); } //代理的其他行为 public void SeeHome(){ System.out.println("中介带客户看房"); } public void free(){ System.out.println("中介收代理费"); } }
-
4、客户端访问代理角色
package com.study.proxy.demo1; public class Client { public static void main(String[] args) { //创建房东对象 Host host = new Host(); //创建代理对象将房东对象注入其中 Proxy proxy = new Proxy(host); //代理带房东出租房子 proxy.rent(); } }
输出:
我要出租房子!
中介带客户看房
中介收代理费
2、业务场景:对基础业务增删改查添加log日志
-
UserServiceImpl实现类
package com.study.proxy.demo2; public class UserServiceImpl implements UserService{ @Override public void add() { System.out.println("我是添加方法"); } @Override public void delete() { System.out.println("我是删除方法"); } @Override public void alter() { System.out.println("我是修改方法"); } @Override public void query() { System.out.println("我是查询方法"); } }
-
UserServiceProxy代理类:
package com.study.proxy.demo2; //代理 public class UserServiceProxy implements UserService{ //组合方式:加入UserServiceImpl类作为属性(则可以调用他的方法) private UserServiceImpl userService; //set方式注入: public void setUserService(UserServiceImpl userService) { this.userService = userService; } @Override public void add() { log("添加"); userService.add(); } @Override public void delete() { log("删除"); userService.delete(); } @Override public void alter() { log("修改"); userService.alter(); } @Override public void query() { log("查询"); userService.query(); } //在增删改的基础上实现打印日志功能:日志方法 public void log(String msg){ System.out.println("使用"+msg+"方法"); } }
-
客户端访问代理类
package com.study.proxy.demo2; public class Client { public static void main(String[] args) { //创建UserServiceImpl对象 UserServiceImpl userService = new UserServiceImpl(); //创建代理对象 UserServiceProxy userServiceProxy = new UserServiceProxy(); //set依赖注入对象 userServiceProxy.setUserService(userService); //代理调用方法; userServiceProxy.add(); } } 输出: 使用添加方法 我是添加方法
代理模式的好处:
- 可以使真实角色的操作更加纯粹,不用去关注一些公共的业务
- 公共业务也就交给代理角色!实现业务的分工!降低了耦合性
- 公共业务发生扩展的时候,方便集中管理!
缺点:
- 一个真实的角色就会产生一个代理角色;代码量会翻倍,开发效率就会变低
1、2动态代理
- 动态代理和静态代理角色一样
- 动态代理的代理类是动态生产的,不是我们直接 写的
- 动态代理分为两大类:基于接口的动态代理,基于类的动态代理
- 基于接口---JDK动态代理(主要使用)
- 基于类:cglib
- java字节码实现:javassist
- 需要了解两个类:
- Proxy:代理
- InvocationHandler:调用处理程序
动态代理的好处:
-
一个动态代理类代理的是一个接口,一般就是对应的一类业务
-
一个动态代理类可以代理多个了类,只要是实现了同一个接口即可!
-
案例分析:
Rent接口类,Host类如上:
ProxyInvocationHandler代理类
package com.study.proxy.dome3; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; //等我们会用这个类,自动生成代理类 public class ProxyInvocationHandler implements InvocationHandler { //被代理的接口 private Rent rent; public void setRent(Rent rent) { this.rent = rent; } //生成得到代理类 public Object getProxy(){ return Proxy.newProxyInstance( this.getClass().getClassLoader(), rent.getClass().getInterfaces(), this); } //处理代理实例,并返回结果 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //动态代理的本质,就是使用反射机制实现 Object result = method.invoke(rent,args); return result; } }
-
client类
package com.study.proxy.dome3; public class Client { public static void main(String[] args) { //真实角色 Host host = new Host(); //代理角色:现在没有 ProxyInvocationHandler pih = new ProxyInvocationHandler(); //通过调用程序处理角色处理我们要调用的接口对象 pih.setRent(host); Rent proxy = (Rent) pih.getProxy(); proxy.rent(); } }
-
输出:我要出租房子!
-
一个动态代理类代理的是一个接口,一般就是对应的一类业务
//处理代理实例,并返回结果 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //动态代理的本质,就是使用反射机制实现 Object result = method.invoke(rent,args); see(); free(); return result; } public void free(){ System.out.println("中介带看房子收费!"); } public void see(){ System.out.println("中介带看房子"); } =================================================== 输出:我要出租房子! 中介带看房子 中介带看房子收费!
2、什么是AOP
AOP(Aspect Orient Programming),面向切面编程,
-
通过预编译方式和运行期动态代理实现程序功能的一种维护的一种技术。AOP是OOP的延伸,是函数式编程的一种衍生泛型。
-
利用AOP可以对业务逻辑的各个部分进行隔离,从而是的业务逻辑个部分之间的耦合度降低,提高程序的可重用性,同时提高开发的效率。
- 所谓切面,相当于应用对象间的横切点,我们可以将其单独抽象为单独的模块。
2、1为什么需要 AOP
- 在传统的面向过程编程中,开发中在多个模块间有某段重复的代码,我们也会将这段代码,抽象成一个方法,然后在需要的地方分别调用这个方法,这样当这段代码需要修改时,我们只需要改变这个方法就可以了。
- 新增了一个需求,需要再多出做修改,我们需要再抽象出一个方法,然后再在需要的地方分别调用这个方法,又或者我们不需要这个方法了,我们还是得删除掉每一处调用该方法的地方。
2、2 AOP 目的
- AOP 要达到的效果是,保证开发者不修改源代码的前提下,去为系统中的业务组件添加某种通用功能。
- AOP 的本质是由 AOP 框架修改业务组件的多个方法的源代码
2、3AOP的作用
提供声明式事务;允许用户自定义切面
-
横切关注点:跨越应用程序多个模块的方法或功能。即:与我们业务逻辑无关的,但我们需要关注的部分,就是横切关注点。如日志,安全,缓存,事务等。。。。。。
-
切面(Aspect):横切关注点被模块化的特殊对象即:他是一个类
-
通知(Advice):切面必须要完成的工作,即他是类中的一个方法。
-
目标(Target):被通知对象。
-
代理(Proxy):向目标对象应用通知之后创建的对象
-
切入点(PointCut):切面通知执行的“地点”的定义
-
连接点(JoinPoint):与切入点匹配的执行点。
![](https://gitee.com/yyb1024/myimage/raw/weiyi_work_img/img/20210306171246.png)
4、使用Spring实现AOP
- 使用AOP织入,需要导入一个依赖包!
<!-- https://mvnrepository.com/artifact/aspectj/aspectjweaver --> <dependency> <groupId>aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.5.3</version> </dependency>
xml配置:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd"> </beans>
-
方式一:使用原生 Spring API接口
service层:
package com.study.service; public interface UserService { public void add (); public void delete (); public void alter (); public void query (); }
package com.study.service; public class UserServiceImpl implements UserService{ @Override public void add() { System.out.println("增加一个用户"); } @Override public void delete() { System.out.println("删除一个用户"); } @Override public void alter() { System.out.println("修改一个用户"); } @Override public void query() { System.out.println("插询一个用户"); } }
Log日志:
-
package com.study.log; import org.springframework.aop.AfterReturningAdvice; import java.lang.reflect.Method; //后置通知 public class AfterLog implements AfterReturningAdvice { //returnValue:返回值 @Override public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable { System.out.println("执行了"+method.getName()+"方法,返回结果为"+returnValue); } }
package com.study.log; import org.springframework.aop.MethodBeforeAdvice; import java.lang.reflect.Method; /*通过Advice定义横切了逻辑Spring支持Advice的5类型*/ //前置通知: public class Log implements MethodBeforeAdvice { //method :要执行目标对象的方法 //args :参数 //target:目标参数 @Override public void before(Method method, Object[] args, Object target) throws Throwable { System.out.println( target.getClass().getName()+"的"+ method.getName()+"被执行了"); } }
applicationContext.xml
-
<!--注册bean--> <bean id="userSevice" class="com.study.service.UserServiceImpl"></bean> <bean id="aferLog" class="com.study.log.AfterLog"></bean> <bean id="log" class="com.study.log.Log"></bean> <!--方式一:使用原生 Spring API接口--> <!--配置AOP:需要导入aop约束--> <aop:config> <!--切入点:expression:表达式 execution(要执行的位置!* * * *)--> <aop:pointcut id="pointcut" expression="execution(* com.study.service.UserServiceImpl.*(..))"/> <!--执行环绕增加--> <aop:advisor advice-ref="log" pointcut-ref="pointcut"/> <aop:advisor advice-ref="aferLog" pointcut-ref="pointcut"/> </aop:config>
Test类:
import com.study.service.UserService; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class MyTest { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); //动态代理的是接口 UserService userService = (UserService) context.getBean("userSevice"); userService.add(); } }
输出:增加一个用户
-
方法二、自定义类
在上面代码基础上增加diy类:
package com.study.diy; public class DiyPointCut { public void before(){ System.out.println("====方法执行前===="); } public void after(){ System.out.println("=====方法执行后====="); } }
<!--方法二:自定义类--> <bean id="diy" class="com.study.diy.DiyPointCut"></bean> <aop:config> <!--自定义切面,ref要引用的类--> <aop:aspect ref="diy"> <!--切入点--> <aop:pointcut id="point" expression="execution(* com.study.service.UserServiceImpl.*(..))"/> <!--通知--> <aop:before method="before" pointcut-ref="point"/> <aop:before method="after" pointcut-ref="point"/> </aop:aspect> =============================== 输出: ====方法执行前==== =====方法执行后===== 增加一个用户
-
方法三、使用注解方式实现AOP
通过注解声明 5 种通知类型
Spring AOP 中有 5 中通知类型,分别如下
-
在上面代码基础上增加AnnotationPointCut类:
package com.study.diy;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;//方式三:使用注解方式实现AOP
@Aspect//标注这个类值切面
public class AnnotationPointCut {
@Before("execution(* com.study.service.UserServiceImpl.(..))")
public void before(){
System.out.println("方法执行前==");
}
@After("execution( com.study.service.UserServiceImpl.*(..))")
public void after(){
System.out.println("方法执行后==");
}//在环绕增强中,我们可以给定一个参数,代表我们要获取处理的切入点:
@Around("execution(* com.study.service.UserServiceImpl.*(..))")
public void around(ProceedingJoinPoint jp) throws Throwable{
System.out.println("环绕前");//获得签名 Signature signature = jp.getSignature(); System.out.println("signature"+signature); //执行方法 Object proceed = jp.proceed(); System.out.println("环绕后!"); System.out.println(proceed);
}
```xml <!--方法三--> <bean id="annotationPointCut" class="com.study.diy.AnnotationPointCut"></bean> <!--开启注解支持 JDK(默认 proxy-target-class="false") 而 cglib 是proxy-target-class="true"--> <aop:aspectj-autoproxy proxy-target-class="true"/> 输出: 环绕前 signaturevoid com.study.service.UserServiceImpl.add() ====方法执行前====== 增加一个用户 环绕后! null ====方法执行后======
-
注意只有execution指示器是唯一的执行匹配,而其他的指示器都是用于限制匹配的。这说明execution指示器是我们在编写切点定义时最主要使用的指示器,在此基础上,我们使用其他指示器来限制所匹配的切点。
-
下图的切点表达式表示当Instrument的play方法执行时会触发通知。
-
-
我们使用execution指示器选择Instrument的play方法,方法表达式以
*
号开始,标识我们不关心方法的返回值类型。然后我们指定了全限定类名和方法名。对于方法参数列表,我们使用..
标识切点选择任意的play方法,无论该方法的入参是什么。 -
多个匹配之间我们可以使用链接符
&&
、||
、!
来表示 “且”、“或”、“非”的关系。但是在使用 XML 文件配置时,这些符号有特殊的含义,所以我们使用 “and”、“or”、“not”来表示。 -
举例:
限定该切点仅匹配的包是com.sharpcj.aopdemo.test1
,可以使用
execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..)) && within(com.sharpcj.aopdemo.test1.*)
在切点中选择 bean,可以使用:execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..)) && bean(girl)