一次把 AOP 吃透

AOP SPRING

Posted by gomyck on June 9, 2021

整理 AOP 相关知识点, 一次性搞懂

在搞懂 AOP 之前, 我想在说AOP, 大部分人都会把它与 spring AOP 划等号, 实际上 spring AOP 是对 aop 的一种扩展和实现

AOP 实际上是一组规范, 规定了一套如何解决面相切面编程的规范, 也就是 AOPALLIANCE http://aopalliance.sourceforge.net/

spring 的作者 Rod Johnson 也是 aopalliance 的主要贡献者, 这也就不难看出, 为什么 spring 的 aop 做的如此出色, 因为在最底层的设计都是同一个人操刀, 思想肯定是统一的

1. 浅谈 AOPALLIANCE

最初版本的 aop 联盟包仅有两个子包: aop 和 intercept, 下面分别介绍下各个包和接口类

aop 包: 抽象了最高等级的 AOP 行为

Advice: 实现该接口的类被标记为通知类, 一般来说, 实现通知类的方式都是拦截器

通知可以理解为, 以某个逻辑点作为参照, 当 逻辑点 执行之前, 会有通知, 执行之后会有通知, 而通知的实现就是实现通知接口的类

AspectException: 切面异常

intercept 包: advice 的一种实现包

这个包下有几个比较重要的接口: Interceptor Joinpoint

Interceptor: 继承了 advice, 是通知的一种实现方式, 通过拦截实现, 它还有几个子类, 分别是方法拦截器和构造方法拦截器

Joinpoint: 运行时的集合点, 相当于一个上述的逻辑点, 通知就是围绕Joinpoint来进行工作的, 它还有几个子类, Invocation相当程序调用, 也是某个逻辑点, 还有更具体的方法调用点, 和构造方法调用点

2. spring aop

术语:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
AOP的术语很多,虽然不清楚术语我们也能很熟练地使用AOP,但是要理解分析源码,术语就需要深刻体会其含义。

通知(Advice) 增强的具体实现

目标对象(Target) 被代理|织入增强逻辑的类

连接点(JoinPoint) 程序执行到某个特定的逻辑点

切点(Pointcut) 一组逻辑点称为切点, 通过切点, 可以匹配多个连接点

引介(Introduction) 引介是一种特殊的增强, 它为类添加一些属性和方法. 即使一个类原本没有实现某个接口, 通过AOP的引介功能, 我们可以动态地为该类添加接口的实现逻辑, 让类成为这个接口的实现类

织入(Weaving) 即如何将增强添加到目标对象的连接点上,有动态(运行期生成代理)、静态(编译期、类加载时期)两种方式。

代理(Proxy) 目标对象被增强后,就会产生一个代理对象,该对象可能是和原对象实现了同样的一个接口(JDK),也可能是原对象的子类(CGLIB)。

切面(Aspect、Advisor) 切面由切点和增强组成

spring aop 的实现很有意思, 它造了一半的轮子, 然后借了一半的轮子, 也就是代理类生成使用 jdk proxy cglib, 切面处理规范使用了 aspectj 的接口, 很有趣, 这让很多时候, 让我一直以为 aspectj 就是 spring aop 的全部

实际上 aspectj 是个很大很全的工具包, 它的官网如下: https://www.eclipse.org/aspectj/

1
2
3
4
5
lib/
├── aspectjrt.jar     运行时
├── aspectjtools.jar  工具包
├── aspectjweaver.jar 织入规范
└── org.aspectj.matcher.jar

spring 依赖的是 weaver 包, 至于工具包, 它并没有用, 里面有牛逼哄哄的 ajc 编译器, 可以在编译期将代码织入目标类, spring aop 使用的是代理方式增强类, 与织入的方式不同(反射), 而且效率也没有织入的高

我个人认为通过动态代理可以更好的更换运行时代理对象, 虽然在业务稳定环境下, 没有织入效率高, 但是个人认为织入有局限性, 具体哪里存在局限性, 还有待找资料学习补充

spring aop 扩展

spring aop 扩展了 advice 和 intercept, 而且自定义了一种包含通知的容器类: Advisor, 这个抽象类规定了, 实现该接口的类, 必须内部要保持一种通知实现

这个类我个人认为主要是为了聚合一些行为: 比如获取通知类, 获取集合点信息等操作, 可以通过其子类 PointcutAdvisor看出来

同时 spring 扩展了 advice, 把其细分为 前置通知BeforeAdvice, 后置通知AfterAdvice, 使通知的具体细节更加完善, 更细化的就是 java 目前的最小颗粒度通知: 方法的生命周期, MethodBeforeAdvice, AfterReturningAdvice

spring 上下文刷新方法中 通过 beanFactory 后置处理器, 对实例化的 bean 进行了增强处理, BeanFactoryPostProcessor, 具体的代码简直看的脑仁疼, 感兴趣的可以看看AbstractAutoProxyCreator这个类, 是增强处理的具体实现

通过后置处理器, 可以使 interceptor 可以介入到实际的 pointcut 中

aspectj 的 weaver 包使用

定义一个类, 实现AbstractPointcutAdvisor, 在内部维护一个拦截器: MethodInterceptor, 自己实现其方法, 在 getPointcut() 中, 我们有如下方式去定义集合点:

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
Pointcut pointcut = new AnnotationMatchingPointcut(CkDemo.class);

//AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();

// 指定注解拦截
//pointcut.setExpression("@annotation(com.gomyck.demo.CkDemo)");

// 表达式拦截  规则为: execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
// 参数匹配((param-pattern))可以指定具体的参数类型,多个参数间用“,”隔开,各个参数也可以用“”来表示匹配任意类型的参数,如(String)表示匹配一个String参数的方法
// (,String) 表示匹配有两个参数的方法,第一个参数可以是任意类型,而第二个参数是String类型;可以用(…)表示零个或多个任意参数
//pointcut.setExpression("execution(* com.gomyck.demo.CkDemoBean.*(..))");

// 指定类型, 方法会被拦截, 但是父类子类不会被拦截
//pointcut.setExpression("within(com.gomyck.demo.CkDemoBean)");

// 为可以逆变为此类型的匹配类型拦截(当前类和其子类-支持多重继承) this(代理对象) -> 表达式的转变
// this 和 target 的区别是使用 cglib 和 jdk proxy 时  会有点区别, 其他情况下一样
//pointcut.setExpression("this(com.gomyck.demo.CkDemoBeanParent)");

// 为可以逆变为此类型的匹配类型拦截(当前类和其子类-支持多重继承) target(被代理对象) -> 表达式的转变
// this 和 target 的区别是使用 cglib 和 jdk proxy 时  会有点区别, 其他情况下一样
//pointcut.setExpression("target(com.gomyck.demo.CkDemoBean)");

// 根据参数类型拦截 必须要和其他表达式一起使用, 否则会把框架本身的方法也拦截到, 很不友好
//pointcut.setExpression("args(java.lang.String) && execution(* com.gomyck.demo.CkDemoBean.*(..))");

需要源代码的小伙伴可以加我微信 如果帮助到你, 请我喝杯星巴克吧~