在绝望之谷研究Spring AOP顺序

上一篇文章是一年前。。武汉疫情,人生第一次在家“正式上班”。。。悠悠假期不写博客?
这是一篇介绍方法的文章,为的是鼓励经常不自信的自己。我发现自己好像只要一段时间不解决问题,就似乎失去了能证明自己能力的依据,再加上健忘,解决过的难题过一段时间不接触就忘的差不多了,因此心里总是不踏实。
这不是介绍Spring Aop的文章,否则你会嫌弃我口水太多没有主旨。经过2天的学习研究,基本弄明白之后,此刻信心满满的我需要记录下我解决问题的过程,为将来不自信的自己加油鼓劲~

  1. 2018年下旬定下的3年目标中的一个是学框架,其实当时没想好怎么学,当时只是作为一个新手一下扎进java分布式系统开发的浪潮,所接所触皆是框架,一下子被框架这个词所倾倒。现在看来,Spring应该是学习java的第一个框架,因为它已经是J2EE的事实标准。学了一段时间,写点东西作为阶段性总结和成果。
  2. 苏总说过,如果把一个人知道的东西比作是一个圆内的面积的话,随着这个圆越来越大,圆的周长也会越来越长,因此他知道圆外面自己不懂的东西也越来越多,因此也会越来越谦虚。后来看到邓宁-克鲁格效应,我知道自己在2018年已经翻过了愚昧山峰,因此才会定下3年计划。此刻,我正站在绝望之谷抬头仰望。在解决问题前我居然都感觉到了绝望,觉得坑太大,技术欠账太多,学无止境,不知道怎么才能解决这个问题。解决完后我又重拾自信,但是,问题是无穷无尽的,我不能一直这样过山车的心理状态。因此,我把这次解决问题的过程写下来,不管以后再遇到什么问题,我相信按着这个套路一定能解决的,不必慌张,不必害怕,我要为我爬上开悟之坡加油鼓劲。不打怪怎么升级呢?遇到的怪越多越好~~
  3. Spring AOP顺序是一个典型的spring的问题,可深可浅。定义了多个Spring的切面后,怎么控制顺序?Spring里面,别人写的定义切面的方法那么多,为什么自己写的时候有些可以有些不行?那么多属性,又各有什么含义?作为一个新手,怎么尽快解决这个问题?我只在这里回答最后一个问题。

授人以渔

  1. 看书
    看书的好处是系统的教你,坏处是厚厚的一本,只看书不一定能快速解决当前的问题。
    顺便说一下,我看的是《Spring in action》,一开始觉得不习惯,感觉里面说的东西很多都一带而过,但是又很喜欢,因为里面把一些基础的思想、原理知识点都提到了。后来我明白了,因为Spring还有厚厚的用户指南,Spring是一个开源的不断变化的项目,这本书无法也没必要去覆盖这些细节。因此你还应该学会如下。

  2. google
    官网资料,各种资料。这是看书的补充,好处是更详尽,坏处也是更慢,不可能通读。Sping就有很详细的用户手册,详细到一度让我望而生畏,望而却步。但是,接触多了之后,你就知道,这是理所当然的学习方式。
    别人的经验。好处是最快捷最直接,如果找到了那就解决问题了。坏处是得碰运气,各种试,而且如果别人没把原理说清楚,你也只是暂时解决了问题,没有根本解决问题。

  3. 过来
    终于到了靠自己的时候了,能解决到什么程度,利用多少工具,就完全看个人修行啦。
    (1)首先要有源码,这样才有分析的可能,不然就还是只能各种碰运气试,看日志猜。这也证明了开源的重要性,当然也可以反编译,只是有源码会更好一些。
    (2)通过调试,跟踪的模式,把问题相对的聚焦到相关的源码上面。另外,log4j支持打印日志的同时,打印“输出这段日志的代码对应的源码”的具体位置,即程序名及对应代码所在的行数。这样就可以在相关的地方打断点,然后通过堆栈往上查找,看看相关的实现逻辑。另外,log4j或slf4j还支持桥接别的日志输出框架到自身,然后slf4j可以代理到别的日志输出框架(比如log4j),最后统一由logj4j控制debug级别输出。
    (3)看代码的时候要做笔记,看源码时最好的辅助工具应该是画uml了。
    (4)重现问题,或者做实验的最好方式,是写一个简化版的demo去试验,这样才能聚焦问题,避免被其它因素影响,才能最终从本质上理解。

对待框架的正确态度
对于框架,即使是Spring这么优秀文档这么齐全,更别说平常工作中别人以方便使用的名义写的各种封装代码,只能说真是又爱又恨。恨的是因为框架做了各种封装,一定程度上阻碍了你认识事物的本质,让你从学习事物本质变为学习别人框架的使用。爱的是,其实你不能不爱,没有框架,你一个人是做不过来的。解决这之间的矛盾,方法有一个,看源码,看别人写的代码。代码可以不是你写的,但只要有可能,就去看源码,这样遇到问题的时候你才可以靠自己。

实操

解决思路

看书和google没什么好说的,做就是了。
分享一个找到的相关网页:薛定饿,里面的评论激励了我要靠自己。
显示一段资料,虽然跟AOP顺序无关,但是跟AOP密切相关,经常在<aop:aspectj-autoproxy>元素中可以看到这两个属性。另外,这里也说明了通过AopContext.currentProxy()可以在“代理对象”内获取“自己”,而不是通过this获取“被代理的对象”。因为这样才可以重新进入“代理对象”,触发“代理对象”上你写的(如果有的话)其它通知(拦截方法)。

如下是靠自己的过程:

  1. 首先是简化版的demo,就是建一个项目,写一段Spring启动加载xml的代码,然后定义多个切面,以便验证各种方法下切面的顺序是否有效。
  2. 然后是把jar包都关联源代码。
  3. 之后是修改log4j的日志级别和打印输出日志所在的源码位置,以便找到需要打断点的地方。

分析结果

先给结论吧,如下是我调试的堆栈和找到的关键代码行,需要的话可以直接参考去打断点。
经过分析,大致可以看出Spring Aop控制顺序的处理逻辑是:获取Ioc容器里面的一个bean的时候,会同时搜出对应这个bean的所有代理,然后对代理做一个排序,排序是用AspectJ自己的方法进行的,大致是根据类上面的Order注解或者类实现Order接口后返回的数字作一个排序,另外也看同一个Order大小下,声明的前后顺序排序。
具体还是看代码比较好,不同版本不确定是否有差异,正如哪位大神说的“废话少说,放码过来”。

调试过程

调试的过程,就是一个逆向工程思维的过程,要不断看代码理解代码的含义,从中找到线索。

1. 以函数调用为起点

先在切面的方法上打断点,循着调用堆栈往上看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Thread [main] (Suspended (breakpoint at line 13 in Aspect4))	
Aspect4.around(ProceedingJoinPoint) line: 13
NativeMethodAccessorImpl.invoke0(Method, Object, Object[]) line: not available [native method]
NativeMethodAccessorImpl.invoke(Object, Object[]) line: not available
DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: not available
Method.invoke(Object, Object...) line: not available
AspectJAroundAdvice(AbstractAspectJAdvice).invokeAdviceMethodWithGivenArgs(Object[]) line: 629
AspectJAroundAdvice(AbstractAspectJAdvice).invokeAdviceMethod(JoinPoint, JoinPointMatch, Object, Throwable) line: 618
AspectJAroundAdvice.invoke(MethodInvocation) line: 70
CglibAopProxy$CglibMethodInvocation(ReflectiveMethodInvocation).proceed() line: 179
ExposeInvocationInterceptor.invoke(MethodInvocation) line: 92
CglibAopProxy$CglibMethodInvocation(ReflectiveMethodInvocation).proceed() line: 179
CglibAopProxy$DynamicAdvisedInterceptor.intercept(Object, Method, Object[], MethodProxy) line: 673
ExampleRun$$EnhancerBySpringCGLIB$$88fa362e.run() line: not available
TestAop.main(String[]) line: 13

2. 找到逐个切面调用的地方

从上往下,在第一个CglibAopProxy$CglibMethodInvocation(ReflectiveMethodInvocation).proceed() line: 179这里看源码,可以发现切面的调用不是一个套一个的递归调用,而是在一个列表里面循环调用:this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);

1
2
3
4
5
6
7
8
9
10
public class ReflectiveMethodInvocation implements ProxyMethodInvocation, Cloneable {
@Override
public Object proceed() throws Throwable {
// We start with an index of -1 and increment early.
if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
return invokeJoinpoint();
}

Object interceptorOrInterceptionAdvice =
this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);

3. 找到当前适用的aop切面列表

既然切面列表是在interceptorsAndDynamicMethodMatchers变量内,那么就要找到是在哪里创建它,并且做了排序。
继续往上看调用链,可以发现是在CglibAopProxy$DynamicAdvisedInterceptor.intercept(Object, Method, Object[], MethodProxy) line: 673这里的chain传递的列表。

1
2
3
4
5
6
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
...
...
else {
// We need to create a method invocation...
retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();

根据chain = this.advised这一句,接下来要找的就是advised变量。
advised变量内容如下,可以看到一共5个切面,其中4个是我自定义的。而且,这里的切面已经是排序后的了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
org.springframework.aop.framework.ProxyFactory:
0 interfaces []; 5 advisors [
org.springframework.aop.interceptor.ExposeInvocationInterceptor.ADVISOR,

InstantiationModelAwarePointcutAdvisor: expression [execution(* testAop.ExampleRun.*(..))];
advice method [public java.lang.Object testAop.Aspect4.around(org.aspectj.lang.ProceedingJoinPoint) throws java.lang.Throwable];
perClauseKind=SINGLETON,

InstantiationModelAwarePointcutAdvisor: expression [execution(* testAop.ExampleRun.*(..))];
advice method [public java.lang.Object testAop.Aspect3.around(org.aspectj.lang.ProceedingJoinPoint) throws java.lang.Throwable];
perClauseKind=SINGLETON,

InstantiationModelAwarePointcutAdvisor: expression [execution(* testAop.ExampleRun.*(..))];
advice method [public java.lang.Object testAop.Aspect1.around(org.aspectj.lang.ProceedingJoinPoint) throws java.lang.Throwable];
perClauseKind=SINGLETON,

InstantiationModelAwarePointcutAdvisor: expression [execution(* testAop.ExampleRun.*(..))];
advice method [public java.lang.Object testAop.Aspect2.around(org.aspectj.lang.ProceedingJoinPoint) throws java.lang.Throwable];
perClauseKind=SINGLETON
];
targetSource [SingletonTargetSource for target object [testAop.ExampleRun@e53b70]];
proxyTargetClass=true;
optimize=false;
opaque=false;
exposeProxy=true;
frozen=false

4. 找到切面列表的来源

为了看到在哪里排序,再继续看源码,打新的断点,可以找到下面这一句赋值:this.advised = config;,在第127行。

1
2
3
4
5
6
7
8
9
10
class CglibAopProxy implements AopProxy, Serializable {

public CglibAopProxy(AdvisedSupport config) throws AopConfigException {
Assert.notNull(config, "AdvisedSupport must not be null");
if (config.getAdvisors().length == 0 && config.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE) {
throw new AopConfigException("No advisors and no TargetSource specified");
}
this.advised = config;
this.advisedDispatcher = new AdvisedDispatcher(this.advised);
}

接着再打新的断点,寻找config的来源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Thread [main] (Suspended (breakpoint at line 127 in CglibAopProxy))	
owns: ProxyFactory (id=43)
owns: ConcurrentHashMap<K,V> (id=44)
owns: Object (id=23)
ObjenesisCglibAopProxy(CglibAopProxy).<init>(AdvisedSupport) line: 127
ObjenesisCglibAopProxy.<init>(AdvisedSupport) line: 48
DefaultAopProxyFactory.createAopProxy(AdvisedSupport) line: 60
ProxyFactory(ProxyCreatorSupport).createAopProxy() line: 105
ProxyFactory.getProxy(ClassLoader) line: 109
AnnotationAwareAspectJAutoProxyCreator(AbstractAutoProxyCreator).createProxy(Class<?>, String, Object[], TargetSource) line: 469
AnnotationAwareAspectJAutoProxyCreator(AbstractAutoProxyCreator).wrapIfNecessary(Object, String, Object) line: 349
AnnotationAwareAspectJAutoProxyCreator(AbstractAutoProxyCreator).postProcessAfterInitialization(Object, String) line: 298
DefaultListableBeanFactory(AbstractAutowireCapableBeanFactory).applyBeanPostProcessorsAfterInitialization(Object, String) line: 423
DefaultListableBeanFactory(AbstractAutowireCapableBeanFactory).initializeBean(String, Object, RootBeanDefinition) line: 1633
DefaultListableBeanFactory(AbstractAutowireCapableBeanFactory).doCreateBean(String, RootBeanDefinition, Object[]) line: 555
DefaultListableBeanFactory(AbstractAutowireCapableBeanFactory).createBean(String, RootBeanDefinition, Object[]) line: 483
AbstractBeanFactory$1.getObject() line: 306
DefaultListableBeanFactory(DefaultSingletonBeanRegistry).getSingleton(String, ObjectFactory<?>) line: 230
DefaultListableBeanFactory(AbstractBeanFactory).doGetBean(String, Class<T>, Object[], boolean) line: 302
DefaultListableBeanFactory(AbstractBeanFactory).getBean(String) line: 197
DefaultListableBeanFactory.preInstantiateSingletons() line: 761
ClassPathXmlApplicationContext(AbstractApplicationContext).finishBeanFactoryInitialization(ConfigurableListableBeanFactory) line: 867
ClassPathXmlApplicationContext(AbstractApplicationContext).refresh() line: 543
ClassPathXmlApplicationContext.<init>(String[], boolean, ApplicationContext) line: 139
ClassPathXmlApplicationContext.<init>(String) line: 83
TestAop.main(String[]) line: 10

ProxyFactory(ProxyCreatorSupport).createAopProxy() line: 105找到创建aop切面代理的语句createAopProxy(this)

1
2
3
4
5
6
7
public class ProxyCreatorSupport extends AdvisedSupport {
protected final synchronized AopProxy createAopProxy() {
if (!this.active) {
activate();
}
return getAopProxyFactory().createAopProxy(this);
}

继续沿着调用堆栈往上,查找this的来源为AnnotationAwareAspectJAutoProxyCreator(AbstractAutoProxyCreator).createProxy(Class<?>, String, Object[], TargetSource) line: 469proxyFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {

Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
for (Advisor advisor : advisors) {
proxyFactory.addAdvisor(advisor);
}

proxyFactory.setTargetSource(targetSource);
customizeProxyFactory(proxyFactory);

proxyFactory.setFrozen(this.freezeProxy);
if (advisorsPreFiltered()) {
proxyFactory.setPreFiltered(true);
}

return proxyFactory.getProxy(getProxyClassLoader());

根据proxyFactory.addAdvisor(advisor);可知,继续往上找specificInterceptors,在AnnotationAwareAspectJAutoProxyCreator(AbstractAutoProxyCreator).wrapIfNecessary(Object, String, Object) line: 349找到getAdvicesAndAdvisorsForBean方法

1
2
3
4
5
6
7
8
9
public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {

// Create proxy if we have advice.
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));

补充
可以在切面的定义里面,写一个构造函数,在里面打断点
也可以用log4j日志的方法,找到Creating instance of bean '切面的名字'这一句
断点位置对应上述堆栈的DefaultListableBeanFactory(AbstractAutowireCapableBeanFactory).createBean(String, RootBeanDefinition, Object[]) line: 483中的后两行:485行。

5. 找到对切面排序的地方

由于上述是抽象类,在getAdvicesAndAdvisorsForBean方法上打断点,找到findEligibleAdvisors方法,进而找到sortAdvisors方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public abstract class AbstractAdvisorAutoProxyCreator extends AbstractAutoProxyCreator {
@Override
protected Object[] getAdvicesAndAdvisorsForBean(Class<?> beanClass, String beanName, TargetSource targetSource) {
List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
if (advisors.isEmpty()) {
return DO_NOT_PROXY;
}
return advisors.toArray();
}

protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
List<Advisor> candidateAdvisors = findCandidateAdvisors();
List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
extendAdvisors(eligibleAdvisors);
if (!eligibleAdvisors.isEmpty()) {
eligibleAdvisors = sortAdvisors(eligibleAdvisors);
}
return eligibleAdvisors;
}

进入实现类中,查看sortAdvisors方法,最终找到AOP的排序实现PartialOrder.sort

1
2
3
4
5
6
7
8
9
10
11
public class AspectJAwareAdvisorAutoProxyCreator extends AbstractAdvisorAutoProxyCreator {
protected List<Advisor> sortAdvisors(List<Advisor> advisors) {
List<PartiallyComparableAdvisorHolder> partiallyComparableAdvisors =
new ArrayList<PartiallyComparableAdvisorHolder>(advisors.size());
for (Advisor element : advisors) {
partiallyComparableAdvisors.add(
new PartiallyComparableAdvisorHolder(element, DEFAULT_PRECEDENCE_COMPARATOR));
}
List<PartiallyComparableAdvisorHolder> sorted =
PartialOrder.sort(partiallyComparableAdvisors);
if (sorted != null) {

6. 找到排序的依据

查看上一步最后一行的sorted变量如下。

1
2
3
4
5
[ExposeInvocationInterceptor: order -2147483647, , 
AspectJAroundAdvice: order -1000, aspect4, declaration order 0,
AspectJAroundAdvice: order 1, aspect2, declaration order 0,
AspectJAroundAdvice: order 2, aspect3, declaration order 0,
AspectJAroundAdvice: order 3, aspect1, declaration order 0]

根据实现的代码,则可以知道SpringAop的排序实现首先是根据Ordered接口进行比较的,然后由于代码实现的逻辑,有可能导致其它排序顺序,但这就跟具体实现相关,一般不推荐据此进行排序。

后话

问题解决之前往往是一片茫然,问题解决后回顾又似乎没有难度。其实不然,
如果你不会翻墙,只会百度。
如果你不会使用log4j输出Srping的所有日志,并且显示行数,协助你找到需要打断点的地方。
如果你没养成调试看源码的习惯,不擅于逆向工程

本来看spring源码没什么好说的,但是因为spring用的是gradle(一个类似maven的组件),一个不熟悉的gradle也折腾了我两天,顺带记录下,提醒自己开始看spring源码之旅。

spring源码

本来官网上就有说明,但是我嫌步骤太多,我只是想在eclipse上能打开项目看看源码。
所以又参考了如下这两篇博客:吴楠予-博客园tianjinsong-CSDN博客
最后实践发现,其实很简单。。。

下载spring

首先下载自己需要的spring版本,比如我需要的spring-framework-4.3.10.RELEASE.zip

下载Eclipse

无意中发现有带gradle插件的eclipse,应该是只有java版的eclipse才有gradle插件,j2ee版没有。上面官网上的指引提到的最新版eclipse是Eclipse 4.14,即Version: 2019-12 (4.14.0)。因此我下载的是:eclipse-java-2019-12-R-win32-x86_64.zip

下载gradle

首先说明一下,如果你像我那样,讨厌繁琐,讨厌新问题一个接一个,只想解决最原始的需求(在Eclipse导入Spring源码看看,暂时不需要运行),那么跳过这一步吧,我试过了,只是多了一些jar包找不到而已,自己手工加都行,反正我今天还不想学gradle。

按照上面博客和官网的说明,是需要手工运行gradle命令的。
因此,下载gradle的binary-only程序,下载后设置环境变量: GRADLE_HOME = gradle主目录 , 并在path中加入;%GRADLE_HOME%\bin;
注意,gradle的版本需要对应spring的版本,比如我用的是spring-framework-4.3.10,里面用的是gradle-2.14.1版本(运行命令的时候就会看到下载的提示了),因此我下载的就是gradle-2.14.1-bin.zip。这里并不是版本越新越好,会因为不兼容导致各种报错。
运行吴楠予博客说的命令gradlew :spring-oxm:compileTestJava,也可以运行官网说的命令:Build spring-oxm from the command line with ./gradlew :spring-oxm:check

Eclipse导入

  1. 方式一
    使用eclipse 将整个文件导入 File -> Import -> Existing Gradle Project -> 找到源码目录 点击finish 开始导入

  2. 方式二
    运行Spring目录中的脚本:import-into-eclipse.bat

离线阅读

最后,把eclipse和spring源码目录,以及C:\Users\xxx.gradle打包,就可以拷贝到非联网的环境看源码了。
再次导入工程的时候,如果是用方式一,则eclipse上的gradle应该设置为offline离线模式。如果是方式二,则注意修改.classpath文件里面的路径去适应新的环境。