Spring AOP应用

一、注解、配置的专业术语介绍

这三个是我们写代码看得见摸得着的:

  • 通知(Advice): AOP 框架中的增强处理。通知描述了切面何时执行以及如何执行增强处理。

    ​ (BeforeAdvice 前置通知,AfterAdvice 后置通知等,这些就是通知。)

  • 切入点(PointCut): 可以插入增强处理的 连接点

  • 切面(Aspect): 切面是 通知 和 切点 的结合。

这三个说实话,不看源码,都是在瞎说:

  • 连接点(JoinPoint): 连接点表示应用执行过程中能够插入切面的一个点,这个点可以是方法的调用、异常的抛出。在 Spring AOP 中,连接点总是方法的调用。

    这个连接点术语 不易说明,看源码才能多少明白,就是拦截器链 执行整个通知的流转中,方法调用的**点**。

  • 引介(Introduction):引介是一种特殊的增强,引介允许我们向现有的类添加新的方法或者属性。

    ​ 这样,即使一个业务类原本没有实现某个接口,通过AOP的引介功能,我们可以动态地为该业务类添加接口的实现逻辑,让业务类成为这个接口的实现类。

    ​ 想看 这个功能的话,可以看gitee.com/old_yogurt/… 我这个项目里的 aopintroduction 包中,关于引介增强器的demo。

  • 织入(Weaving): 将增强处理添加到目标对象中,并创建一个被增强的对象,这个过程就是织入。(把切面应用到目标对象并创建新的代理对象的过程)

  • 增强器(Advisor):也是 通知 + 切入点的结合;但是增强器也可以是一个拦截器,引介(也可以叫做一个增强器,也是一个拦截器);这些都是本人在看源码时候自身体会,可能也不太对。

二、表达式介绍

AspectJ指示器描述
arg()限制连接点 匹配参数为指定类型 的执行方法
@arg()限制连接点 匹配参数由指定注解标注 的执行方法
execution()用于 匹配是连接点 的执行方法
this()限制连接点 匹配AOP代理的Bean引用 为指定类型 的类
target()限制连接点 匹配目标对象为指定类型 的类
@target()限制连接点 匹配特定的执行对象,这些对象对应的类要具备指定类型的注解
within()限制连点 匹配指定的类型
@within()限制连点 匹配指定注解所标注的类型 (当使用 Spring AOP时,方法定义在由指定的注解所标注的类里)
@annotation限制匹配带有指定注解连接点

网上摘的图;切入点表达式

image.png

三、依赖引入

可以参看本人 a-spring-test-pro项目的中 .gradle文件内容

只用 spring-context、spring-aop两个工厂就行了。

 

java

复制代码

dependencies { compile(project(":spring-context")) compile(project(":spring-aop")) }

四、注解方式应用

(demo所在 a-spring-test-pro 的 aop包中)

目标接口、实现类:

 

java

复制代码

public interface IAnimal { String tails(); } @Service public class Cat implements IAnimal { @Override public String tails() { System.out.println("小猫的尾巴长"); return "小猫"; } } @Service public class Dog implements IAnimal { @Override public String tails() { System.out.println("小狗的尾巴短"); return "小狗"; } }

注解启动自动代理:

 

java

复制代码

@Configuration @ComponentScan(basePackages = "aop") // 要扫描调包 // proxyTargetClass = true 指定proxyTargetClass来强制执行CGLIB代理,或者指定一个或多个接口来使用JDK动态代理。这会源码中介绍 @EnableAspectJAutoProxy(proxyTargetClass = true) public class AspectConfig { }

切面类:

 

java

复制代码

@Aspect @Component public class AnimalAspectJ { @Pointcut("execution(* aop.service.IAnimal.tails(..))") public void pointCut(){ } @Before("pointCut()") public void before(){ System.out.println("小猫小狗都有尾巴 before..."); } @After("pointCut()") public void after(){ System.out.println("小猫小狗都有尾巴 after..."); } @AfterReturning("pointCut()") public void afterReturning() { System.out.println("小猫小狗都有尾巴 afterReturning..."); } @AfterThrowing("pointCut()") public void afterThrowing() { System.out.println("小猫小狗都有尾巴 afterThrowing..."); } /** * 该通知会将目标方法封装起来, * 并且 Around before ... -> 在 @Before前; * Around after ... -> 在 @After前. * @param pj */ @Around("pointCut()") public void xxx(ProceedingJoinPoint pj) { try { System.out.println("Around before ..."); pj.proceed(); System.out.println("Around after ..."); } catch (Throwable throwable) { throwable.printStackTrace(); } } }

测试类:

 

java

复制代码

public class AppTest { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AspectConfig.class); Dog dog = context.getBean("dog", Dog.class); //Cat cat = (Cat) context.getBean("cat"); dog.tails(); //cat.tails(); } }

测试结果:

 

java

复制代码

Around before ... 小猫小狗都有尾巴 before... 小狗的尾巴短 Around after ... 小猫小狗都有尾巴 after... 小猫小狗都有尾巴 afterReturning...

五、配置方式应用

(demo所在 a-spring-test-pro 的 xmlaop包中)

目标接口、目标类:

 

java

复制代码

/** * 汉堡 */ public interface HamburgerService { public void steak(); } @Service public class HamburgerServiceImpl implements HamburgerService{ @Override public void steak() { System.out.println("中间夹牛排~~~"); } }

切面类:

 

java

复制代码

public class HamburgerAspectConfig { public void before() { System.out.println("before 面包片 ..."); } public void after() { System.out.println("after 面包片 ..."); } public void afterReturning() { System.out.println("afterReturning 芝士片 ..."); } public void afterThrowing(){ System.out.println("afterThrowing 汉堡烤"); } public void around(ProceedingJoinPoint pj) { try { System.out.println("Around 西红柿片 ..."); pj.proceed(); System.out.println("Around 西红柿片 ..."); } catch (Throwable throwable) { throwable.printStackTrace(); } } }

配置文件:这里直接连 配置文件头也复制进来

 

java

复制代码

<?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" xmlns:cache="http://www.springframework.org/schema/cache" xmlns:tx="http://www.springframework.org/schema/cache" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd"> <!-- 注入bean --> <bean id="hamburger" class="xmlaop.service.HamburgerServiceImpl"></bean> <!-- 切面 --> <bean id="hamburgerAspectConfig" class="xmlaop.aspect.HamburgerAspectConfig"></bean> <aop:config proxy-target-class="false"> <!-- 切入点 expression这个表达式的方法,就是切入点--> <aop:pointcut id="hamburgerPointcut" expression="execution(* xmlaop.service.HamburgerService.steak())"/> <!-- 切面--> <aop:aspect id="hamburgerAspect" ref="hamburgerAspectConfig"> <!--前置通知--> <aop:before pointcut-ref="hamburgerPointcut" method="before"></aop:before> <!--后置通知--> <aop:after pointcut-ref="hamburgerPointcut" method="after"></aop:after> <!--返回通知--> <aop:after-returning pointcut-ref="hamburgerPointcut" method="afterReturning"/> <!--异常通知 : 只有在目标方法抛出异常时才执行--> <aop:after-throwing pointcut-ref="hamburgerPointcut" method="afterThrowing"/> <!--这特喵,这环绕,在xml配置里面还会受位置影响,影响执行结果,这个环绕通知如果放在 前置通知前面,那么环绕通知会先执行。--> <!--放在这里,会在前置通知执行完之后再执行--> <!--环绕通知--> <aop:around pointcut-ref="hamburgerPointcut" method="around"/> </aop:aspect> </aop:config> </beans>

测试类:

 

java

复制代码

public class XmlAopConfigTest { public static void main(String[] args) { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml"); HamburgerService hamburgerService = (HamburgerService)context.getBean("hamburger"); hamburgerService.steak(); } }

测试结果:

发现这里有两种情况:

  • 情况一:

    (注意这里的 before 面包片 ... 输出在 Around 西红柿片 ...前面);说明前置通知先输出,再输出环绕通知。

    这种情况出现的原因是:配置文件中 <aop:around pointcut-ref="hamburgerPointcut" method="around"/> 代码位置 前置通知的下面

     

    java

    复制代码

    before 面包片 ... Around 西红柿片 ... 中间夹牛排~~~ Around 西红柿片 ... afterReturning 芝士片 ... after 面包片 ...
  • 情况二:

    (注意这里的 before 面包片 ... 输出在 Around 西红柿片 ... 后面);说明环绕通知输出在前置通知前面。

    这种情况出现的原因是:配置文件中 <aop:around pointcut-ref="hamburgerPointcut" method="around"/> 代码位置 前置通知的上面

     

    java

    复制代码

    Around 西红柿片 ... before 面包片 ... 中间夹牛排~~~ Around 西红柿片 ... after 面包片 ... afterReturning 芝士片 ...

六、总结

​ 这也说明了,配置文件方式,环绕通知的顺序会受到位置的影响(进而影响了 前置通知)

​ 但是其实我们对比看 注解方式配置时候发现,配置文件方式情况二和注解方式相同。

​ 其实,在我们配置这些的时候,Spring AOP 会为我们按通知类型排序的,因为当我们执行目标方法的时候,其实执行的是代理方法(被Spring AOP代理的),会有一个拦截器链(通知链)

​ 还有,注解方式 通知注解的顺序,不会影响输出结果。

​ 因为(我擦,好难解释),有一个类 ReflectiveAspectJAdvisorFactory,该类里面有一个 METHOD_COMPARATOR属性,就是通知比较器,会在获取注解方式的通知的时候( ReflectiveAspectJAdvisorFactory#getAdvisorMethods )进行排序的。(后面写 AOP 源码执行流程的时候可能会写这个排序吧。)

​ 对比一下 ReflectiveMethodInvocation类中 process()方法中的 interceptorsAndDynamicMethodMatchers属性 ,执行器链中通知的顺序,注意真正的执行顺序是倒序执行的,从index 5开始的 到 index 0 。

  • 注解中的:不论通知注解的顺序都是下面的结果:

    image-20220422180842781.png

  • 配置文件方式:

    • 情况一:(环绕通知在 前置通知后面)

      image-20220422181153831.png

    • 情况二:(环绕通知在 前置通知前面)

      image-20220422181256949.png

  • 上面说的是配置文件方式,环绕通知的顺序会受到位置的影响(进而影响了 前置通知);另外三个通知是不会受到排序影响的,为什么呢? 还是从每个通知的处理入手。

    • 先说 @After 通知,找到源码里面的 AspectJAfterAdvice类的 invoke方法,在后置通知时候,调用的该方法:

      注意,mi.proceed()是递归(后面将源码流程再讲递归);这里只要知道@After 通知方法会在 finally块 里面执行,也就是一定是最后执行的即可。

       

      java

      复制代码

      @Override public Object invoke(MethodInvocation mi) throws Throwable { try { // 执行方法 // 调用拦截器链 org.springframework.aop.framework.ReflectiveMethodInvocation.proceed() return mi.proceed(); } finally { // 注意这里是 finally块中 // 调用 org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod() 通知基础类的方法 // 执行通知方法 invokeAdviceMethod(getJoinPointMatch(), null, null); } }
    • 然后,@AfterThrowing ;在catch块中执行,就不多说了,和上面一样

       

      java

      复制代码

      @Override public Object invoke(MethodInvocation mi) throws Throwable { try { // 执行方法 // 调用拦截器链 org.springframework.aop.framework.ReflectiveMethodInvocation.proceed() return mi.proceed(); } catch (Throwable ex) { // 注意这里是在 catch块中 if (shouldInvokeOnThrowing(ex)) { // 调用 org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod() 通知基础类的方法 // 执行通知方法 invokeAdviceMethod(getJoinPointMatch(), null, ex); } throw ex; } }
    • 再然后 @AfterReturning,看 AspectJAfterReturningAdvice

       

      java

      复制代码

      @Override public void afterReturning(@Nullable Object returnValue, Method method, Object[] args, @Nullable Object target) throws Throwable { // 这里会有判断,当方法调用结束之后,才回进入if里面 执行后置返回通知 if (shouldInvokeOnReturnValueOf(method, returnValue)) { // 调用 org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod() 通知基础类的方法 // 执行通知方法 invokeAdviceMethod(getJoinPointMatch(), returnValue, null); } }

上面说的这些方法都是在 拦截器链(或者叫通知链)执行的,整个就是在 ReflectiveMethodInvocation类中 process()方法 调用,递归的过程。(想了解还要看源码执行流程,本人会在 Spring AOP源码执行流程文章里面写),这里先贴一个递归调用流程,从[1] → [7],层次往里面走,然后再从 [7]->[1]往外出,注意看invoke()方法的类型。

image-20220424105405817.png

  • 27
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值