阅读本文之前建议先掌握的前置知识:
Spring AOP有常用的两种方式,一种是使用XML的方式,另一种是使用注解的方式。本文将详细的分析注解方式的实现原理。将会从如下几个点展开。
用于调试的代码如下:
代码远程仓库地址:https://gitee.com/gongsenlin/aoptest/tree/master
//启动类
package hdu.gongsenlin.aoptest;
import hdu.gongsenlin.aoptest.service.UserService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class AoptestApplication {
public static void main(String[] args) {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(Appconfig.class);
// jdk
// MyService a = (MyService) ac.getBean("userService");
// a.query();
//cglib
UserService a = ac.getBean(UserService.class);
a.query();
}
}
//配置类
package hdu.gongsenlin.aoptest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;
/**
* @author gongsenlin
* @version 1.0
* @date 2021-4-22 19:59
*/
@Configuration
@ComponentScan("hdu.gongsenlin.aoptest")
@EnableAspectJAutoProxy
public class Appconfig {
}
//切面类1
package hdu.gongsenlin.aoptest.aop;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.core.Ordered;
/**
* @author gongsenlin
* @version 1.0
* @date 2021-4-22 20:00
*/
@Aspect
@Component
public class Aop{
@Pointcut("execution(* hdu.gongsenlin.aoptest.service..*.*(..))")
private void pointcut(){}
@Pointcut("execution(* hdu.gongsenlin.aoptest.controller..*.*(..))")
private void pointcut2(){}
@Before("pointcut()")
public void beforeAdvice(){
System.out.println("前置增强1");
}
@Before("pointcut()")
public void abeforeAdvice(){
System.out.println("前置增强2");
}
@After("pointcut()")
public void afterAdvice(){
System.out.println("后置增强1");
}
@After("pointcut2()")
public void afterAdvice2(){
System.out.println("后置增强2");
}
}
//业务类
package hdu.gongsenlin.aoptest.service;
import org.springframework.stereotype.Component;
/**
* @author gongsenlin
* @version 1.0
* @date 2021-4-22 19:59
*/
@Component
public class UserService implements MyService {
@Override
public void query(){
System.out.println("query do");
}
@Override
public void f() {
}
}
public interface MyService {
void f();
void query();
}
主要是@EnableAspectJAutoProxy注解的作用。
该注解是一个复合注解,如下所示:
@Import注解可以注入一个类到Spring容器当中,在之前的博客中详细介绍过该注解的作用,博客地址如下:
《Spring中@Import注解的使用和实现原理》
ConfigurationClassPostProcessor解析启动时候的配置类Appconfig
由于Appconfig的注解中包含了@Import注解,那么会将@Import注解中的类 先缓存到Appconfig配置类的importBeanDefinitionRegistrars集合中。
ConfigurationClassPostProcessor扫描完了所有的类,执行加载和注册beanDefinition的时候
此时如果该类中包含了ImportBeanDefinitionRegistrar
那么会调用该类的registerBeanDefinitions方法进行注册beanDefinition。
也就来到了AspectJAutoProxyRegistrar中的registerBeanDefinitions方法。
该方法的作用就是将AnnotationAwareAspectJAutoProxyCreator注册到容器当中,这个类是Spring AOP的关键。
此时Spring中就有了Aspect AOP的功能
Spring是需要手动添加@EnableAspectJAutoProxy注解进行集成的,而SpringBoot中使用自动装配的技术,可以不手动加这个注解就实现集成。
在org/springframework/boot/spring-boot-autoconfigure/2.1.7.RELEASE/spring-boot-autoconfigure-2.1.7.RELEASE.jar!/META-INF/spring.factories中,添加了AopAutoConfiguration,AopAutoConfiguration中加上了@EnableAspectJAutoProxy注解。
有了AopAutoConfiguration配置类,后续的解析配置类的逻辑就和Spring是一样的了。
SpringBoot如何实现自动注入,可以参考博客《SpringBoot自动装配原理分析和实现自定义启动器》
了解Bean的生命周期的读者,一定清楚BeanPostProcessor接口的postProcessAfterInitialization方法。
AspectJAwareAdvisorAutoProxyCreator实现了BeanPostProcessor该接口,所以在每个Bean的生命周期中,都会执行AspectJAwareAdvisorAutoProxyCreator的postProcessAfterInitialization方法。
来到父类中AbstractAutoProxyCreator中的postProcessAfterInitialization。只有当earlyProxyReferences集合中不存在cacheKey的时候,才会执行wrapIfNecessary方法。该集合的作用在之前的博客中也说明了。
SpringAOP对象生成的时机有两个,一个是提前AOP,提前AOP的对象会被放入到earlyProxyReferences集合当中,若没有提前AOP那么会在Bean的生命周期的最后执行postProcessAfterInitialization的时候进行AOP动态代理。这一部分知识不清楚的读者可以阅读博客《Spring IOC—AOP代理对象生成的时机》。
没有进行AOP的对象会执行wrapIfNecessary方法,该方法中会去寻找通知链,若存在匹配的通知链,那么会进行AOP动态代理。
调用findCandidateAdvisors方法 得到所有的候选通知
然后调用findAdvisorsThatCanApply方法进行过滤,判断是否可以作用于当前Bean的增强处理。
调用父类的findCandidateAdvisors方法是加载使用XML方式配置的AOP声明,此处不讨论。
下面会调用aspectJAdvisorsBuilder的buildAspectJAdvisors方法去加载注解方式配置的AOP声明。
第一次进来的时候,此时缓存还是为空的,那么会进第一个if,从容器中拿到所有的beanName。
遍历BeanName,得到其对应的对象类型,调用 this.advisorFactory.isAspect(beanType)判断该类是否是切面类,也就是判断是否加了@Aspect注解。
是切面类的话,判断切面类的PerClause是否是Singleton,正常来说是的,接着构建用于生成通知链的工厂对象。然后基于该工厂对象生产通知链,调用this.advisorFactory.getAdvisors(factory)方法
getAdvisorMethods 获得类中所有的方法(包含父类的方法),pointCut方法除外。
遍历这些方法,判断是否是通知,构建通知并添加到通知链当中。
解析切面类中的成员变量,判断是否加了@DeclareParents注解,构建DeclareParentsAdvisor。
每个advisors中都包含了PointCut切点相关的内容。
测试代码得到的通知链结果如下:
回到BeanFactoryAspectJAdvisorsBuilder中的buildAspectJAdvisors方法。
如果这个切面bean是一个单例,那么将得到的通知链放入advisorsCache缓存当中,beanName作为key,通知链作为value。如果切面bean不是单例的,那么将用于构建通知链的工厂放入aspectFactoryCache缓存当中,beanName作为key,构建通知链的工厂作为value。
之后再访问BeanFactoryAspectJAdvisorsBuilder中的buildAspectJAdvisors方法的时候,直接根据beanName拿到对应的缓存即可。
目前拿到的通知链是还没有经历过匹配筛选的,方法来到AbstractAdvisorAutoProxyCreator中的findEligibleAdvisors方法
findCandidateAdvisors()方法返回的是候选的通知链,下面需要对里面的通知进行筛选,可以匹配上切点定义的通知才可以作用到当前的Bean上。
调用findAdvisorsThatCanApply方法进行通知链的匹配,匹配规则就根据切点中的execution表达式
候选通知原本有4个,筛选后,满足条件的仅有3个。其中afterAdvice2方法描述的通知所对应的切点表达式和当前加载的bean匹配不上。
筛选完之后,在调用extendAdvisors 添加一个ExposeInvocationInterceptor拦截器,用于再通知链的任意一环得到MethodInvocation对象。具体的作用下面会说明。
最后对通知进行排序,至此就得到了用于增强的通知链。
以上就是通知链的构建过程。简单来说就是遍历所有的类,判断类上是否加了@Aspect注解,对加了该注解的类再判断其拥有的所有方法,对于加了通知注解的方法构建出Advisor通知对象放入候选通知链当中。接着基于当前加载的Bean筛选通知,添加ExposeInvocationInterceptor拦截器,最后对通知链进行排序,得到最终的通知链。测试代码得到的最终通知链结果如下:
上一节中得到了用于增强的通知链后,代码定位到AbstractAutoProxyCreator中的wrapIfNecessary方法
第一个红色框框得到的结果是完整的通知链,第二个红色框框就是进行动态代理的地方了。下面来看看createProxy做了些什么。
构建ProxyFactory代理工厂,判断使用JDK动态代理还是使用CGLib动态代理
设置代理工厂的一些属性,例如通知链,被代理对象等。
然后调用工厂的getProxy方法得到代理对象。
基于工厂属性的设置,调用createAopProxy()得到不同的对象。
AOP的代理方式有两种,一种是Cglib代理,使用ObjenesisCglibAopProxy来创建代理对象,另一种是JDK动态代理,使用JdkDynamicAopProxy来创建代理对象。
都是调用其getProxy方法。
使用ObjenesisCglibAopProxy创建代理对象
ObjenesisCglibAopProxy继承自CglibAopProxy,getProxy方法来自父类,如下
构建Enhancer对象,设置属性。我们比较关心的通知链也被设置在Enhancer对象当中。
最后调用createProxyClassAndInstance得到基于Cglib动态代理的对象。
使用JdkDynamicAopProxy创建代理对象
此时修改一下测试代码中的UserService,让它实现一个接口。这样就会走JDK的动态代理。重新调试
getProxy代码如下:
这就是比较熟悉的JDK动态代理的方式了。JdkDynamicAopProxy本身也实现了InvocationHandler。所以这里newProxyInstance第三个参数传入的是自身。下一节再来看具体的调用链的执行过程。
通过java自带的工具HSDB,可以查看动态生成的类的源代码。
JDK动态代理生成的类
query方法内调用了父类中的InvocationHandler的invoke,也就是调用了JdkDynamicAopProxy中的invoke方法。JdkDynamicAopProxy实现了InvocationHandler接口。
下一篇博客将探究SpringAOP使用时的注意事项。
例如:同一个切面类中的多个同类型的通知的排列顺序,不同切面类中的同类型的通知的排列顺序等。