我们在业务开发中,使用得最多的是面向对象编程(OOP),因为它的代码逻辑直观,从上往下就能查看完整的执行链路。
在这个基础上延伸,出现了面向切面编程(AOP),将可以重复性的横切逻辑抽取到统一的模块中。
例如日志打印、安全监测,如果按照 OOP
的思想,在每个方法的前后都要加上重复的代码,之后要修改的话,更改的地方就会太多,导致不好维护。所以出现了 AOP
编程, AOP
所关注的方向是横向的,不同于 OOP
的纵向。
所以接下来一起来学习 AOP
是如何使用以及 Spring
容器里面的处理逻辑~
如何使用
之前由于业务开发中需要用到 AOP
,所以也整理一篇 Spring自定义注解实现AOP,感兴趣的同学可以去看看~
接下来是书中的例子:
创建用于拦截的 bean
|
|
创建 Advisor
|
|
首先类打上了 @Aspect
注解,让 Spring
认识到这个是一个切面 bean
,在方法打上 @Pointcut("execution(* *.testAop(..))")
,表示这是一个切点方法,execution()
内部的表达式指明被拦截的方法,Before
、After
、Around
分别表示在被拦截方法的前、后已经环绕执行。
创建配置文件 aop.xml
|
|
测试 Demo
|
|
根据上面的启动例子,发现在自己写的核心业务方法 testAop()
上,明明只是简单打印了 I am the true aop bean
,但执行结果输出了其它内容,说明这个类被增强了,在不修改核心业务方法上,我们对它进行了扩展。证明了 AOP 可以使辅助功能独立于核心业务之外,方便了程序的扩展和解耦。
使用起来很方便,接下来一起来看看 Spring
是如何实现 AOP
功能的吧~
动态 AOP 自定义标签
之前在介绍自定义标签时,提到了 AOP
的实现也借助了自定义注解,根据自定义标签的思想:每个自定义的标签,都有对应的解析器,然后借助强大的开发工具 IDEA
定位功能,找到解析器注册的地方:
- 按住
ctrl
,定位标签对应的xsd
文件 - 根据命名文件,在
META-INF
目录下找到了spring.handlers
文件 - 在处理器文件中发现了处理器
AopNamespaceHandler
|
|
处理器继承自 NamespaceHandlerSupport
,在加载过程中,将会执行 init
初始化方法,在这里,会注册 aspectj-autoproxy
类型的解析器 AspectJAutoProxyBeanDefinitionParser
如何注册自定义解析器之前也了解过了,所以接下来直接来看看,遇到 aspectj-autoproxy
类型的 bean
,程序是如何解析的。
注册 AnnotationAwareAspectJAutoProxyCreator
来看下解析时,它的入口方法如下:
|
|
入口方法一如既往的简洁,交代了要做的事情,然后具体复杂逻辑再交给工具类或者子类继续实现,所以接下来要看的是如何注册 AnnotationAwareAspectJAutoProxyCreator
。
|
|
可以看到这个方法内部有三个处理逻辑,所以我们来一个一个去分析了解:
注册或者升级 AnnotationAwareAspectJAutoProxyCreator
对于 AOP
的实现,基本上都是靠 AnnotationAwareAspectJAutoProxyCreator
去完成,它可以根据 @Point
注解定义的切点来自动代理相匹配的 bean
。
由于 Spring
替我们做了很多工作,所以开发 AOP
业务时才可以这么简单,连配置也简化了许多,所以来看下 Spring
是如何使用自定义配置来帮助我们自动注册 AnnotationAwareAspectJAutoProxyCreator
。
|
|
这个步骤中,实现了自动注册 AnnotationAwareAspectJAutoProxyCreator
类,同时能看到涉及到优先级的概念和注册名一直都是 AUTO_PROXY_CREATOR_BEAN_NAME
。
处理 proxy-target-class 以及 expose-proxy 属性
|
|
关于 AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
方法,它是一个属性设置的过程,如果解析到的属性为 true
,将它们加入代理注册器的属性列表中,这里不细说下去。
将这两个属性分开熟悉:
proxy-target-class
Spring AOP
部分使用 JDK
动态代理 (Proxy + InvocationHandler),或者 CGLIB
(Code Generation LIB)来为目标对象创建代理。书中提到,推荐使用的是 JDK
动态代理。
如果被代理的目标对象实现了至少一个接口,则会使用 JDK
动态代理。所有该目标类型实现的接口都将被代理。
若该目标对象没有实现任何接口,则创建一个 CGLIB
代理。如果希望代理目标对象的所有方法,而不只是实现自接口的方法,可以通过该属性 proxy-target-class
开启强制使用 CGLIB
代理。
但是强制开启 CGLIB
会有以下两个问题:
- 无法同时(advise)Final 方法,因为他们不能被覆写
- 需要将 CGLB 二进制发行包放在 classpath 下面
如果考虑好上面两个方面,那就可以通过以下两个地方来强制开启 CGLIB
代理:
|
|
其中有关 CGLIB
代理,这位老哥讲得很透彻,建议大家可以去了解一下~ Cglib及其基本使用
expose-proxy
有时候目标对象内部的自我调用将无法实施切面中的增强。
例如两个方法都加上了事务注解 @Transactional
但是事务类型不一样:
|
|
此处的 this
指向了目标对象, this.b()
方法将不会执行 b
事务的切面,即不会执行事务增强。
为了解决这个问题,使 a()
和 b()
方法同时增强,可以通过 expose-proxy
来实现:
|
|
注册组件并通知
emmmm,这个方法内部逻辑如名字一样清晰,所以不细说啦。
创建 AOP 代理
前面主要围绕着自动代理器 AnnotationAwareAspectJAutoProxyCreator
的注册流程来讲解,接下来看自动代理器做了什么来完成 AOP
的操作。
下面是 AnnotationAwareAspectJAutoProxyCreator
的继承体系:
在图片右上角,发现它实现了 BeanPostProcessor
接口,之前文章提到过,它是一个后处理器,可以在 bean
实例化前后进行扩展。查看了实现了该接口的两个方法,postProcessBeforeInitialization
没有做处理,直接返回该对象。
实际进行处理的是 postProcessAfterInitialization
方法,在 bean
实例化之后的处理,在这一步中进行里代理增强,所以来看下这个方法:
|
|
来提取一下核心流程:
- 获取增强方法或者增强器
我们刚才写的@Before
、@After
之类的,就是增强方法,AOP
处理时,要先找出这些增强方法。 - 根据获取的增强进行代理
找到增强方法后,需要对这些增强方法进行增强代理,实际上这个bean
已经不完全是原来的类型了,会变成代理后的类型。
获取增强方法或者增强器
入口方法在这里:
|
|
对于指定 bean
的增强方法的获取包含这两个步骤,获取所有的增强以及寻找所有增强中适用于 bean
的增强并应用。对应于 findCandidateAdvisors
和 findAdvisorsThatCanApply
这两个方法。如果没找到对应的增强器,那就返回 DO_NOT_PROXY
,表示不需要进行增强。
由于逻辑太多,所以接下来贴的代码不会太多,主要来了解它的大致流程,有需要的可以跟着源码工程的注释跟踪完整的流程~:
寻找对应的增强器 findCandidateAdvisors
|
|
实际来看,关键是这个方法 this.aspectJAdvisorsBuilder.buildAspectJAdvisors()
这个方法看起来简单,但是实际处理的逻辑很多,代码深度也很多,所以为了避免太多代码,我罗列了主要流程,和关键的处理方法做了什么
主要流程如下:
- 获取所有 beanName,会将之前在 beanFactory 中注册的 bean 都提取出来。
- 遍历前一步骤提取出来的 bean 列表,找出打上 @AspectJ 注解的类,进行进一步处理
- 继续对前一步提取的 @AspectJ 注解的类进行增强器的提取
- 将提取结果加入缓存中
可以查询代码中的注释,从 [注释 8.3] 到 [注释 8.8 根据切点信息生成增强器] 都是这个方法的处理逻辑
※※在这个流程的最后一步中,会将识别到的切点信息(PointCut)和增强方法(Advice)进行封装,具体是由 Advisor
的实现类 InstantiationModelAwarePointcutAdvisorImpl
进行统一封装。
|
|
封装体前半部分逻辑只是简单赋值。关键是这个方法 instantiateAdvice(this.declaredPointcut)
,在这一步中,对不同的增强(Before/After/Around)实现的逻辑是不一样的。在 ReflectiveAspectJAdvisorFactory#getAdvice
方法中区别实现了根据不同的注解类型封装不同的增强器。
|
|
最后切点方法通过解析和封装成 Advisor
,提取到的结果加入到缓存中。细心的你可能会发现除了普通的增强器外,还有另外两种增强器:同步实例化增强器和引介增强器。由于用的比较少,所以我看到源码中这两个分支处理没有深入去学习,感兴趣的同学请继续深入学习这两种增强器~
获取匹配的增强器 findAdvisorsThatCanApply
在前面流程中,已经完成了所有增强器的解析,但是对于前面解析到的增强器,并不一定都适用于当前处理的 bean
,所以还需要通过一个方法来挑选出合适的增强器。
|
|
可以看到,具体实现过滤操作的是工具类方法 AopUtils.findAdvisorsThatCanApply
:
|
|
具体判断逻辑在 canApply()
方法中,如果判断符合条件的,加入到 eligibleAdvisors
中,最后返回对于这个 bean
适合的增强器列表。
创建代理
通过前面的流程,获取到了所有对应 bean
的增强器后,可以开始代理的创建。
|
|
对于代理类的创建和处理, Spring
委托给了 ProxyFactory
去处理,在上面贴出的函数主要是对 ProxyFactory
的初始化操作,进而对真正的创建代理做准备,主要流程如下:
- 获取当前类的属性
- 添加代理接口
- 封装 Advisor 并加入到 ProxyFactory
- 设置要代理的类
- 为子类提供定制的函数 customizeProxyFactory,子类通过该方法对 ProxyFactory 进行进一步的封装
- 进行获取代理操作
比较关键的是第三个步骤和第六个步骤,其中在第三个步骤中,进行的是拦截器包装,详细代码流程请查 [注释 8.9 为给定的bean创建AOP代理] 和 [注释 8.10 包装拦截器,封装成 Advisor]。
接着,完成了所有增强器的封装过程,到了解析的最后一步,进行代理的创建和获取。
|
|
创建代理 createAopProxy()
定位到创建代理的代码:
|
|
从上面代码中能看出,根据了几个关键属性,判断创建的是哪种类型的 AopProxy
,一种是 JDK 动态代理,另一种是 CGLIB 动态代理。
前面提到过的 proxy-target-class
属性和 targetClass
属性,在这里判断了应该创建哪一个代理。
获取代理 getProxy()
观察图片以及前面分析,可以知道有两种代理方式:[JDK 动态代理] 和 [CGLIB 动态代理]
同时先说下动态代理的含义:抽象类在编译期间是未确定具体实现子类,在运行时才生成最终对象。
JDK 动态代理
JDK
代理是默认推荐的代理方式,使用的是 Proxy
+ InvocationHandler
。
可以通过以下方式实现:定义一个接口、实现类,和一个处理器继承于 InvocationHandler
,然后重载处理器中的 invoke
方法,对代理对象进行增强。
JdkDynamicAopProxy.java
|
|
获取代理的核心步骤在 Proxy.newProxyInstance(classLoader, proxiedInterfaces, this)
,第三个参数是 JdkDynamicAopProxy
本身,而且它实现了 InvocationHandler
接口,重载了 invoke
方法。
org.springframework.aop.framework.JdkDynamicAopProxy#invoke
|
|
创建 JDK
代理过程中,主要的工作时创建了一个拦截器链,并使用 ReflectiveMethodInvocation
类进行封装,封装之后,逐一调用它的 proceed
方法, 用来实现在目标方法的前置增强和后置增强。
org.springframework.aop.framework.ReflectiveMethodInvocation#proceed
|
|
具体代码和注释请定位到该方法查看。关于 JDK
动态代理,深入学习的话也可以单独拎出来,所以推荐看这篇资料 小豹子带你看源码:JDK 动态代理,进行了和学习
CGLIB 动态代理
CGLIB[Code Generation LIB]
是一个强大的高性能的代码生成包。它广泛应用于许多 AOP
框架。
再次推荐参考资料一,这位老哥将 CGLIB
代理, 详细介绍了 CGLIB
在什么场景使用,以及被它增强后代码处理顺序,Cglib及其基本使用。
希望看完这篇文章,能过了解到 CGLIB
代码生成包具体是如何对类进行增强。
代理增强结果
通过前面一系列步骤,解析标签、属性、增强方法,到最后获取 CGLIB
代理,通过代理创建 bean
来看下最后被代理的 bean
内部:
从图中可以看到,最终创建的是被修饰后的 bean
,内部很明显是 CGGLIB
代理生成的代码,我们在不修改业务代码的情况下,实现了方法增强。
静态 AOP
既然有动态代理,那么也会有静态代理。
使用静态 AOP
的时候,需要用到 LTW
(Load-Time Weaving 加载时织入),指的是在虚拟机载入字节码文件时动态织入 AspectJ
切面。
AOP
的静态代理主要是在虚拟机启动时通过改变目标对象字节码的方式来完成对目标对象的增强,它与动态代理相比具有更高的效率,因为在动态代理调用的过程中,还需要一个动态创建代理类并代理目标对象的步骤,而静态代理则是在启动时便完成了字节码增减,当系统再次调用目标类时,与调动正常的类并无区别,所以在效率上会相对高些。
关于静态 AOP
的使用和学习,可以参考这篇文章:从代理机制到Spring AOP
总结
动态 AOP
使用起来很简单,对于如何实现,总结起来就两点:
- 动态解析
AOP
标签 - 创建
AOP
代理
但在 Spring
底层实现逻辑却是复杂到不行,从 Spring
框架中可以看到这是良好的代码设计思路,顶层入口尽量简单,使用者很容易就能掌握该功能,复杂实现逻辑都被隐藏了。
写这一篇 AOP
学习总结,花了将近一周,先看了一遍书籍, 下班后花了一晚,将大致流程理了一遍,第二天晚上走读代码,发现有些地方还存在疑惑,例如 JDK
和 cglib
动态代理是怎么回事,翻阅查询资料,弄懂后又过了一天。
将代码注释加上,分析动态代理每一个步骤做的事情,结合之前学的后处理器 BeanPostProcessor
知识和自定义标签解析知识一起又梳理一遍。零零散散,终于整理完成。
在静态 AOP
知识点,按照我的理解,越往系统底层深入,它的执行效率越高,所以减少了动态创建代理类和代理目标对象的步骤,静态代理的速度会得到提升。同时由于接近底层后,代码编写的复杂度同样会增加,所以我在权衡高频率使用场景(动态代理),本次学习没有详细去了解,留下这个坑,以后有机会再填吧~
由于个人技术有限,如果有理解不到位或者错误的地方,请留下评论,我会根据朋友们的建议进行修正
Gitee 地址 https://gitee.com/vip-augus/spring-analysis-note.git
Github 地址 https://github.com/Vip-Augus/spring-analysis-note
传送门:
参考资料
Spring 源码深度解析 / 郝佳编著. – 北京 : 人民邮电出版社