一、注解、配置的专业术语介绍
这三个是我们写代码看得见摸得着的:
-
通知(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 | 限制匹配带有指定注解连接点 |
网上摘的图;切入点表达式
三、依赖引入
可以参看本人 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 。
-
注解中的:不论通知注解的顺序都是下面的结果:
-
配置文件方式:
-
情况一:(环绕通知在 前置通知后面)
-
情况二:(环绕通知在 前置通知前面)
-
-
上面说的是配置文件方式,环绕通知的顺序会受到位置的影响(进而影响了 前置通知);另外三个通知是不会受到排序影响的,为什么呢? 还是从每个通知的处理入手。
-
先说 @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()方法的类型。