Spring AOP应用
建议阅读AOP相关文章顺序:
这里直接对 Spring AOP 以
注解方式
实现和配置文件
方式实现的应用。本人写的代码仓库位置:gitee.com/old_yogurt/...
主要是基于spring 源码的5.0做的;想看Spring AOP源码所有类功能归纳说明,流程流转看下面的文章;
在 gitee.com/old_yogurt/... 工程中对多数AOP 代码做的注释,每个package-info.java都有对该包中类功能的归类说明注释。
最精彩的 第六节对于通知循环的解释。
一、注解、配置的专业术语介绍
这三个是我们写代码看得见摸得着的:
-
通知(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"/>
代码位置 前置通知的下面。javabefore 面包片 ... Around 西红柿片 ... 中间夹牛排~~~ Around 西红柿片 ... afterReturning 芝士片 ... after 面包片 ...
-
情况二:
(注意这里的
before 面包片 ...
输出在Around 西红柿片 ...
后面);说明环绕通知输出在前置通知前面。这种情况出现的原因是:配置文件中
<aop:around pointcut-ref="hamburgerPointcut" method="around"/>
代码位置 前置通知的上面。javaAround 西红柿片 ... 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()方法的类型。