在使用Spring AOP的过程中,我们会用@Before,@Around,@After,@AfterReturning,@AfterThrowing注解标注在切面的方法上,以此来作为通知作用于某个切点上。这篇博客,我们就分析这些通知是如何执行的?
之前我们已经分析过Spring AOP使用JDK动态代理方式的源码执行逻辑 ,这里不会再介绍切面及通知的解析以及代理类的生成,着重讲解关于Advice的执行处理逻辑,也就是我们业务代码中依赖注入进来的代理类调用代理方法的具体逻辑。
Advisor的构成
Advisor,它是解析切面时,根据切面内每一个标注了@Before,@Around,@After,@AfterReturning,@AfterThrowing,@DeclareParents(特殊,一般用于代理原目标类没有实现的接口中的方法)注解的方法生成的。它主要包含了两部分:
-
Advice:通知,也就是我们要增强方法的具体逻辑。根据上边的AspectJ注解会依次有以下几种具体类型。
AspectJMethodBeforeAdvice:前置通知。
AspectJAroundAdvice:环绕通知。
AspectJAfterAdvice:后置通知。
AspectJAfterReturningAdvice:最终通知。
AspectJAfterThrowingAdvice:异常通知。 -
Pointcut:切点,定义了我们的通知逻辑起作用的方法或类。
我们先看一下其中一个AspectJMethodBeforeAdvice前置通知对应的Advisor的具体内容。
可以看到,每一个Advisor中基本包含了反射执行方法的各种元信息,当然最总重要的切面对象这里是肯定没有,等到我们的代理类调用代理方法时,从BeanFactory中就可以获取切面对象了,然后通过method.invoke(obj, parameter),这样基本就可以执行Advice逻辑了,当然这其中还有很多复杂的执行过程,接下来就开始分析Advice的执行逻辑。
代理过程调用分析
首先就是代理类调用,它会进入JdkDynamicAopProxy的invoke方法。
invoke方法内,核心的过程是它要先通过所有匹配此Class的Advisor去进一步过滤是否匹配此方法。然后将每一个合适的Advisor,获取其中的Advice。看一下这个处理过程的源码。
过滤此方法的Advisor
@Override
public List<Object> getInterceptorsAndDynamicInterceptionAdvice(
Advised config, Method method, @Nullable Class<?> targetClass) {
// This is somewhat tricky... We have to process introductions first,
// but we need to preserve order in the ultimate list.
List<Object> interceptorList = new ArrayList<Object>(config.getAdvisors().length);
Class<?> actualClass = (targetClass != null ? targetClass : method.getDeclaringClass());
boolean hasIntroductions = hasMatchingIntroductions(config, actualClass);
AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance();
// 遍历匹配此Class的所有Advisor
for (Advisor advisor : config.getAdvisors()) {
// 默认情况下,我们的Advisor都是此接口类型
if (advisor instanceof PointcutAdvisor) {
// Add it conditionally.
PointcutAdvisor pointcutAdvisor = (PointcutAdvisor) advisor;
// 判断是否匹配此Class,isPreFiltered之前创建代理类时,设置为true。因为已经判断过Class是否匹配,不需要再判断。
// isPreFiltered会短路掉后面的判断条件
if (config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass)) {
MethodMatcher mm = pointcutAdvisor.getPointcut().getMethodMatcher();
// 判断方法是否匹配
if (MethodMatchers.matches(mm, method, actualClass, hasIntroductions)) {
// 获取MethodInterceptor链
// 这里会判断Advisor是否实现MethodInterceptor。
// before通知/AfterReturn通知/throw通知,在此处会通过适配器转换为MethodInterceptor。
MethodInterceptor[] interceptors = registry.getInterceptors(advisor);
if (mm.isRuntime()) {
// Creating a new object instance in the getInterceptors() method
// isn't a problem as we normally cache created chains.
for (MethodInterceptor interceptor : interceptors) {
interceptorList.add(new InterceptorAndDynamicMethodMatcher(interceptor, mm));
}
}
else {
interceptorList.addAll(Arrays.asList(interceptors));
}
}
}
}
else if (advisor instanceof IntroductionAdvisor) {
// 这里应该是处理@DeclareParents这种注解的。功能挺强大,但用处不多。
IntroductionAdvisor ia = (IntroductionAdvisor) advisor;
if (config.isPreFiltered() || ia.getClassFilter().matches(actualClass)) {
Interceptor[] interceptors = registry.getInterceptors(advisor);
interceptorList.addAll(Arrays.asList(interceptors));
}
}
else {
Interceptor[] interceptors = registry.getInterceptors(advisor);
interceptorList.addAll(Arrays.asList(interceptors));
}
}
return interceptorList;
}
以上代码,我们可以看到中间有一处逻辑是从合适的Advisor中获取MethodInterceptor数组。也就是 registry.getInterceptors(advisor); 方法。
这个MethodInterceptor又是什么呢?
可以看到,它也继承了Advice接口,同时它内部仅有一个invoke方法。那是不是可以认为实现了此接口的类本身也就是一个Advice了。而MethodInterceptor接口的作用是什么?
其实此接口的方法就是后期遍历执行每个Advice的的通知逻辑的行为。从字面意思理解,每一个Advice就是方法拦截器。这里暂且有个印象,后续执行Advice的时候再说。
看一下,它是如何从Advisor中获取MethodInterceptor的?
适配器转换前置通知/最终通知/异常通知
@Override
public MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdviceTypeException {
List<MethodInterceptor> interceptors = new ArrayList<>(3);
// 获取Advisor中的Advice
Advice advice = advisor.getAdvice();
// 如果此Advice已经是MethodInterceptor,则直接添加到数组中。
if (advice instanceof MethodInterceptor) {
interceptors.add((MethodInterceptor) advice);
}
// 前置通知/最终通知/异常通知,则需要通过适配器模式转换成MethodInterceptor类型。
for (AdvisorAdapter adapter : this.adapters) {
if (adapter.supportsAdvice(advice)) {
interceptors.add(adapter.getInterceptor(advisor));
}
}
if (interceptors.isEmpty()) {
throw new UnknownAdviceTypeException(advisor.getAdvice());
}
return interceptors.toArray(new MethodInterceptor[0]);
}
adapters适配器的种类如下。
private final List<AdvisorAdapter> adapters = new ArrayList<>(3);
/**
* Create a new DefaultAdvisorAdapterRegistry, registering well-known adapters.
*/
public DefaultAdvisorAdapterRegistry() {
registerAdvisorAdapter(new MethodBeforeAdviceAdapter());
registerAdvisorAdapter(new AfterReturningAdviceAdapter());
registerAdvisorAdapter(new ThrowsAdviceAdapter());
}
以上可以看到,前置通知和最终通知以及异常通知都会被适配成MethodInterceptor类型。因为这三种Advice本身都没有实现MethodInterceptor接口。看一下前置通知的继承关系。
AspectJMethodBeforeAdvice类继承关系
AbstractAspectJAdvice类是通用的通知处理逻辑抽象类,其中包含切面方法的反射调用逻辑。
MethodBeforeAdvice则是前置通知特有的处理接口,其中包含前置通知before方法。
AspectJMethodBeforeAdvice则是MethodBeforeAdvice的具体实现,内部会调用通用通知抽象类方法。
可以看到它并没有实现MethodInterceptor接口。我们再看一下实现了此接口的后置通知类的继承关系。
AspectJAfterAdvice类继承关系
对比前置通知继承关系可以看到,AspectJAfterAdvice类同样继承了通用的通知处理逻辑抽象类AbstractAspectJAdvice。
同时它相对于前置通知,多实现了MethodInterceptor接口,重写了其中的invoke方法。
后期需要对所有的Advice执行同样的行为----方法拦截,invoke。那前置通知等没有此行为,就需要选择对应的适配器去适配此行为。
执行方法拦截器
找到并适配好所有合适的Advice(MethodInterceptor)后,接下来就要准备执行这些方法拦截器了。
首先它会创建一个MethodInvocation对象,从上图可以看到,此对象内部包含了以下信息。
- 代理类对象
- 目标类对象
- 目标方法
- 方法参数
- 目标类Class
- 各种适用于此方法的Advice(MethodInterceptor)
这些信息基本就可以执行Advice逻辑了,具体的执行逻辑,我们再往下看proceed方法内是如何处理的。
@Override
@Nullable
public Object proceed() throws Throwable {
// We start with an index of -1 and increment early.
// 当执行完所有的方法拦截器后,就准备执行目标对象方法
if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
// 反射调用目标对象方法
return invokeJoinpoint();
}
// 获取当前索引的MethodInterceptor对象,同时索引自增。
Object interceptorOrInterceptionAdvice =
this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
// 是否可以动态的再次过滤此方法,默认不会执行此逻辑。
if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
// Evaluate dynamic method matcher here: static part will already have
// been evaluated and found to match.
InterceptorAndDynamicMethodMatcher dm =
(InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
if (dm.methodMatcher.matches(this.method, this.targetClass, this.arguments)) {
return dm.interceptor.invoke(this);
}
else {
// Dynamic matching failed.
// Skip this interceptor and invoke the next in the chain.
return proceed();
}
}
else {
// It's an interceptor, so we just invoke it: The pointcut will have
// been evaluated statically before this object was constructed.
// 默认的方法拦截器的执行逻辑,调用器invoke方法,参数是当前对象。
return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
}
}
上面逻辑可以知道,它会依次遍历所有的MethodInterceptor,然后执行其invoke方法,对所有不同类型的Advice进行统一的调用,所以才有了前面适配器转换前置通知等Advice的过程。(重点:执行到这里,所有的Advice本质都是MethodInterceptor。)
环绕通知@Around的执行
这里先不要去考虑各种类型的通知的执行顺序,我们先看一眼我们定义的切面中环绕通知的执行逻辑。
@Around("pointCut()")
public Object adviceAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("advice ---->>>> Around Start");
// 执行目标方法
Object result = proceedingJoinPoint.proceed();
log.info("advice ---->>>> Around End");
return result;
}
依照此逻辑,分析Spring是如何实现的。
首先就会进入MethodInterceptor方法拦截器的invoke方法,核心在于invokeAdviceMethod方法,此方法是父类AbstractAspectJAdvice中的方法,所有的通知对象都会继承此方法。
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
if (!(mi instanceof ProxyMethodInvocation)) {
throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi);
}
ProxyMethodInvocation pmi = (ProxyMethodInvocation) mi;
// 创建连接点对象,实际类型是MethodInvocationProceedingJoinPoint。
// 内部维护MethodInvocation对象。
ProceedingJoinPoint pjp = lazyGetProceedingJoinPoint(pmi);
JoinPointMatch jpm = getJoinPointMatch(pmi);
// 准备反射调用环绕通知方法---此方法也是所用通知通用方法。
return invokeAdviceMethod(pjp, jpm, null, null);
}
ProceedingJoinPoint对象的获取
环绕通知,我们大家肯定都知道,它的方法参数类型是ProceedingJoinPoint这个接口类型,一般的前置通知,方法参数类型是JoinPoint类型。它两个是什么关系呢?看一下它们的继承关系。
ProceedingJoinPoint接口的实现类MethodInvocationProceedingJoinPoint。
protected ProceedingJoinPoint lazyGetProceedingJoinPoint(ProxyMethodInvocation rmi) {
return new MethodInvocationProceedingJoinPoint(rmi);
}
JointPoint的作用是获取目标对象或方法参数信息等。而ProceedingJoinPoint作为它的实现接口,主要是增加了proceed这个方法。而此方法本身又是MethodInvocation类中的方法,用于获取并执行下一个通知逻辑。这里我们可以猜到,ProceedingJoinPoint的实现类MethodInvocationProceedingJoinPoint,它本质就是包装了MethodInvocation的功能,因为之前我们说过,目标方法和目标对象以及所有的Advisor等信息都存储在MethodInvocation对象中。
获取Advice方法参数
接下来就要分析invokeAdviceMethod这个方法,此方法也是所有种类Advice都调用的方法。这里分析Spring AOP是如何知道我们的Advice参数对象的。
// As above, but in this case we are given the join point.
protected Object invokeAdviceMethod(JoinPoint jp, @Nullable JoinPointMatch jpMatch,
@Nullable Object returnValue, @Nullable Throwable t) throws Throwable {
// argBinding方法获取此通知方法的参数
return invokeAdviceMethodWithGivenArgs(argBinding(jp, jpMatch, returnValue, t));
}
分析argBinding方法。
/**
* Take the arguments at the method execution join point and output a set of arguments
* to the advice method
* @param jp the current JoinPoint
* @param jpMatch the join point match that matched this execution join point
* @param returnValue the return value from the method execution (may be null)
* @param ex the exception thrown by the method execution (may be null)
* @return the empty array if there are no arguments
*/
protected Object[] argBinding(JoinPoint jp, @Nullable JoinPointMatch jpMatch,
@Nullable Object returnValue, @Nullable Throwable ex) {
// 解析参数,默认这里不需要再解析,前面创建Advisor的时候已经解析过一次了。
calculateArgumentBindings();
// AMC start
// 通知方法的参数
Object[] adviceInvocationArgs = new Object[this.parameterTypes.length];
int numBound = 0;
// 由于此方法是执行切面通知的通用方法,这里joinPointArgumentIndex如果为0,
// 说明此方法参数为JoinPoint对象。具体类型是根据我们的通知方法的参数类型解析出来的。
if (this.joinPointArgumentIndex != -1) {
adviceInvocationArgs[this.joinPointArgumentIndex] = jp;
numBound++;
}
else if (this.joinPointStaticPartArgumentIndex != -1) {
// 如果参数类型是JoinPoint.StaticPart,则给定此对象。一般我们不会给定此类型。
adviceInvocationArgs[this.joinPointStaticPartArgumentIndex] = jp.getStaticPart();
numBound++;
}
// argumentBindings一般是后置通知或最终通知以及异常通知。我们方法上给定参数对象的变量名。
// 满足上述条件才有会进入此if逻辑。
if (!CollectionUtils.isEmpty(this.argumentBindings)) {
// binding from pointcut match
// JoinPointMatch对象是前面从ProxyMethodInvocation中获取,一般我们不会设置,默认返回null。
if (jpMatch != null) {
PointcutParameter[] parameterBindings = jpMatch.getParameterBindings();
for (PointcutParameter parameter : parameterBindings) {
String name = parameter.getName();
Integer index = this.argumentBindings.get(name);
adviceInvocationArgs[index] = parameter.getBinding();
numBound++;
}
}
// binding from returning clause
// 如果是目标方法执行后的通知,则可以获取它的返回值。
// 如果我们的通知方法参数给定了此返回值参数,这里就会进行返回值赋值。
if (this.returningName != null) {
Integer index = this.argumentBindings.get(this.returningName);
adviceInvocationArgs[index] = returnValue;
numBound++;
}
// binding from thrown exception
// 这里就是异常通知时,如果方法参数上指定,则进行异常对象赋值。
if (this.throwingName != null) {
Integer index = this.argumentBindings.get(this.throwingName);
adviceInvocationArgs[index] = ex;
numBound++;
}
}
if (numBound != this.parameterTypes.length) {
throw new IllegalStateException("Required to bind " + this.parameterTypes.length +
" arguments, but only bound " + numBound + " (JoinPointMatch " +
(jpMatch == null ? "was NOT" : "WAS") + " bound in invocation)");
}
// 返回最终的参数对象数组。
return adviceInvocationArgs;
}
此方法逻辑并不复杂,大致分析一下。
- 当执行环绕通知时,我们的通知方法参数只有ProceedingJoinPoint对象,这里就会通过joinPointArgumentIndex参数判断赋值给定的对象。
- 如果是前置通知,这里也会赋值给定对象ProceedingJoinPoint,但其实我们方法参数内获取到的仅仅是它的父类JoinPoint,如果你用ProceedingJoinPoint做参数,Spring会启动报错。目的就是防止我们使用ProceedingJoinPoint的proceed方法,破坏了Spring AOP的执行。这里有个有意思的事,如果我们骗过Spring AOP通知的方法参数检测,强行这么做会有什么后果?
@Before("pointCut()")
public void adviceBefore(JoinPoint joinPoint) throws Throwable {
// 不按套路出牌,强行改变连接点对象类型,然后执行proceed方法。
ProceedingJoinPoint proceedingJoinPoint = (ProceedingJoinPoint)joinPoint;
proceedingJoinPoint.proceed();
log.info("advice ---->>>> Before");
}
2021-01-28 23:08:02 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory Finished creating instance of bean 'userAspect'
2021-01-28 23:08:02 [main] INFO c.b.a.c.UserAspect advice ---->>>> Around Start
Exception in thread "main" java.lang.StackOverflowError
2021-01-28 23:08:02 [main] INFO c.b.a.c.UserAspect advice ---->>>> After
2021-01-28 23:08:02 [main] INFO c.b.a.c.UserAspect advice ---->>>> AfterThrowing
at org.springframework.aop.aspectj.AbstractAspectJAdvice.currentJoinPoint(AbstractAspectJAdvice.java:80)
at org.springframework.aop.aspectj.AbstractAspectJAdvice.getJoinPoint(AbstractAspectJAdvice.java:672)
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:637)
at org.springframework.aop.aspectj.AspectJMethodBeforeAdvice.before(AspectJMethodBeforeAdvice.java:44)
at org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor.invoke(MethodBeforeAdviceInterceptor.java:55)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:191)
at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:88)
结果就是它会死循环执行所有通知逻辑,导致方法栈内存溢出。当然这个研究没什么意义,纯属看了源码才发现的坑点。
- 如果是后置通知,它会和前置通知一样,给定JoinPoint类型对象。
- 如果是最终通知,它会根据注解指定的返回参数变量名,给定对应的目标方法返回值。
- 如果是异常通知,则会根据注解指定的异常变量名称,给定对应的异常对象。
反射执行通知方法
好了,到这里基本万事具备,只欠东风了。哪个东风,想想看,方法对象Method有了,参数也已经有了,还差什么?东风就是方法反射执行所需的对象,此对象就是切面对象。
protected Object invokeAdviceMethodWithGivenArgs(Object[] args) throws Throwable {
Object[] actualArgs = args;
// 如果我们没有给定通知方法参数,则默认null
if (this.aspectJAdviceMethod.getParameterCount() == 0) {
actualArgs = null;
}
try {
// 强制设置反射方法的可见性
ReflectionUtils.makeAccessible(this.aspectJAdviceMethod);
// TODO AopUtils.invokeJoinpointUsingReflection
// 获取切面对象,反射执行通知方法。
return this.aspectJAdviceMethod.invoke(this.aspectInstanceFactory.getAspectInstance(), actualArgs);
}
catch (IllegalArgumentException ex) {
throw new AopInvocationException("Mismatch on arguments to advice method [" +
this.aspectJAdviceMethod + "]; pointcut expression [" +
this.pointcut.getPointcutExpression() + "]", ex);
}
catch (InvocationTargetException ex) {
throw ex.getTargetException();
}
}
方法内部逻辑很简单,就是反射过程。我们先看看它是如何获取切面对象的?直接debug进去就知道了,看一看结果。
直接通过BeanFactory根据name获取bean,就是这么直接。
有了对象,反射调用环绕通知,结果如下。
到这里,基本就结束了环绕通知执行的逻辑分析。逻辑不复杂,心态是关键。
各种类型的Advice串联执行分析
在同一切面内,通知的执行顺序是Around—Before—target method—After—AfterReturning Or AfterThrowing。
那么Spring是如何实现这一调用顺序的?
Spring的设计很巧妙,这里我们先看一下前置通知的调用逻辑。
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
// 先执行此前置通知的方法
this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());
// 调用下一个通知逻辑
return mi.proceed();
}
方法内第一行是执行前置通知方法,点进去看一看它的逻辑。
@Override
public void before(Method method, Object[] args, @Nullable Object target) throws Throwable {
// 执行通用父类的通知执行逻辑
invokeAdviceMethod(getJoinPointMatch(), null, null);
}
上面的invokeAdviceMethod方法是不是很熟悉了,这不就是之前我们分析环绕通知时所说的,所有种类的通知都会调用的通用处理方法么!
再看一看后置通知的逻辑。
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
try {
return mi.proceed();
}
finally {
invokeAdviceMethod(getJoinPointMatch(), null, null);
}
}
重点在于mi.proceed()方法的作用和意义。此方法,你可以理解为执行下一个通知逻辑,所有的通知逻辑执行后,再执行目标对象方法。
此方法的源码逻辑,可以参考这篇博客讲 执行方法拦截器 部分内容。
基于此源码逻辑,那么我们是不是可以这么理解,proceed方法的调用位置节点决定了通知的执行时机。
假如我们此时只有前置通知和后置通知这两个通知方法,分析一下此逻辑实现过程。
- 首先会先执行前置通知的invoke方法,此方法内部会先执行我们自定义的通知方法。
- 执行后,调用proceed方法,获取下一个通知,也就是后置通知。
- 执行后置通知的invoke方法,此方法内部会先执行MethodInvocation的proceed方法。
- 再一次进入proceed方法,这时已经没有未执行的通知了,就会执行目标对象的方法。
- 执行目标方法后,回到后置通知invoke中,执行finally中我们自定义的的后置通知方法。
- 执行完毕,按照方法调用栈依次返回结果。
以上逻辑,我们会发现两件事:
1.我们自定义的通知方法的执行时机完全取决于它对应的方法拦截器内部是否在proceed方法调用前还是调用后。
2.这个执行时机和通知的执行顺序无关,即使你先执行后置通知(当然,实际上这两个确实是先执行后置通知的,内部会有排序机制),它也会在invoke内部先执行proceed,再调用前置通知。
这里应该就对各种类型的通知是如何串联保证顺序有了大概的认知了。
我们最后再看一看,最终通知和异常通知的方法拦截器逻辑。
AfterReturningAdviceInterceptor
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
Object retVal = mi.proceed();
this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis());
return retVal;
}
AspectJAfterThrowingAdvice
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
try {
return mi.proceed();
}
catch (Throwable ex) {
if (shouldInvokeOnThrowing(ex)) {
invokeAdviceMethod(getJoinPointMatch(), null, ex);
}
throw ex;
}
}
总结
本篇博客主要介绍了同一个切面内,不同种类的Advice是如何执行的,它们是如何保证执行的先后执行时机的。当然不同切面的Advice的执行时机,底层都是按照此逻辑执行,唯一区别的是两个切面,相同种类的Advice,先后顺序就取决于Advice的执行顺序了,这个执行顺序又是经过解析阶段排序机制确定的,这个下篇博客会介绍。
推荐一篇关于Spring AOP不错的博客。