spring 通过 aop 主要用于在调用接口的前后,做一些操作,例如打印日志等。
对 aop 有所了解的人都知道,aop 是通过代理模式实现的。在前面的文章《spring源码中设计模式的使用》中也写过。
当我们想在一个对象中的方法的前后,增加一些统一的逻辑时,可以根据这个对象生成一个代理对象。想要调用目标方法时,不使用原始对象,而使用代理对象来调用此方法。这个时候就可以在方法的前后增加一些自己的逻辑。
所以,我们首先看看 aop 中代理对象的生成过程。
我写了个切面类 LogAspect,用于在 PaymentService 接口的所有方法前后打印日志。本文以这个场景为例,详细讲解 spring aop 的源码。
@Slf4j
@Aspect
@Component
public class LogAspect {
@Pointcut("execution(* cn.xujingyi.sample.service.PaymentService.*(..))")
public void pointCut() { }
@Before(value = "pointCut()")
public void before(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
log.info("enter method[" + methodName + "] before advice. " +
"current time > " + System.currentTimeMillis());
}
@After(value = "pointCut()")
public void after(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
log.info("enter method[" + methodName + "] after advice. " +
"current time > " + System.currentTimeMillis());
}
}
上一篇文章《spring ioc源码详解》中,详细讲解了 bean 的实例化、初始化的源码。但是在创建代理对象的地方跳过了。这里详细讲解下创建代理对象的源码。
回到 doCreateBean 的初始化 bean 的地方,可以发现此时这个待返回的 exposedObject,目前还是真实对象。就是在这个 initializeBean 方法中,生成并返回代理对象的。
进入 initializeBean 方法。上一篇文章中也提了,在这个方法里,首先通过 BeanPostProcessors,在 bean 初始化之前做点事情,然后调用 bean 的初始化方法进行初始化,最后通过 BeanPostProcessors,在 bean 初始化之后做点事情。
生成代理对象就是在这个 afterInitialization 中做的。在这个方法里面,循环遍历所有 BeanPostProcessor,调用它的 postProcessAfterInitialization 方法做后置处理。其中,用于生成代理对象的,就是 AnnotationAwareAspectJAutoProxyCreator,它的 proxyTargetClass 值为 true,且参数 advisorAdapterRegistry 中有三个适配器 adapters,就是后面用来将通知转化为拦截器的。
进入 postProcessAfterInitialization 方法,
真正进行处理的地方是 wrapIfNecessary 方法。里面有一段代码,很明显就是用来创建代理对象的。
首先,它在里面根据我们的切面配置,获取对于的通知器,我们在切面中配置了 before 和 after 两个通知,这里就获取到了。还有一个默认拦截器,以及一个事务的通知器,事务这个是因为我对这个方法加了 @Transactional 注解,下一篇文章会讲事务的源码。
然后就是调用 createProxy 方法创建代理对象了!
这个方法里面,首先还是设置通知器等信息,然后设置了 frozen 属性,表示冻结配置,暂时不能修改配置了。最后调用 getProxy 获取代理对象。
首先是获取 AopProxy 对象,发现默认使用的是 cglib 代理,而不是 jdk动态代理。进方法看下
新版本是对实现类 PaymentServiceImpl 进行代理的,不是接口,所以默认使用的是 cglib 代理。以前老版本是对接口代理,默认使用的是 jdk 动态代理。
接下来就是调用 AopProxy 类的 getProxy 获取代理类了,这个属于 cglib 的内容了,就不看了。
返回到 createProxy 的地方,此时已经获取到代理类了,从变量名上也可以看出来,这是个代理类。一步步返回,直到刚刚循环遍历 BeanPostProcessors 的地方,也可以看到返回结果是一个代理类。
回到刚才调用 initializeBean 的地方,发现此时的 exposedObject 是代理类。
接着,会触发一次 getSingleton,此时在缓存中查询不到,方法结束,将代理对象 exposedObject 返回。回到那个带回调函数的 getSingleton 方法中(上一篇讲 ioc 源码的文章中讲过),将当前的代理对象放入缓存
这个 singletonObjects 上一篇文章中讲过,是 spring 三级缓存中的第一级,也叫做单例缓存池,用于存放所有初始化完成的 bean。以后取 bean 也是在这个 singletonObjects 中获取的。
通过截图可以看出,此时单例缓存池中,beanName 为 "paymentServiceImpl" 的 bean,真正存放的是一个 cglib 代理对象!
到这里为止,创建代理对象的整个流程,就讲解完了。接下来就是使用这个代理对象来调用方法了。
编写测试类,调用 PaymentService 的 pay 方法
开启 debug,然后 F7 进入 pay 方法,发现进入的是 cglib 的代理类。上面我们讲过,对于 "paymentServiceImpl" 这个 bean 来说,单例缓存池中放的是代理对象。而我们通过 @Autowire 注解注入的属性,实际上也是通过 getBean 方法从单例缓存池中获取的,所以这里的 paymentService 是一个代理对象。也只有在使用代理对象调用方法时,才能在方法的前后做一些事情。
往下继续看代码,发现了获取拦截器链
这个就是我们在 LogAspect 类中定义的前置和后置通知对应的拦截器 。
进入 advised.getInterceptorsAndDynamicInterceptionAdvice 方法
首先获取 advice 适配器,适配器有3种,是用来将我们定义的 advice 转换成 intercepter 的,最终使用的 intercepter。
然后获取所有的通知,可以看到有三个,一个是默认的,不用去管它。还有两个是我们在 LogAspect 中配置的前置和后置通知。
然后遍历上面获取的所有通知,判断当前调用的方法,是否跟 pointcut 的 execution 表达式匹配,可以看到此处我调用的 pay 方法是匹配的,因为我在 execution 中写的是 PaymentService 中的所有方法。
当确定当前调用的方法需要被拦截时,调用 registry.getInterceptors 将 advice 转化为 intercepter。这个 advice 就是我们在 LogAspect 中配置的 before 和 after 通知。
对于每一个通知 advice,都循环遍历 adapters,通过 supportsAdvice 方法判断那个 adapter 符合这个 advice。例如当前的遍历到的 advice 是 AspectJMethodBeforeAdvice,则发现 MethodBeforeAdviceAdapter 跟它匹配。
知道哪个 adapter 跟此通知匹配后,从此 adapter 中获取对应的拦截器 Intercepter。在此处就是 MethodBeforeAdviceInterceptor。之后将拦截器放入拦截器链,也就是 interceptorList 集合中。
这里的 adapter,运用的是设计模式中的“适配器模式”。这个在前面的文章《spring源码中设计模式的使用》中也写过了。
到此为止,拦截器链就获取结束了。我画了个流程图:
获取了拦截器链后,就真正开始执行拦截器中的方法,并调用目标方法了。
首先创建一个 CglibMethodInvocation,然后调用 proceed 方法,实际调用的是它的父类 ReflectiveMethodInvocation 中的 proceed 方法。我们进入 proceed 方法看看,这个是重点。
currentInterceptorIndex 初始为 -1。interceptorsAndDynamicMethodMatchers 就是在前面获取的3个拦截器。通过判断 currentInterceptorIndex 是否等于 interceptorsAndDynamicMethodMatchers 的 (size - 1),来知道目前应该调用的是拦截器,还是目标方法。
然后就是循环这个 interceptorsAndDynamicMethodMatchers,一次调用拦截器的 invoke 方法
比如当前是 MethodBeforeAdviceInterceptor, 进入它的 invoke 方法查看。它在调用目标方法之前,首先调用了拦截器中的方法
具体是在这个 advice.before 方法中调用的,点进去看下,好几层,一直进入到 invokeAdviceMethodWithGivenArgs 方法中。
这里运用了java 的反射机制。aspectJAdviceMethod 是个 LogAspect 中的 before 方法对应的 Method 对象,调用它的 invoke 方法,就相当于调用了 before 方法。
所以,执行完 invoke,在日志中就可以看到我们定义的打印日志了。
执行完前置通知后,继续调用 proceed 方法,重新进入前面的 proceed 方法
此时 currentInterceptorIndex 加了1,获取到的 advice 是 AspectjAfterAdvice。执行它的 invoke 方法。在这个方法中,可以看到,直接调用 proceed 方法,继续调用下一个拦截器。而在 finally 中,调用后置方法 invokeAdviceMethod!从这里也可以看出,后置通知肯定是会执行的。
由于这个 AspectjAfterAdvice 已经是最后一个了,后面没有其他拦截器了,所以 proceed 方法中,直接调用真实的 pay 方法。
从日志可以看出,目标方法已调用结束,里面我只是打印了一行日志。之后,就执行 finally 中的后置方法 invokeAdviceMethod
一步步 debug,层数比较深,最后会走到我们定义的后置方法中。
到这里为止,一个拦截方法的流程就全部走完了。
本文只讲了 before 和 after 通知。aop 还有两个通知,return 和 throw 通知没讲,因为平时用的 before 和 after 比较多。
还有一点要注意,after 通知最终是在 finally 中执行的,所以一定会执行。而 return 和 throw 就不一定了。
我们可以针对一个方法,做很多的拦截。spring 的事务功能也是通过 aop 来实现的。下一篇会详细讲解下 spring 事务的源码。