AOP
面向切面编程 Aspect Oriented Programming
切面:切入点+通知
给容器中的组件进行增强,AOP其实做的事情就是将委托类组件替换为代理组件,取出的组件就是代理组件。AOP做的是更精细的增强,并不是所有的组件里的方法都做增强,而是做筛选,开发人员提供筛选条件。
切入点 → 筛选容器中的组件中要被增强的方法
通知 → 做什么样的增强
编程术语
Target:目标类、委托类、被代理类
Proxy:代理类
PointCut:切入点
Advice:通知
Aspect:切面
JoinPoint:连接点,增强过程中的点。通过JoinPoint可以拿到一些参数(委托类对象、代理类对象、方法、参数)
Weaver:织入
案例
案例1使用工厂方法注册代理
FactoryBean
@Component public class ServiceFactoryBean implements FactoryBean<UserService> { @Qualifier("userServiceImpl") @Autowired UserService userService; //维护一个成员变量,用于增强 @Override public UserService getObject() throws Exception { Object proxy = Enhancer.create(UserServiceImpl.class, new InvocationHandler() { @Override public Object invoke(Object o, Method method, Object[] objects) throws Throwable { System.out.println("使用FactoryBean的动态代理进行增强"); method.invoke(userService, objects); return null; } }); return (UserService) proxy; } @Override public Class<?> getObjectType() { return UserService.class; } }
这里一定要注意,因为我们的动态代理也会实现委托类的接口,所以在取出委托类的组件时要按照类型+id来取值,因为此时实现了接口,那就相当于有了两个这个类型的组件
@Autowired @Qualifier("serviceFactoryBean") UserService userServiceProxy; @Test public void test3(){ userServiceProxy.demo(); }
SpringAOP
Spring提供了一个生成动态代理对象的ProxyFactoryBean
通过getObject方法来生成代理对象,那么需要指定委托类对象,也需要指定增强
通知要实现接口 → MethodInterceptor → invoke
@FunctionalInterface public interface MethodInterceptor extends Interceptor { Object invoke(MethodInvocation var1) throws Throwable; }
实现接口在导包的时候,一定要注意导的是org.aopalliance下的包
@Component public class UserServiceInter implements MethodInterceptor { @Override public Object invoke(MethodInvocation methodInvocation) throws Throwable { System.out.println("AOP的动态代理"); Object proceed = methodInvocation.proceed(); //执行委托类的方法 System.out.println("AOP的动态代理"); return proceed; } }
通知需要做的事情类似于InvocationHandler
1.委托类方法的执行
2.增强
通过配置文件来注册AOP的对象,target表示的是委托类,interceptorNames表示的就是通知的组件名
<!--ProxyFactoryBean已经编译好了--> <bean id="userServiceProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <!--委托类组件--> <property name="target" ref="userServiceImpl"/> <!--interceptor是什么,长什么样 → 通知--> <property name="interceptorNames" value="customInterceptor"/> </bean>
这里在取出的时候也要注意动态代理会实现委托类的接口,就会多产生一个该类型的实例,就不能使用取出类型的方法取出。
只有是在容器中注册一个代理类才会出现这种情况,如果没有在容器中注册,就不会。
AspectJ
Pointcut
切入点表达式→匹配的方法→判断方法是否在增强范围内→筛选方法进行增强
execution
大的范围指定,指定满足特征的方法
表达式的规范:execution(修饰符 返回值 包名、类名、方法名(形参))
例如:public void com.cskaoyan.service.UserService.sayHello();
1.能否省略
2.能否通配
3.特殊的用法
修饰符
可以省略→直接省略不写就代表任意的修饰符
返回值
不能省略,但可以使用通配符*来表示返回值可以是任意值
如果是Pojo类,那就需要写一个全限定类名,这里面也可以用通配符
例如:com.cskaoyan.bean.User→com.caskaoyan.*.User
包名、类名、方法名
只能省略一部分:包名的第一级和方法名不能省略(头尾不能省略),中间的任意一部分都可以省略
使用…进行省略,这里可以先写出完整的,然后中间的部分删去不写即可,只要用了…就表示省略,如果有多级的,那么也只需要使用一次…
public void com.cskaoyan.service.UserServiceImpl.sayHello();
中间加黑的部分都可以省略
例如:
execution(void com…service.UserServiceImpl.sayHello())
execution( void com.cskaoyan…UserServiceImpl.sayHello()😉
execution( void com.cskaoyan…sayHello()😉
可以使用通配符*,任意位置都可以使用
- execution(void *.cskaoyan…sayHello())
- execution(void com.cskaoyan…say*())
形参
public void com.cskaoyan.service.UserServiceImpl.sayHello()
可以省略不写→表示无参方法
可以使用通配符
*表示任意类型的单个字符
…表示任意参数(类型任意,数量任意)
同样的,如果是pojo类作为形参,那么需要写全限定类名
增强service层的任意方法:
execution(* com.cskaoyan.service…*(…))
前面的第一个…表示的是省略中间的包,*表示的任意方法名,方法参数的…表示的是任意参数
使用
需要引入依赖:aspectjweaver
<!--aspectjweaver → groupid为org.aspectj--> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.7</version> </dependency>
引入aop的schema约束
<aop:config> <aop:pointcut id="mypointcut" expression="execution(* com.cskaoyan.service..*(..))"/> </aop:config>
@annotation
指定要增强的具体的方法,注解的形式
@annotation(自定义的注解的全限定类名)
首先需要自定义一个注解,自定义的注解写在容器中的组件的哪个方法上,哪个方法就被划入了增强的范围
@Target(ElementType.METHOD) //注解可以出现在方法上 @Retention(RetentionPolicy.RUNTIME) //注解生效时间:runtime运行时 public @interface CountTime { }
<aop:pointcut id="mypointcut2" expression="@annotation(com.cskaoyan.anno.CountTime)"/>
Advisor
Advisor:pointcut + advice (实现MethodInterceptor接口)
<aop:config> <aop:pointcut id="myPointcut" expression="execution(* com.fh.service.*(..))"/> <aop:pointcut id="myPointcut2" expression="@annotation(com.fh.anno.CountTime)"/> <aop:advisor advice-ref="countTimeAdvice" pointcut-ref="myPointcut2"/> </aop:config>
@Component public class CountTimeAdvice implements MethodInterceptor { @Override public Object invoke(MethodInvocation methodInvocation) throws Throwable { long start = System.currentTimeMillis(); Object proceed = methodInvocation.proceed(); long end = System.currentTimeMillis(); System.out.println(methodInvocation.getMethod().getName() + "执行时间:" + (end - start)); return proceed; } }
pointcut属性:切入点,可以直接写execution表达式,也可以用注解
advisor:advice-ref表示的是通知的全限定类名,pointcut-ref表示指定的切入点
进行增强后,取出的组件就是增 强后的代理组件,动态织入将委托类替换为代理类;此外,也需要看代理的对象是否在容器中注册,只有注册了才会起冲突。AspectJ中的没有注册,采取的是动态织入的方式。
Aspect
Aspect = pointcut + advice(提供好一些包含时间属性的通知)
具体的增强还是需要自己来实现
时间属性:相对于委托类方法的执行时间,是一个相对时间,只需要关注相对委托类的时间就行,不需要关注通知之间的时间。
- Before :在委托类方法之前执行
- Around:一部分在之前,一部分在之后,类似于InvocationHandler,会使用invoke来调用委托类的方法
- After:在委托类方法执行之后执行,类似于finally,不管发生什么情况,对应的方法一定会执行
- AfterThrowing:在抛出异常后执行,类似于catch
- AfterRetruning:在委托方法执行之后执行,可以获得执行的return的结果,如果抛出异常则不会执行
1.如何将通知和方法结合
将方法写在切面类中,作为通知方法
2.切面组件
1.注册组件
2.标记切面组件
@Component public class CustomAspect { //before通知方法,方法名任意 public void mybefore() { System.out.println("Before通知:正道的光1.0"); } //after通知方法,方法名任意 public void myafter() { System.out.println("After通知:照在大地上1.0"); } //around通知方法 → 类似于InvocationHandler的invoke、类似于MethodInterceptor的invoke //要包含委托类方法的执行和增强两部分 //around方法的返回值作为代理对象的执行结果 → 返回值为Object类型 public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { System.out.println("around的前半部分"); //委托类方法的执行 → ProceedingJoinPoint Object proceed = proceedingJoinPoint.proceed();//执行的是委托类的方法 //增强 System.out.println("around的后半部分"); return proceed + " niupi"; } //afterReturning可以获得委托类方法的执行结果,意味着能够接收到这个值 → 形参中以Object接收 //形参名自己定义 public void afterReturning(Object returnValue) { System.out.println("afterReturning通知:" + returnValue); } //afterThrowing通知 抛出异常的时候执行 → 可以获得你抛出的异常对象 //使用Exception或Throwable来接收 public void afterThrowing(Throwable e) { System.out.println("afterThrowing通知:" + e.getMessage()); } }
<aop:aspect ref="customAspect"/>
如何将通知和切入点结合
使用配置文件:
<aop:config> <aop:pointcut id="mypointcut" expression="execution(* com.cskaoyan.service..*(..))"/> <aop:pointcut id="mypointcut2" expression="@annotation(com.cskaoyan.anno.CountTime)"/> <aop:aspect ref="customAspect"> <!--子标签--> <!--指定方法为什么通知方法、通知是啥切入点--> <!--pointcut属性:直接写切入点表达式--> <!--pointcut-ref属性:引用了切入点的id--> <aop:before method="mybefore" pointcut-ref="mypointcut"/> <aop:after method="myafter" pointcut-ref="mypointcut"/> <aop:around method="around" pointcut-ref="mypointcut"/> <!--returning属性:after-returning通知方法中哪一个Object类型的形参来接收委托类方法的执行结果--> <aop:after-returning method="afterReturning" pointcut-ref="mypointcut" returning="returnValue"/> <!--throwing属性:afterThrowing通知方法中哪一个Exception或Throwable类型的形参来接收委托类方法抛出的异常--> <aop:after-throwing method="afterThrowing" pointcut-ref="mypointcut" throwing="e"/> </aop:aspect> </aop:config>
如果委托类方法抛出异常
Around通知的后半部分和AfterReturning通知执行不到
After通知是可以执行到的,它类似于finally
共有的属性:method、pointcut(-ref)
特有属性:afterReturning → returning、AfterThrowing → throwing ,让通知方法的形参能够接收到一些值
aop:pointcut的作用范围
aop:pointcut标签可以写在aop:aspect标签里,这时候就表示的是局部变量
<aop:config> <!--全局变量:整个aop:config标签里都可以使用--> <aop:pointcut id="mypointcut" expression="execution(* com.cskaoyan.service..*(..))"/> <aop:aspect ref="customAspect"> <!--局部变量:只能在当前aop:aspect标签里使用--> <aop:pointcut id="mypointcut3" expression="execution(* com.cskaoyan.service..*(..))"/> </aop:aspect> </aop:config>
JoinPoint连接点
通过连接点,可以获得增强过程中的一些值:委托类对象、代理对象、方法、
提供这些值可以让业务更加灵活
写在Before通知或者Around通知的形参中
public interface ProceedingJoinPoint extends JoinPoint { //意味着委托类方法的参数不做变化 Object proceed() throws Throwable; //意味着委托类方法传入你的参数 Object proceed(Object[] var1) throws Throwable; } 在around通知中,可以直接使用ProceedingJoinPoint的实例来获得对应的值
before通知里的使用
//before通知方法,方法名任意 public void mybefore(JoinPoint joinPoint) { System.out.println("Before通知:正道的光1.0"); Object proxy = joinPoint.getThis(); //proxy代理对象 Object target = joinPoint.getTarget();//target委托类对象 //拿到方法 Signature signature = joinPoint.getSignature(); String name = signature.getName(); //Signature的name就是方法名 //委托类方法执行的参数 Object[] args = joinPoint.getArgs(); }
around通知里的使用
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { //比如说,我们将传给委托类方法的参数拿到 Object[] args = proceedingJoinPoint.getArgs(); //如果执行的是sayHello5我们做一个个性化的处理 if ("sayHello5".equals(proceedingJoinPoint.getSignature().getName())) { //个性化的处理 //原来传入的参数是songge → 现在给改一下 改成songge and ligenli args[0] += " and ligenli"; } System.out.println("around的前半部分"); //委托类方法的执行 → ProceedingJoinPoint //Object proceed = proceedingJoinPoint.proceed();//执行的是委托类的方法的参数 Object proceed = proceedingJoinPoint.proceed(args);//执行的是委托类的方法,参数被修改 //增强 System.out.println("around的后半部分"); return proceed + " niupi"; }
通过around可以对参数进行修改,更加灵活了,返回值就是最终的方法执行的结果
注解使用aspectj
需要做的事
- aop:pointcut标签配置了切入点以及对应的切入点表达式
- 指定组件为切面组件
- 指定切面组件中的方法为一个什么样的通知方法
- 将方法和切入点绑定起来
打开AspectJ的注解开关
<aop:aspectj-autoproxy/>
注册切面类
在组件上增加 一个@Aspect注解,将当前组件标记为切面组件;注意@Component不能省
@Aspect @Component public class CustomAspect { }
切入点配置
以方法的形式存在:体现出切入点的id和表达式
- id:方法名作为切入点的id
- 表达式:@Pointcut的value属性
@Pointcut("execution(* com.cskaoyan.service..*(..))") public void mypointcut() {//只需要这个方法的方法名 }
指定方法为什么通知方法
在切面类中的方法上增加上通知注解:@Before、@After、@Around、@AfterReturning、@AfterThrowing
将方法与切入点绑定
利用通知注解的value属性
- 直接写切入点表达式
- 引用切入点对应的方法
//@Before("execution(* com.cskaoyan.service..*(..))") //直接写切入点表达式 @Before("mypointcut()") //引用方法名时要有一对括号 public void mybefore(JoinPoint joinPoint) { } //afterReturning可以获得委托类方法的执行结果,意味着能够接收到这个值 → 形参中以Object接收 //形参名自己定义 @AfterReturning(value = "mypointcut()",returning = "returnValue") public void afterReturning(Object returnValue) { System.out.println("afterReturning通知:" + returnValue); } //afterThrowing通知 抛出异常的时候执行 → 可以获得你抛出的异常对象 //使用Exception或Throwable来接收 @AfterThrowing(value = "mypointcut()",throwing = "e") public void afterThrowing(Throwable e) { System.out.println("afterThrowing通知:" + e.getMessage()); }