目录

动态AOP使用示例

创建用于拦截的bean

创建Advisor

创建配置文件

测试

动态AOP自定义标签

注册AnnotationAwareAspectJAutoProxyCreator

注册或者升级AnnotationAwareAspectJAutoProxyCreator

处理proxy-target-class以及expose-proxy属性

创建AOP代理

获取增强器

普通增强器的获取

增加同步实例化增强器

获取DeclareParents注解

寻找匹配的增强器

创建代理

创建代理过程

获取代理过程

静态AOP使用示例

创建AOP静态代理

Instrumentation使用

写ClassFileTransformer类

编写agent类

打包agent

打包应用

自定义标签

织入


注意:本文摘自spring源码深度解析

我们知道,使用面向对象编程(OOP)有一些弊端,当需要为多个不具有继承关系的对象引入同一个公共行为时,例如日志、安全检测等,我们只有在每个对象里引用公共行为,这样程序中就产生了大量的重复代码,程序就不便于维护了,所以就有了一个对面向对象编程的补充,即面向方面编程(AOP),AOP所关注的方向是横向的,不同于OOP的纵向。

Spring中提供了AOP的实现,但是在低版本Spring中定义一个切面是比较麻烦的,需要实现特定的接口,并进行一些较为复杂的配置。低版本Spring AOP的配置是被批评最多的地方。Spring听取了这方面的批评声音,并下决心彻底改变这一现状。在Spring 2.0中,SpringAOP已经焕然一新,你可以使用@AspectJ注解非常容易地定义一个切面,不需要实现任何的接口。

Spring 2.0采用@AspectJ注解对POJO进行标注,从而定义一个包含切点信息和增强横切逻辑的切面。Spring 2.0可以将这个切面织入到匹配的目标Bean中。@AspectJ注解使用AspectJ切点表达式语法进行切点定义,可以通过切点函数、运算符、通配符等高级功能进行切点定义,拥有强大的连接点描述能力。我们先来直观地浏览一下Spring中的AOP实现。

动态AOP使用示例

创建用于拦截的bean

在实际工作中,此bean可能是满足业务需要的核心逻辑,例如test方法中可能会封装着某个核心业务,但是,如果我们想在test前后加入日志来跟踪调试,如果直接修改源码并不符合面向对象的设计方法,而且随意改动原有代码也会造成一定的风险,还好接下来的Spring帮我们做到了这一点。

spring actuator源码分析 spring aop源码分析_动态代理

spring actuator源码分析 spring aop源码分析_AOP_02

创建Advisor

Spring中摒弃了最原始的繁杂配置方式而采用@AspectJ注解对POJO进行标注,使AOP的工作大大简化,例如,在AspectJTest类中,我们要做的就是在所有类的test方法执行前在控制台中打印beforeTest,而在所有类的test方法执行后打印afterTest,同时又使用环绕的方式在所有类的方法执行前后再次分别打印beforel和afterl。

spring actuator源码分析 spring aop源码分析_封装_03

创建配置文件

XML是Spring的基础。尽管Spring一再简化配置,并且大有使用注解取代XML配置之势,但是无论如何,至少现在XML还是Spring的基础。要在Spring中开启AOP功能,还需要在配置文件中作如下声明:

spring actuator源码分析 spring aop源码分析_封装_04

测试

经过以上步骤后,便可以验证Spring的AOP为我们提供的神奇效果了。

spring actuator源码分析 spring aop源码分析_封装_05

Spring实现了对所有类的test方法进行增强,使辅助功能可以独立于核心业务之外,方便与程序的扩展和解耦。

那么,Spring究竟是如何实现AOP的呢?首先我们知道,Spring是否支持注解的AOP是由一个配置文件控制的,也就是<aop:aspectj-autoproxy/>,当在配置文件中声明了这句配置的时候,Spring就会支持注解的AOP,那么我们的分析就从这句注解开始。

动态AOP自定义标签

之前讲过Spring中的自定义注解,如果声明了自定义的注解,那么就一定会在程序中的某个地方注册了对应的解析器。我们搜索整个代码,尝试找到注册的地方,全局搜索后我们发现了在AopNamespaceHandler中对应着这样一段函数:

spring actuator源码分析 spring aop源码分析_spring actuator源码分析_06

此处不再对Spring中的自定义注解方式进行讨论。有兴趣的读者可以回顾之前的内容。我们可以得知,在解析配置文件的时候,一旦遇到aspectj-autoproxy注解时就会使用解析器AspectJAutoProxyBeanDefinitionParser进行解析,那么我们来看一看AspectJAutoProxyBean­DefinitionParser的内部实现。

注册AnnotationAwareAspectJAutoProxyCreator

所有解析器,因为是对BeanDefinitionParser接口的统一实现,入口都是从parse函数开始的,AspectJAutoProxyBeanDefinitionParser的parse函数如下:

spring actuator源码分析 spring aop源码分析_AOP_07

在registerAspectJAnnotationAutoProxyCreatorIfNecessary方法中主要完成了3件事情,基本上每行代码就是一个完整的逻辑。

注册或者升级AnnotationAwareAspectJAutoProxyCreator

对于AOP的实现,基本上都是靠AnnotationAwareAspectJAutoProxyCreator去完成,它可以根据@Point注解定义的切点来自动代理相匹配的bean。但是为了配置简便,Spring使用了自定义配置来帮助我们自动注册AnnotationAwareAspectJAutoProxyCreator,其注册过程就是在这里实现的。

spring actuator源码分析 spring aop源码分析_封装_08

以上代码中实现了自动注册AnnotationAwareAspectJAutoProxyCreator类的功能,同时这里还涉及了一个优先级的问题,如果已经存在了自动代理创建器,而且存在的自动代理创建器与现在的不一致,那么需要根据优先级来判断到底需要使用哪个。

处理proxy-target-class以及expose-proxy属性

useClassProxyinglfNecessary实现了proxy-target-class属性以及expose-proxy属性的处理。

spring actuator源码分析 spring aop源码分析_封装_09

proxy-target-class:Spring AOP部分使用JDK动态代理或者CGLIB来为目标对象创建代理(建议尽量使用JDK的动态代理)。如果被代理的目标对象实现了至少一个接口,则会使用JDK动态代理。所有该目标类型实现的接口都将被代理。若该目标对象没有实现任何接口,则创建一个CGLIB代理。如果你希望强制使用CGLIB代理(例如希望代理目标对象的所有方法,而不只是实现自接口的方法),那也可以。但是需要考虑以下两个问题。

无法通知(advise)Final方法,因为它们不能被覆写。

你需要将CGLIB二进制发行包放在classpath下面。

与之相比,JDK本身就提供了动态代理,强制使用CGLIB代理需要将<aop:config>的proxy-target-class属性设为true

<aop:config proxy-target-class="true"> ... <:/aop:config>

当需要使用CGLIB代理和@AspectJ自动代理支持,可以按照以下方式设置<aop:aspectj­autoproxy>的proxy-target-class属性:

<aop:aspectj-autoproxy proxy-target-class="true"/>

而实际使用的过程中才会发现细节问题的差别,The devil is in the details。

JDK动态代理:其代理对象必须是某个接口的实现,它是通过在运行期间创建一个接口的实现类未完成对目标对象的代理。

CGLIB代理:实现原理类似于JDK动态代理,只是它在运行期间生成的代理对象是针对目标类扩展的子类。CGLIB是高效的代码生成包,底层是依靠ASM(开源的Java字节码编辑类库)操作字节码实现的,性能比JDK强。

expose-proxy:有时候目标对象内部的自我调用将无法实施切面中的增强,如下示例:

spring actuator源码分析 spring aop源码分析_动态代理_10

此处的this指向目标对象,因此调用this.b()将不会执行b事务切面,即不会执行事务增强,因此b方法的事务定义"@Transactional(proragation = Proragation.REQUIRES_NEW)"将不会实施,为了解决这个问题,我们可以这样做:

<aop:aspectj-autoproxy expose-proxy="true"/>

然后将以上代码中的"this.b();"修改为"((AService)AopContext.currentProxy()).b();"即可。

通过以上的修改便可以完成对a和b方法的同时增强。

最后注册组件并通知,便于监听器做进一步处理,这里就不再一一赘述了。

创建AOP代理

上文中讲解了通过自定义配置完成了对AnnotationAwareAspectJAutoProxyCreator类型的自动注册,那么这个类到底做了什么工作来完成AOP的操作呢?首先我们看看Annotation­AwareAspectJAutoProxyCreator类的层次结构,如图所示。

spring actuator源码分析 spring aop源码分析_封装_11

在类的层级中,我们看到AnnotationAwareAspectJAutoProxyCreator实现了BeanPostProcessor接口,而实现BeanPostProcessor后,当Spring加载这个Bean时会在实例化前调用其postProcess­AfterInitialization方法,而我们对于AOP逻辑的分析也由此开始。

在父类AbstractAutoProxyCreator的postProcessAfterInitialization中代码如下:

spring actuator源码分析 spring aop源码分析_AOP_12

spring actuator源码分析 spring aop源码分析_spring actuator源码分析_13

函数中我们已经看到了代理创建的雏形。当然,真正开始之前还需要经过一些判断,比如是否已经处理过或者是否是需要跳过的bean,而真正创建代理的代码是从getAdvicesAnd­AdvisorsForBean开始的。

创建代理主要包含了两个步骤。

1 获取增强方法或者增强器。

2 根据获取的增强进行代理。核心逻辑的时序图如图所示

spring actuator源码分析 spring aop源码分析_封装_14

虽然看似简单,但是每个步骤中都经历了大量复杂的逻辑。首先来看看获取增强方法的实现逻辑。

spring actuator源码分析 spring aop源码分析_封装_15

对于指定bean的增强方法的获取一定是包含两个步骤的,获取所有的增强以及寻找所有增强中适用于bean的增强并应用,那么findCandidateAdvisors与findAdvisorsThatCanApply便是做了这两件事情。当然,如果无法找到对应的增强器便返回DO_NOT_PROXY,其中DO_NOT_PROXY=null。

获取增强器

由于我们分析的是使用注解进行的AOP,所以对于findCandidateAdvisors的实现其实是由AnnotationAwareAspectJAutoProxyCreator类完成的,我们继续跟踪AnnotationAwareAspectJAuto­ProxyCreator的findCandidateAdvisors方法。

spring actuator源码分析 spring aop源码分析_AOP_16

AnnotationAwareAspectJAutoProxyCreator间接继承了AbstractAdvisorAutoProxyCreator,在实现获取增强的方法中除了保留父类的获取配置文件中定义的增强外,同时添加了获取Bean的注解增强的功能,那么其实现正是由this.aspectJAdvisorsBuilder.buildAspectJAdvisors()来实现的。

在真正研究代码之前读者可以尝试着自己去想象一下解析思路,看看自己的实现与Spring是否有差别呢?或者我们一改以往的方式,先来了解函数提供的大概功能框架,读者可以在头脑中尝试实现这些功能点,看看是否有思路。

1 获取所有beanName,这一步骤中所有在beanFacotry中注册的bean都会被提取出来。

2 遍历所有beanName,并找出声明AspectJ注解的类,进行进一步的处理。

3 对标记为AspectJ注解的类进行增强器的提取。

4 将提取结果加入缓存。

现在我们来看看函数实现,对Spring中所有的类进行分析,提取Advisor。

spring actuator源码分析 spring aop源码分析_spring actuator源码分析_17

spring actuator源码分析 spring aop源码分析_AOP_18

至此,我们已经完成了Advisor的提取,在上面的步骤中最为重要也最为繁杂的就是增强器的获取。而这一功能委托给了getAdvisors方法去实现(this.advisorFactory.getAdvisors(factory))。

spring actuator源码分析 spring aop源码分析_动态代理_19

spring actuator源码分析 spring aop源码分析_动态代理_20

函数中首先完成了对增强器的获取,包括获取注解以及根据注解生成增强的步骤,然后考虑到在配置中可能会将增强配置成延迟初始化,那么需要在首位加入同步实例化增强器以保证增强使用之前的实例化,最后是对DeclareParents注解的获取,下面将详细介绍一下每个步骤。

普通增强器的获取

普通增强器的获取逻辑通过getAdvisor方法实现,实现步骤包括对切点的注解的获取以及根据注解信息生成增强。

spring actuator源码分析 spring aop源码分析_动态代理_21

1 切点信息的获取。所谓获取切点信息就是指定注解的表达式信息的获取,如@Before("test()")。

spring actuator源码分析 spring aop源码分析_封装_22

2 根据切点信息生成增强。所有的增强都由Advisor的实现类InstantiationModelAware­PointcutAdvisorlmpl统一封装的。

spring actuator源码分析 spring aop源码分析_动态代理_23

spring actuator源码分析 spring aop源码分析_AOP_24

在封装过程中只是简单地将信息封装在类的实例中,所有的信息单纯地赋值,在实例初始化的过程中还完成了对于增强器的初始化。因为不同的增强所体现的逻辑是不同的,比如@Before("test()")与@After("test()")标签的不同就是增强器增强的位置不同,所以就需要不同的增强器来完成不同的逻辑,而根据注解中的信息初始化对应的增强器就是在instantiateAdvice函数中实现的。

spring actuator源码分析 spring aop源码分析_spring actuator源码分析_25

spring actuator源码分析 spring aop源码分析_AOP_26

spring actuator源码分析 spring aop源码分析_封装_27

从函数中可以看到,Spring会根据不同的注解生成不同的增强器,例如AtBefore会对应AspectJMethod.BeforeAdvice,而在AspectJMethodBeforeAdvice中完成了增强方法的逻辑。我们尝试分析几个常用的增强器实现。

MethodBeforeAdviceInterceptor。

我们首先查看MethodBeforeAdviceInterceptor类的内部实现。

spring actuator源码分析 spring aop源码分析_动态代理_28

spring actuator源码分析 spring aop源码分析_spring actuator源码分析_29

invokeAdviceMethodWithGivenArgs方法中的aspectJAdviceMethod正是对于前置增强的方法,在这里实现了调用。

AspectJAfterAdvice。

后置增强与前置增强有稍许不一致的地方。回顾之前讲过的前置增强,大致的结构是在拦截器链中放置MethodBeforeAdviceInterceptor,而在MethodBeforeAdviceInterceptor中又放置了AspectJMethodBeforeAdvice,并在调用invoke时首先串联调用。但是在后置增强的时候却不一样,没有提供中间的类,而是直接在拦截器链中使用了中间的AspectJAfterAdvice。

spring actuator源码分析 spring aop源码分析_封装_30

spring actuator源码分析 spring aop源码分析_spring actuator源码分析_31

增加同步实例化增强器

如果寻找的增强器不为空而且又配置了增强延迟初始化,那么就需要在首位加入同步实例化增强器。同步实例化增强器SyntheticlnstantiationAdvisor如下:

spring actuator源码分析 spring aop源码分析_spring actuator源码分析_32

获取DeclareParents注解

DeclareParents主要用于引介增强的注解形式的实现,而其实现方式与普通增强很类似,只不过使用DeclareParentsAdvisor对功能进行封装。

spring actuator源码分析 spring aop源码分析_spring actuator源码分析_33

寻找匹配的增强器

前面的函数中已经完成了所有增强器的解析,但是对于所有增强器来讲,并不一定都适用于当前的Bean,还要挑取出适合的增强器,也就是满足我们配置的通配符的增强器。具体实现在findAdvisorsThatCanApply中。

spring actuator源码分析 spring aop源码分析_封装_34

spring actuator源码分析 spring aop源码分析_AOP_35

创建代理

在获取了所有对应bean的增强器后,便可以进行代理的创建了。

spring actuator源码分析 spring aop源码分析_AOP_36

spring actuator源码分析 spring aop源码分析_spring actuator源码分析_37

对于代理类的创建及处理,Spring委托给了ProxyFactory去处理,而在此函数中主要是对

ProxyFactory的初始化操作,进而对真正的创建代理做准备,这些初始化操作包括如下内容。

1 获取当前类中的属性。

2 添加代理接口。

3 封装Advisor并加入到ProxyFactory中。

4 设置要代理的类。

5 当然在Spring中还为子类提供了定制的函数customizeProxyFactory,子类可以在此函数中进行对ProxyFactory的进一步封装。

6 进行获取代理操作。

其中,封装Advisor并加入到ProxyFactory中以及创建代理是两个相对繁琐的过程,可以通过ProxyFactory提供的addAdvisor方法直接将增强器置入代理创建工厂中,但是将拦截器封装为增强器还是需要一定的逻辑的。

spring actuator源码分析 spring aop源码分析_spring actuator源码分析_38

spring actuator源码分析 spring aop源码分析_AOP_39

由于Spring中涉及过多的拦截器、增强器、增强方法等方式来对逻辑进行增强,所以非常有必要统一封装成Advisor来进行代理的创建,完成了增强的封装过程,那么解析最重要的一步就是代理的创建与获取了。

spring actuator源码分析 spring aop源码分析_AOP_40

创建代理过程

spring actuator源码分析 spring aop源码分析_封装_41

到此已经完成了代理的创建,不管我们之前是否阅读过Spring的源代码,但是都或多或少地听过对于Spring的代理中JOKProxy的实现和CglibProxy的实现。Spring是如何选取的呢?网上的介绍有很多,现在我们就从源代码的角度分析,看看到底Spring是如何选择代理方式的。

从计中的判断条件可以看到3个方面影响着Spring的判断。

optimize:用来控制通过CGLIB创建的代理是否使用激进的优化策略。除非完全了解AOP代理如何处理优化,否则不推荐用户使用这个设置。目前这个属性仅用于CGLIB代理,对于JDK动态代理(默认代理)无效。

proxyTargetClass:这个属性为true时,目标类本身被代理而不是目标类的接口。如果这个属性值被设为true,CGUB代理将被创建,设置方式为<aop:aspectj-autoproxy-proxy-target-class="true"/>。

hasNoUserSuppliedProxyinterfaces:是否存在代理接口。

下面是对JDK与Cglib方式的总结。

如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP。

如果目标对象实现了接口,可以强制使用CGLIB实现AOP。

如果目标对象没有实现接口,必须采用CGLIB库,Spring会自动在JDK动态代理和CGLIB之间转换。

如何强制使用CGLIB实现AOP?

添加CGLIB库,Spring—HOME/cglib/*.jar。

在Spring配置文件中加入<aop:aspectj-autoproxy proxy-target-class="true"/>。

JDK动态代理和CGLIB字节码生成的区别?

JDK动态代理只能对实现了接口的类生成代理,而不能针对类。

CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,因为是继承,所以该类或方法最好不要声明成final。

获取代理过程

确定了使用哪种代理方式后便可以进行代理的创建了,但是创建之前有必要回顾一下两种方式的使用方法。

1 JDK代理使用示例。

创建业务接口,业务对外提供的接口,包含着业务可以对外提供的功能。

spring actuator源码分析 spring aop源码分析_spring actuator源码分析_42

spring actuator源码分析 spring aop源码分析_AOP_43

spring actuator源码分析 spring aop源码分析_动态代理_44

用起来很简单,其实这基本上就是AOP的一个简单实现了,在目标对象的方法执行之前和执行之后进行了增强。Spring的AOP实现其实也是用了Proxy和InvocationHandler这两个东西的。

我们再次来回顾一下使用JDK代理的方式,在整个创建过程中,对于InvocationHandler

的创建是最为核心的,在自定义的InvocationHandler中需要重写3个函数。

构造函数,将代理的对象传入。

invoke方法,此方法中实现了AOP增强的所有逻辑。getProxy方法,此方法于篇一律,但是必不可少。

那么,我们看看Spring中的JDK代理实现是不是也是这么做的呢?继续之前的跟踪,到达JdkDynamicAopProxy的getProxy。

spring actuator源码分析 spring aop源码分析_动态代理_45

通过之前的示例我们知道,JDKProxy的使用关键是创建自定义的InvocationHandler,而InvocationHandler中包含了需要覆盖的函数getProxy,而当前的方法正是完成了这个操作。再次确认一下JdkDynamicAopProxy也确实实现了InvocationHandler接口,那么我们就可以推断出,在JdkDynamicAopProxy中一定会有个invoke函数,并且JdkDynamicAopProxy会把AOP的核心逻辑写在其中。查看代码,果然有这样一个函数:

spring actuator源码分析 spring aop源码分析_AOP_46

spring actuator源码分析 spring aop源码分析_动态代理_47

spring actuator源码分析 spring aop源码分析_spring actuator源码分析_48

上面的函数中最主要的工作就是创建了一个拦截器链,并使用ReflectiveMethodInvocation类进行了链的封装,而在ReflectiveMethodlnvocation类的proceed方法中实现了拦截器的逐一调用,那么我们继续来探究,在proceed方法中是怎么实现前置增强在目标方法前调用后置增强在目标方法后调用的逻辑呢?

spring actuator源码分析 spring aop源码分析_spring actuator源码分析_49

在proceed方法中,或许代码逻辑并没有我们想象得那么复杂,ReflectiveMethodlnvocation中的主要职责是维护了链接调用的计数器,记录着当前调用链接的位置,以便链可以有序地进行下去,那么在这个方法中并没有我们之前设想的维护各种增强的顺序,而是将此工作委托给了各个增强器,使各个增强器在内部进行逻辑实现。

2 CGLIB使用示例。

CGLIB是一个强大的高性能的代码生成包。它广泛地被许多AOP的框架使用,例如SpringAOP和dynaop,为它们提供方法的Interception(拦截)。最流行的ORMapping工具Hibernate也使用CGLIB来代理单端single-ended(多对一和一对一)关联(对集合的延迟抓取是采用其他机制实现的)。EasyMock和jMock是通过使用模仿(moke)对象来测试Java代码的包。它们都通过使用CGLIB来为那些没有接口的类创建模仿(moke)对象。

CGLIB包的底层通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类。除了CGLIB包,脚本语言例如Groovy和BeanShell,也是使用ASM来生成Java的字节码。当然不鼓励直接使用ASM,因为它要求你必须对NM内部结构(包括class文件的格式和指令集)都很熟悉。

我们先快速地了解CGLIB的使用示例。

spring actuator源码分析 spring aop源码分析_spring actuator源码分析_50

spring actuator源码分析 spring aop源码分析_动态代理_51

可以看到System.out.println(demo),demo首先调用了toString()方法,然后又调用了hashCode,生成的对象为EnhancerDemo$$EnhancerByCGLIB$$bc9b2066的实例,这个类是运行时由CGLIB产生的。

完成CGLIB代理的类是委托给Cglib2AopProxy类去实现的,我们进入这个类一探究竞。按照前面提供的示例,我们容易判断出来,Cglib2AopProxy的入口应该是在getProxy,也就是说在Cglib2AopProxy类的getProxy方法中实现了Enhancer的创建及接口封装。

spring actuator源码分析 spring aop源码分析_spring actuator源码分析_52

spring actuator源码分析 spring aop源码分析_封装_53

以上函数完整地阐述了一个创建Spring中的:Enhancer的过程,读者可以参考Enhancer的文档查看每个步骤的含义,这里最重要的是通过getCallbacks方法设置拦截器链。

spring actuator源码分析 spring aop源码分析_spring actuator源码分析_54

spring actuator源码分析 spring aop源码分析_AOP_55

spring actuator源码分析 spring aop源码分析_AOP_56

在getCallback中Spring考虑了很多情况,但是对于我们来说,只需要理解最常用的就可以了,比如将advised属性封装在DynamicAdvisedlnterceptor并加入在callbacks中,这么做的目的是什么呢,如何调用呢?在前面的示例中,我们了解到CGLIB中对于方法的拦截是通过将自定义的拦截器(实现Methodlnterceptor接口)加入Callback中并在调用代理时直接激活拦截器中的intercept方法来实现的,那么在getCallback中正是实现了这样一个目的,Dynamic­Advisedlnterceptor继承自Methocllnterceptor,加入Callback中后,在再次调用代理时会直接调用DynamicAdvisedlnterceptor中的intercept方法,由此推断,对于CGLIB方式实现的代理,其核心逻辑必然在DynamicAdvisedlnterceptor中的intercept中。

spring actuator源码分析 spring aop源码分析_封装_57

spring actuator源码分析 spring aop源码分析_spring actuator源码分析_58

上述的实现与JDK方式实现代理中的invoke方法大同小异,都是首先构造链,然后封装此链进行串联调用,稍有些区别就是在IDK中直接构造ReflectiveMethodlnvocation,而在cglib中使用CglibMethodlnvocation。CglibMethodlnvocation继承自ReflectiveMethodlnvocation,但是proceed方法并没有重写。

静态AOP使用示例

加载时织入(Load-Time Weaving , LTW)指的是在虚拟机载入字节码文件时动态织入AspectJ切面。Spring框架的值添加为AspectJ LTW在动态织入过程中提供了更细粒度的控制。使用Java(5+)的代理能使用一个叫"Vanilla"的AspectJ LTW,这需要在启动NM的时候将某个JVM参数设置为开。这种NM范闱的设置在一些情况下或许不错,但通常情况下显得有些粗颗粒。而用Spring的LTW能让你在per-ClassLoader的基础上打开LTW,这显然更加细粒度并且对“单JVM多应用”的环境更具意义(例如在一个典型应用服务器环境中)。另外,在某些环境下,这能让你使用LTW而不对应用服务器的启动脚本做任何改动,不然则需要添加-javaagent:path/to/aspectjweaver.jar或者(以下将会提及)-javaagent:path/to/Spring-agent.jar。开发人员只需简单修改应用上下文的一个或几个文件就能使用LTW,而不需依靠那些管理者部署配置,比如启动脚本的系统管理员。

我们还是以之前的AOP示例为基础,如果想从动态代理的方式改成静态代理的方式需要

做如下改动。

1. Spring全局配置文件的修改,加入LWT开关。

spring actuator源码分析 spring aop源码分析_封装_59

spring actuator源码分析 spring aop源码分析_动态代理_60

2. 加入aop.xml。在class目录下的META-INF(没有则自已建立)文件夹下建立aop.xml,内容如下:

spring actuator源码分析 spring aop源码分析_封装_61

spring actuator源码分析 spring aop源码分析_AOP_62

创建AOP静态代理

AOP的静态代理主要是在虚拟机启动时通过改变目标对象字节码的方式来完成对目标对象的增强,它与动态代理相比具有更高的效率,因为在动态代理调用的过程中,还需要一个动态创建代理类并代理目标对象的步骤,而静态代理则是在启动时便完成了字节码增强,当系统再次调用目标类时与调用正常的类并无差别,所以在效率上会相对高些。

Instrumentation使用

Java在l.5引入Java.lang.instrument,你可以由此实现一个Java agent,通过此agent来修改类的字节码即改变一个类。本节会通过Java Instrument实现一个简单的profiler。当然instrument并不限于profiler,instrument可以做很多事情,它类似一种更低级、更松耦合的AOP,可以从底层来改变一个类的行为。你可以由此产生无限的遐想。接下来要做的事情,就是计算一个方法所花的时间,通常我们会在代码中按以下方式编写。

在方法开头加入long stime = System.nanoTime();,在方法结尾通过System.nanoTime()-stime得出方法所花时间。你不得不在想监控的每个方法中写入重复的代码,好一点的情况,你可以用AOP来做这事,但总是感觉有点别扭,这种profiler的代码还是要打包在你的项目中,Java Instrument使得这一切更干净。

写ClassFileTransformer类

spring actuator源码分析 spring aop源码分析_动态代理_63

spring actuator源码分析 spring aop源码分析_spring actuator源码分析_64

编写agent类

spring actuator源码分析 spring aop源码分析_AOP_65

上面两个类就是agent的核心了,NM启动时在应用加载前会调用PerfMonAgent.premain,然后PerfMonAgent.premain中实例化了一个定制的ClassFileTransforrne,即PerfMonXformer并通过inst.addTransforrner(trans)把PerfMonXformer的实例加入Instrumentation实例(由JVM传入),这就使得应用中的类加载时,PerfMonXforrner.transform都会被调用,你在此方法中可以改变加载的类。真的有点神奇,为了改变类的字节码,我使用了JBoss的Javassist,虽然你不一定要这么用,但JBoss的Javassist真的很强大,能让你很容易地改变类的字节码。在上面的方法中我通过改变类的字节码,在每个类的方法入口中加入了long stime = System.nanoTime(),在方法的出口加入了:

System.out.println("methodClassName.methodName:"+(System.nano'rime()-stime));

打包agent

对于agent的打包,有点讲究。

JAR的META-INF/MANIFEST.MF加入Premain-Class:xx,xx在此语境中就是我们的agent类,即org.toy.PerfMonAgent。

如果你的agent类引入别的包,需使用Boot-Class-Path:xx,xx在此语境中就是上面提到的JBossjavassit,即/home/pwlazy/.m2/repository/javassistJjavassistJ3.8.0.GA/javassist-3.8.0.GA.jar。

下面附上Maven的POM。

spring actuator源码分析 spring aop源码分析_AOP_66

spring actuator源码分析 spring aop源码分析_动态代理_67

打包应用

spring actuator源码分析 spring aop源码分析_动态代理_68

由执行结果可以看出,执行顺序以及通过改变org.toy.App的字节码加入监控代码确实生效了。你也可以发现,通过Instrment实现agent使得监控代码和应用代码完全隔离了。

通过之前的两个小示例我们似乎已经有所体会,在Spring中的静态AOP直接使用了AspectJ提供的方法,而AspectJ又是在Instrument基础上进行的封装。就以上面的例子来看,至少在AspectJ中会有如下功能。

读取META-INF/aop.Xml。

将aop.Xml中定义的增强器通过自定义的ClassFileTransformer织入对应的类中。

当然这都是AspectJ所做的事情,并不在我们讨论的范畴,Spring是直接使用AspectJ,也就是将动态代理的任务直接委托给了AspectJ,那么,Spring怎么嵌入AspectJ的呢?同样我们还是从配置文件入手。

自定义标签

在Spring中如果需要使用AspectJ的功能,首先要做的第一步就是在配置文件中加入配置:<Context:load-time-weaver/>。我们根据之前介绍的自定义命名空间的知识便可以推断,引用AspectJ的入口便是这里,可以通过查找load-time-weaver来找到对应的自定义命名处理类。

通过Eclipse提供的字符串搜索功能,我们找到了ContextNamespaceHandler,在其中有这样一段函数。

spring actuator源码分析 spring aop源码分析_AOP_69

继续跟进LoadTimeWeaverBeanDefinitionParser,作为BeanDefinitionParser接口的实现类,它的核心逻辑是从parse函数开始的,而经过父类的封装,LoadTimeWeaverBeanDefinitionParser类的核心实现被转移到了doParse函数中,如下:

spring actuator源码分析 spring aop源码分析_封装_70

spring actuator源码分析 spring aop源码分析_动态代理_71

其实之前在分析动态AOP也就是在分析配置<aop:aspectj-autoproxy/>中已经提到了自定义配置的解析流程,对于<aop:aspectj-autoproxy/>的解析无非是以标签作为标志,进而进行相关处理类的注册,那么对于自定义标签<Context:load-time-weaver/>其实是起到了同样的作用。

上面函数的核心作用其实就是注册一个对于ApectJ处理的类org.Springframework.Contextweaving.AspectJWeavingEnabler,它的注册流程总结起来如下。

1 是否开启AspectJ。

之前虽然反复提到了在配置文件中加入了<Context:load-time-weaver/>便相当于加入了AspectJ开关。但是,并不是配置了这个标签就意味着开启了AspectJ功能,这个标签中还有一个属性aspectj-weaving,这个属性有3个备选值,on、off和autodetect,默认为autodetect,也就是说,如果我们只是使用了<Context:load-time-weaver/>,那么Spring会帮助我们检测是否可以使用AspectJ功能,而检测的依据便是文件META-INF/aop.xml是否存在,看看在Spring中的实现方式。

spring actuator源码分析 spring aop源码分析_spring actuator源码分析_72

2 将org.Springframework.context.weaving.AspectJWeavingEnabler封装在BeanDefinition中注册。

当通过AspectJ功能验证后便可以进行AspectJWeavingEnabler的注册了,注册的方式很简单,无非是将类路径注册在新初始化的RootBeanDefinition中,在RootBeanDefinition的获取时会转换成对应的class。

尽管在init方法中注册了AspectJWeavingEnabler,但是对于标签本身Spring也会以bean的形式保存,也就是当Spring解析到<contex:t:load-time-weaver/>标签的时候也会产生一个bean,而这个bean中的信息是什么呢?

spring actuator源码分析 spring aop源码分析_AOP_73

单凭以上的信息我们至少可以推断,当Spring在读取到自定义标签<Context:load-time-weaver/>后会产生一个bean,而这个bean的id为loadTimeWeaver,class为org.Springframework.Context.weavingDefaultContextLoadTimeWeaver,也就是完成了DefaultContextLoadTimeWeaver类的注册。

完成了以上的注册功能后,并不意味这在Spring中就可以使用AspectJ了,因为我们还有一个很重要的步骤忽略了,就是LoadTimeWeaverAwareProcessor的注册。在AbstractApplicationContext中的prepareBeanFactory函数中有这样一段代码:

spring actuator源码分析 spring aop源码分析_动态代理_74

织入

当我们完成了所有的AspectJ的准备工作后便可以进行织入分析了,首先还是从LoadTimeWeaverAwareProcessor开始。

LoadTimeWeaverAwareProcessor实现BeanPostProcessor方法,那么对于BeanPostProcessor接口来讲,postProcessBeforelnitialization与postProcessAfterlnitialization有着其特殊意义,也就是说在所有bean的初始化之前与之后都会分别调用对应的方法,那么在LoadTimeWeaverAwareProcessor中的postProcessBeforelnitialization函数中完成了什么样的逻辑呢?

spring actuator源码分析 spring aop源码分析_封装_75

我们综合之前讲解的所有信息,将所有相关信息串联起来一起分析这个函数。

在LoadTimeWeaverAwareProcessor中的postProcessBeforelnitialization函数中,因为最开始的if判断注定这个后处理器只对LoadTimeWeaverAware类型的bean起作用,而纵观所有的bean,实现LoadTimeWeaver接口的类只有AspectJWeavingEnabler。

当在Spring中调用AspectJWeavingEnabler时,this.loadTimeWeaver尚未被初始化,那么,会直接调用beanFactory.getBean方法获取对应的DefaultContextLoadTimeWeaver类型的bean,并将其设置为AspectJWeavingEnabler类型bean的loadTimeWeaver属性中。当然AspectJWeavingEnabler同样实现了BeanClassLoaderAware以及Ordered接口,实现BeanClassLoaderAware接口保证了在bean初始化的时候调用AbstractAutowireCapableBeanFactory的invokeAwareMethods的时候将beanClassLoader赋值给当前类。而实现Ordered接口则保证在实例化bean时当前bean会被最先初始化。

而DefaultContextLoadTimeWeaver类又同时实现了LoadTimeWeaver、BeanClassLoaderAware以及DisposableBean。其中DisposableBean接口保证在bean销毁时会调用destroy方法进行bean的清理,而BeanClassLoaderAware接口则保证在bean的初始化调用AbstractAutowireCapable­BeanFactory的invokeAwareMethods时调用setBeanClassLoader方法。

spring actuator源码分析 spring aop源码分析_AOP_76

spring actuator源码分析 spring aop源码分析_动态代理_77

这句代码不仅仅是实例化了一个InstrumentationLoadTimeWeaver类型的实例,而且在实例化过程中还做了一些额外的操作。

在实例化的过程中会对当前的this.instrumentation属性进行初始化,而初始化的代码如下:this.instrumentation = getlnstrumentation(),也就是说在InstrumentationLoadTimeWeaver实例化后其属性Instrumentation已经被初始化为代表着当前虚拟机的实例了。综合我们讲过的例子,对于注册转换器,如addTransformer函数等,便可以直接使用此属性进行操作了。

也就是经过以上程序的处理后,在Spring中的bean之间的关系如下。

AspectJWeavingEnabler类型的bean中的LoadTimeWeaver属性被初始化为DefaultContextLoadTimeWeaver类型的bean。

DefaultContextLoadTimeWeaver类型的bean中的loadTimeWeaver属性被初始化为InstrumentationLoadTimeWeaver。

因为AspectJWeavingEnabler类同样实现了BeanFactoryPostProcessor,所以当所有bean解析结束后会调用其postProcessBeanFactory方法。

spring actuator源码分析 spring aop源码分析_spring actuator源码分析_78

spring actuator源码分析 spring aop源码分析_spring actuator源码分析_79

AspectJClassBypassingClassFileTransformer的作用仅仅是告诉AspectJ以org.aspectj开头的或者org/aspectj开头的类不进行处理。