AOP基础知识
概念和术语
连接点:应用程序执行期间明确定义的一个点。eg:方法调用、方法调用本身、类初始化、对象实例化;
通知(Advice):在特定连接点执行的代码
就是通知。通知类型举例:前置通知(在连接点之前执行),后置通知(在连接点之后执行);
切入点(Pointcut):用于定义何时执行的
连接点集合
切面(Advisor):封装 类中 通知和切入点的集合
;
织入:在适当的位置 将切面插入到 应用程序 代码中的过程;编译时AOP:在生成时完成
;运行时AOP:在运行态执行
;AspectJ还支持另一种加载时织入(LTW)的植入机制;拦截底层的JVM类加载器,在加载字节码时织入
;
目标对象(target):由AOP进程修改的对象
称为 目标对象;通常也被称为 被通知对象;
引入:通过引入其他方法 来改变 对象结构 的过程。可以通过引入 AOP来使任何对象实现特定的接口,而无需对象的类显式实现该接口;
AOP类型
静态AOP
织入过程 是 应用程序生成的一个步骤;
修改 实际的字节码 来实现 AOP织入过程,最终结果是Java字节码
,并且运行时无需特殊技巧确定应该何时执行;
缺点:对切面做的任何修改都需要 重新编译整个程序;
AspectJ的编译时织入 就是 静态AOP;
动态AOP
织入过程 在运行时 动态执行;
缺点:性能不如静态AOP;
优点:可以轻松修改 应用程序的整个切面集,而无需重新编译主应用程序代码;
选择AOP类型
静态AOP和动态AOP各有优点
静态AOP: 实现时间长,倾向于实现更多功能丰富的实现;具有更多的可用连接点;而且如果性能很重要,就倾向于静态AOP;
如果需要SpringAOP未实现的功能 就需要 AspectJ;
Spring AOP示例
Spring AOP实现两个逻辑部分:
1、AOP内核:完全解耦的纯编程 AOP功能;
2、使 AOP 更易于使用的 一组框架服务;
AOP Alliance
许多开源项目共同定制的 一组AOP标准接口。
Spring使用了AOP Alliance 接口
官网:http://aopalliance.sourceforge.net/
AOP Alliance示例
1、pom.xml中引入jar包
1 | <dependency> |
2、定义目标对象
1 | public class Agent { |
3、定义增强类 AgentDecorator
目标:打印 James Bond!
1 | import org.aopalliance.intercept.MethodInterceptor; |
实现 AOP Alliance 标准接口 MethodInterceptor,围绕 invocation.proceed() 实现 环绕通知(around advice);
4、将invocation.proceed()方法 织入 目标对象 中
1 | import org.springframework.aop.framework.ProxyFactory; |
运行结果:
1 | Bond |
Spring AOP架构
Spring AOP的核心架构 基于 代理。
想要创建一个类的被通知实例时,必须使用ProxyFactory创建该类的代理实例;
大多数情况下,我们可以使用声明式AOP配置(ProxyFactoryBean、AOP名称空间和@AspectJ样式注解)
来完成代理的创建;
但我们需要了解代理的创建过程;因此,首先延时代理创建的编程方法,再讨论 Spring的声明式AOP配置;
运行时,Spring会分析为 ApplicationContext中bean定义的横切关注点
,并动态生成代理 bean(封装了底层的目标bean)。不会直接调用目标bean,而是将调用者注入代理bean。然后代理bean分析运行条件,并相应的织入适当的通知。
Spring中的两种代理实现:JDK动态代理、CGLIB代理
Spring中的连接点
Spring只支持一种连接点类型:方法调用;
如果需要其他连接点:则可以使用Spring和AspectJ;
Spring中的切面
在Spring AOP中,切面由实现了 Advisor 接口的类的实例表示
。
Advisor有两个子接口:PointcutAdvisor 和 IntroductionAdvisor。
关于ProxyFactory类
ProxyFactory 控制 SpringAOP 中织入和代理创建过程;
1 | Agent proxy = ((Agent) proxyFactory.getProxy()); |
ProxyFactory将代理创建委托给 DefaultProxyFactory的一个实例,继而又委托给 ObjenesisCglibAopProxy 或 JdkDynamicAopProxy
1 | proxyFactory.addAdvice(new AgentDecorator()); |
addAdvice将传入的通知封装到 DefaultPointcutAdvisor的一个实例中,并使用默认的方法进行配置
ProxyFactory可以创建多个代理,每个代理都有不同的切面
1 | proxyFactory.addAdvice(); |
在Spring中创建通知
Spring中支持6中通知
通知名称 | 接口 | 描述 |
---|---|---|
前置通知 | org.springframework.aop.MethodBeforeAdvice | 在连接点之前执行 |
后置返回通知 | org.springframework.aop.AfterReturningAdvice | 在连接点return之后 执行 |
后置通知 | org.springframework.aop.AfterAdvice | 连接点执行完成之后执行 |
环绕通知 | org.aopalliance.intercept.MethodInterceptor | 在连接点前后都执行 |
异常通知 | org.springframework.aop.ThrowsAdvice | 在异常执行以后执行,捕获特定异常 |
引入通知 | org.springframework.aop.IntroductionAdvisor | 将引入建模为特殊类型的拦截器 |
通知(Advice)的接口
通知(Advice)和顾问(Advisor)的区别在于:Advisor可以携带相关切入点的通知,从而更细致的控制在哪个连接点上拦截通知。
Advice的层次结构图:
创建前置通知(MethodBeforeAdvice)
创建目标对象:
1 | public interface Singer { |
创建前置通知:
1 | import org.springframework.aop.MethodBeforeAdvice; |
设置切入点并运行示例:
1 | import org.springframework.aop.framework.ProxyFactory; |
运行结果:
1 | Before 'sing' , tune guitar. |
创建后置返回通知
1 | import org.springframework.aop.AfterReturningAdvice; |
创建异常通知(ThrowsAdvice)
ThrowsAdvice内置四个接口,通过反射调用:
1 | public void afterThrowing(Exception ex) |
示例:
1 | import org.springframework.aop.ThrowsAdvice; |
执行结果:
可以看到只执行了第二个方法,通过Cglib代理;
使用顾问(Advisor)和切入点(Pointcut)
避免硬编码 将方法检查 放入通知,尽可能使用切入点来控制通知目标上方法的适用性;
如果通知跟目标 之间有 目标关联性,则在通知内部检查也可以;
Pointcut接口
1 | package org.springframework.aop; |
1 | package org.springframework.aop; |
1 | package org.springframework.aop; |
MethodMatcher 分为 静态MethodMatcher 和 动态 MethodMatcher;
Spring 通过 isRuntime() 判断 MethodMatcher是 静态(false),还是动态(true);
1、静态切入点:
Spring 针对目标上的每个方法,调用一次MethodMatcher的matches(Method, Class<?> )
方法,并缓存返回值;
2、动态切入点:
Spring先调用 matches(Method, Class<?> )
方法进行静态检查,返回true时,再调用 matches(Method, Class<?>, Object[])
对每个参数进一步检查;
比较:
静态Pointcut 执行的比较好,动态Pointcut更灵活;推荐尽量使用静态Pointcut;
使用动态Pointcut 可以 避免 不必要的通知调用;通知调用对性能影响很大;
可用的Pointcut实现
4.0开始Spring提供了八个Pointcut接口的实现:2个用作创建静态、动态切入点的抽象类,6个具体类;
每个具体类可以完成的操作:
一起构成多个切入点
处理控制流切入点
执行简单的基于名称的匹配
使用正则表达式定义切入点
使用AspectJ定义切入点
定义在类或者方法级别查找特定注解的切入点
实现类 | 描述 |
---|---|
org.springframework.aop.support.annotation. AnnotationMatchingPointcut |
在类或方法上查找指定Java注解 |
org.springframework.aop.aspectj. AspectJExpressionPointcut |
使用AspectJ植入器以AspectJ语法评估切入点表达式 |
org.springframework.aop.support. ComposablePointcut |
ComposablePointcut类使用诸如union()和intersection()等操作组合两个或更多的切入点 |
org.springframework.aop.support. ControlFlowPointcut |
ControlFlowPointcut是一种特殊的切入点,他们匹配另一个方法的控制流中的所有方法,即任何作为另一个方法的结果而直接或间接调用的方法 |
org.springframework.aop.support. DynamicMethodMatcherPointcut |
构建动态切入点的基类 |
org.springframework.aop.support. JdkRegexpMethodPointcut |
正则表达式支持定义切入点 |
org.springframework.aop.support. NameMatchMethodPointcut |
创建一个切入点,对方法名称列表执行简单匹配 |
org.springframework.aop.support. StaticMethodMatcherPointcut |
构建静态切入点的基础 |
使用DefaultPointcutAdvisor
使用Pointcut之前,必须先创建 Advisor接口的实例,具体的说是创建一个PointcutAdvisor接口;
Advisor是Spring中某个切面的表示,它是 通知 和 切入点 的 结合体;
DefaultPointcutAdvisor 将 点那个Pointcut 和 Advice 相关联;
1、使用 StaticMethodMatcherPointcut 创建静态切入点
思路:创建两个目标对象类GoodGuitarist和GreatGuitarist,通过DefaultPointcutAdvisor创建这两个类的代理,但通知 只应用到GoodGuitarist;
创建两个目标对象类:
1 | public class GoodGuitarist implements Singer { |
创建静态切入点:
1 | import org.springframework.aop.ClassFilter; |
创建通知:
1 | import org.aopalliance.intercept.MethodInterceptor; |
使用DefaultPointcutAdvisor组织Pointcut和Advice:
1 | import com.learn.aop.Singer; |
运行结果:
1 | >> Invoke: sing |
可以看到只有 GoodGuitarist的sing方法被通知,GreatGuitarist没有被通知;
2、使用DynamicMethodMatcherPointcut创建动态切入点
在本例中,只想通知foo()方法,而且 只有在 int参数 不等于 100 时,才通知该方法
创建目标对象:
1 | public class SampleBean { |
创建针对foo的切入点:
1 | import org.springframework.aop.ClassFilter; |
创建Advisor组织切入点和通知,调用代理对象:
1 | import com.learn.aop.pointcut.staticp.SimpleAdvice; |
运行结果:
可以看到只有两个foo()被通知到,x=100时没有被通知;bar()没有被通知到;
foo进行了两次静态检查,一次在初始阶段,一次在被调用时;
3、使用简单名称匹配
无需考虑方法签名,只根据方法名匹配;
1 | public class Guitar { |
使用NameMatchMethodPointcut匹配:
1 | import com.learn.aop.pointcut.staticp.SimpleAdvice; |
运行结果:
4、使用正则表达式创建切入点
创建目标对象:
1 | import com.learn.aop.Singer; |
创建Advisor组织切入点和通知,并调用:
1 | import com.learn.aop.pointcut.staticp.SimpleAdvice; |
运行结果:
5、使用 AspectJ切入点表达式 创建切入点
AspectJ切入点表达式
在pom.xml中添加:
1 | <dependency> |
使用 AspectJ表达式 实现 JDK正则表达式 相同的功能:
1 | import com.learn.aop.pointcut.regex.Guitarist; |
运行结果:
6、创建注解匹配切入点
基于注解定义切入点AnnotationMatchingPointcut;
定义注解:
1 | import java.lang.annotation.ElementType; |
在方法上使用注解:
1 | public class Guitarist implements Singer { |
对注解类织入切面:
1 | import com.learn.aop.pointcut.name.Guitar; |
运行结果:
了解代理ProxyFactory
到目前为止,只是粗略的了解了一下 ProxyFactory的使用;
代理的核心:拦截方法调用,必要时 执行适用于特定方法的通知链;
通知的管理和调用 独立于代理,由Spring AOP框架管理。而代理主要负责拦截对方法的调用,并将它们根据需要传递给AOP框架,以便应用通知。
Spring有两种代理:JDK代理、CGLIB代理;
JDK动态代理
只能生成接口的代理,不能生成类的代理;
如何处理特定方法调用的决策 都在 运行时 做出;
无法区分 被通知方法 和 未被通知方法;
1 | public static void runJdkTests(Advisor advisor, SimpleBean target) { |
CGLIB代理
CGLIB会为每个代理 动态生成新类的字节码,并尽可能重用已生成的类;
所生成的类 是 目标对象target类的子类;
每次创建CGLIB代理时,CGLIB会询问Spring如何处理每个方法;
由于CGLIB生成实际的字节码,因此在处理方法上很灵活;
CGLIB生成固定通知链以后,可以减少执行通知链的运行时间开销;
1 | public static void runCglibTests(Advisor advisor, SimpleBean target) { |
比较代理性能
定义Target:
1 | public interface SimpleBean { |
1 | public class DefaultSimpleBean implements SimpleBean { |
定义通知:
1 | import org.springframework.aop.MethodBeforeAdvice; |
定义切入点:
1 | import org.springframework.aop.support.StaticMethodMatcherPointcut; |
分别调用JDK代理,CGLIB代理,CGLIB Frozen代理,比较他们的性能:
1 | import org.springframework.aop.Advisor; |
运行结果如下:
1 | >> Running CGLIB (Standard) Tests |
可以看到JDK代理的性能最低,其次,CGLIB Frozen的性能要明显好于 CGLIB;
选择要使用的代理
CGLIB 可以代理类和接口,而JDK只能代理 接口;
CGLIB Frozen模式性能明显好于 JDK代理和CGLIB代理;
如果要使用CGGLIB代理接口,必须使用proxyFactory.setOptimize(true);
方法将ProxyFactory的optimize标志设置为true;
切入点的高级使用
前面讲了6个Pointcut的使用,如果如下更大的灵活性,可以使用 ComposablePointcut 和 ControlFlowPointcut
使用控制流切入点ControlFlowPointcut
定义target:
1 | public class TestBean { |
定义通知:
1 | import org.springframework.aop.MethodBeforeAdvice; |
使用控制流ControlFlowPointcut:
1 | import org.springframework.aop.Advisor; |
运行结果:
1 | Trying normal invoke |
关键代码:
1 | Pointcut pointcut = new ControlFlowPointcut(ControlFlowDemo.class, "test"); |
表示 为ControlFlowDemo类的test()方法 创建了一个 ControlFlowPointcut实例, 切入从ControlFlowDemo.test() 调用 target 的所有方法
;
target 在 test()之外调用时,并不会被通知;只有在test()内调用时,才会被通知;
缺点:在其他切入点上使用 ControlFlowPointcut 会大大降低性能
使用组合切入点ComposablePointcut
在前面的实例中,仅为每个Advisor使用了一个切入点。在大多数情况下,这通常是足够的,但有时需要多个Pointcut组合在一起 共同实现目标;
ComposablePointcut 支持两种方法:union() 《等价于 “or”运算》和 intersection()《等价于 “and”运算》;
可以通过传入 ClassFilter、MethodMatcher、Pointcut 调用 union() 和 intersection();
使用案例:
1 | import com.learn.aop.pointcut.controlflow.SimpleBeforeAdvice; |
运行结果:
1 | Test 1 >>> |
“Test 1” 匹配si开头的方法,所以两个 sing() 方法都被通知;
“Test 2”匹配si开头的方法 或 匹配方法名为talk的方法,所以两个 sing() 方法 和 talk() 都被通知;
“Test 3” 匹配( 以si 开头 || talk()) && 以st 结尾, 没有方法符合,所以没有方法被通知;如果有一个方法名为 si***st() 那么,应该会被调用;
组合和切入点接口
org.springframework.aop.support.Pointcuts 接口也能用来 组合两个Pointcut,但更多Pointcut的组合还是试用ComposablePointcut;
引入入门
引入基础知识
一种特殊类型的环绕通知;
适用于类级别,不能在使用引入时 使用切入点,两者语义不匹配;
实现 接口 IntroductionInterceptor来创建引入:
继承 类 DelegatingIntroductionInterceptor 来创建引入:
引入通知 构成了 被通知对象 状态的一部分;因此,每个被通知对象 都必须有一个独立的引入实例;
使用引入进行对象修改检测
定义接口:
1 | public interface IsModified { |
创建通知Advice类:
1 | import java.lang.reflect.Method; |
创建顾问Advisor, 关联通知IsModifiedMixin:
1 | import org.springframework.aop.support.DefaultIntroductionAdvisor; |
创建目标对象Target:
1 | // 创建对象target |
组装 Advisor 通知 和 target:
1 | import org.springframework.aop.IntroductionAdvisor; |
运行结果:
1 | Is Contact? : true |
proxy对象 既是 target, 又是IsModified;
proxyInterface.isModified() 第一次调用 查看初始状态John Mayer:未修改;
proxyInterface.isModified() 第二次调用 设为同样的值John Mayer:未修改;
proxyInterface.isModified() 第三次调用 设为新的值Eric Clapton:已经修改;
proxyInterface.isModified() 第四次调用 设为新的值Eric Clapton:已经修改;
proxyInterface.isModified() 第吴次调用 设为新的值Eric Clapton:已经修改;
目前只能检测一次变更;如果去掉 IsModifiedMixin 中的 if (!isModified) 就能检测多次变更;
1 | Is Contact? : true |
引入小结
引入时Spring AOP最强大的 功能之一:不仅可以扩展现有方法的功能; 还可以动态扩展接口和对象实现。
AOP 框架事务
我们不得不编写大量的代码 来生成代理;
Spring提供了额外的框架,将代理注入到目标bean中;
以声明的方式配置AOP
使用Spring AOP 声明式配置时,存在三个选项:
- 使用ProxyFactoryBean, ProxyFactoryBean提供了一种声明方式配置Spring的ApplicationContext(以及底层的BeanFactory);
- 使用Spring AOP 命名空间:
- 使用@AspectJ 样式注解:
使用ProxyFactoryBean
ProxyFactoryBean 允许 指定一个bean作为目标,并且为 该 bean 提供一组通知Advice 和 顾问Advisor(这些Advice 和 Advisor最终被合并到一个AOP代理中);
ProxyFactoryBean 将 拦截器 应用于现有的 目标bean;