一. Spring AOP 概念
-
Aspect
:
一个模块化的概念, 表示一个横跨多个 class 的 切面.tansaction management 就是一个切面的例子
-
join point
:
程序执行过程中的一个点, 比如执行一个方法或是进行异常处理. Spring aop 中, join point 通常表现为一个方法的执行 -
Advie (通知, 增强)
:
切面 (Aspect) 在某个 join point (表现为一个方法) 上执行的动作. 该动作有不同形式(before, after, around). spring aop 中, advice 表现为一个拦截器
, 或是围绕 joint point 的一系列拦截器
-
pointcut
:
一种对匹配 join point 的预言. advice 和point expression
组合, 然后在 join point 上执行. 用切点表达式来匹配找到 join point 是 spring aop 的核心 -
Introdution
:
spring aop 可以让被通知, 被增强 (adviced) 的对象实现新的接口. -
Target Object
:
也叫Adviced Object
, 是被一个或多个切面增强的对象. 这个对象一般是代理对象, spring aop 是运行时生成代理对象 -
AOP proxy
被 AOP 框架创建来实现切面功能的对象. spring aop中, aop proxy 是JDK动态代理
或CGLIB代理
-
Weaving
链接切面和对象创建出被增强的对象 (target object
) 这个过程
就叫织入. 织入可以在编译阶段
( Aspectj 编译器), 也可以在运行阶段
.
spring aop 是运行阶段织入
三. 声明 PointCut
- 声明方式
有2种声明 pointcut 的方式- 使用
@Pointcut(切点表达式)
在类中的某个方法上注解, 该方法要是无参数且返回 void 的
@Pointcut("execution(* transfer(..))") // the pointcut expression private void anyOldTransfer() {} // the pointcut signature
- 使用
@Advice(切点表达式)
同时声明 advice 和切点
- 使用
- 切点表达式例子
- 执行的所有 public 方法
execution(public * *(..))
- 执行的所有 set 开头的方法
execution(* set*(..))
- 执行的所有
AccountService
接口的方法
execution(* com.xyz.service.AccountService.*(..))
- 执行的 service 包下的所有方法
execution(* com.xyz.service.*.*(..))
- 执行的 service 包或其子包下的方法
execution(* com.xyz.service..*.*(..))
-
within
: 匹配特定包下的所有 join point -
this
与target
this
的作用是匹配一个代理对象 (proxy object), 这个代理对象是一个给定类型的实例(instance of).
target
匹配的是一个目标对象(target object), 这个目标对象是一个给定类型的实例(instance of).
四. 声明 Advice
Around Advice
- 使用方式
- 有机会在被增强方法的 before 和 after 处执行, 也可以决定是否放行被增强的方法.
- 使用
@Around
注解声明. 增强方法 (advice method) 的第一个参数必须是ProceedingJoinPoint
, 其proceed([]Object)
方法可以放行内部的方法, 参数为内部方法的参数列表
- 示例:
import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; @Aspect public class BeforeExample { @Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation()") // pointcut 表达式在 CommonPointcuts 类的 dataAccessOperation 方法上 public void doAccessCheck() { // ... } }
- Advice 参数
-
如何获取 joinpoint 的信息
getArgs()
: Returns the method arguments.
getThis()
: Returns the proxy object.
getTarget()
: Returns the target object.
getSignature()
: Returns a description of the method that is being advised.
toString()
: Prints a useful description of the method being advised. -
在 advice body 处, 如何得知 join point 的消息?
- 通过
参数名
传递 advice参数
@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)") public void audit(Auditable auditable) { AuditCode code = auditable.value(); // ... }
切点表达式中
args(account ...)
部分有2个目的: (1) 预匹配的 join cut , 是至少有1个参数的方法, 且参数类型是Account
; (2) 让真实的Account 参数
可以被 advice 使用- 通过
类型和参数名
传递泛型参数
如果 joinpoint 的参数是泛型参数, spring 通过具体类型+参数名进行参数传递
// joinpoint 实现的接口 public interface Sample<T> { void sampleGenericMethod(T param); void sampleGenericCollectionMethod(Collection<T> param); }
对
T
类型传递@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)") public void beforeSampleMethod(MyType param) { // Advice implementation }
对
Collection<T>
类型传递@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)") public void beforeSampleMethod(Collection<MyType> param) { // Advice implementation }
- 通过
-
- Spring 如何知道函数的参数名称呢?
因为 Java 反射无法得到函数的参数名, 所以 Spring 采用如下方式得知参数名
首先
, 在 advice 和 pointcut 注解上有一个参数argNames
, 可以手动指定, 让 advice 和 join point 的参数明对应;然后
, 如果编译程序时, 加上了-g:vars
属性, spring 可以分析 debug 日志获取参数名;然后
, 最重要的是 spring-core 使用LocalVariableTableParameterNameDiscoverer
类的getMethodInfo
方法, 通过 ASM 字节码操控获取参数名. 具体做法参考 为什么spring可以获取参数名,而mybatis不可以
五. advice 顺序与实例模型
-
Advice 顺序
- 当有好几个 @Aspect 的 adive 要在同一个 joinpoint 上运行时, 就要指定不同 advice 的优先级. 可以让 class 实现
core.Ordered
接口, 或在 class 上使用@Order
注解指定优先级.Ordered.getOrder()
方法返回的数值越小, 表示优先级越高 - 如果是同一个 @Aspect 的不同 advice 方法要在同一个 joinpoint 上运行, 优先级基于 advice type. 有嫌疑从低到高为
@Around
,@Before
,@After
(finally),@AfterReturning
,@AfterThrowing
(catch)
- 当有好几个 @Aspect 的 adive 要在同一个 joinpoint 上运行时, 就要指定不同 advice 的优先级. 可以让 class 实现
-
实例模型
默认下, 只有一个单例对象在 spring 容器中. 但是 spring 还额外支持2种实例模型-
perthis
: 为每个匹配切点表达式的类声明一个切面实例 -
pertarget
:
https://www.jianshu.com/p/a0b9c53ac019
-
五. 完整的 AOP 例子
六. Spring AOP 的代理机制
- JDK动态代理 or CGLIB 代理
- 特点
如果target object
实现了至少一个接口, spring 将使用JDK动态代理
. 被 target object 实现的接口下的所有方法都会被代理; 如果 target object 没有实现任何接口, 则使用 CGLIB 动态代理.
有事想让 spring 强制使用 CGLIB 动态代理. (比如: 想对只在 target object 中声明, 但没在实现的接口中声明的方法进行代理).
CGLIB 代理有以下几个问题
:- 无法对 final 声明的方法进行代理. 因为运行时生成的
子类
无法覆盖 final 方法 - 被代理对象的构造函数只能被调用一次
- 无法对 final 声明的方法进行代理. 因为运行时生成的
- 强制使用 CGLIB 代理的做法
要想在 @Aspect 定义的 advice 上使用 CGLIB, xml 添加如下属性配置<aop:aspectj-autoproxy proxy-target-class="true"/>
- 特点
- 关于被代理对象的
this
, 在被调用时, 是指向代理对象, 还是指向 target object- 结论: this 指的是 target object, 而不是 proxy object. 即 this 的语义并不更改
- 如果想让 this 指向代理对象, 可以这样做
// target object class SimplePojo implements Pojo { public void foo() { this.bar(); } public void bar() { } } interface Pojo { public void foo(); public void bar(); } public class TestProxy { public static void main(String[] args) { ProxyFactory factory = new ProxyFactory(new SimplePojo()); factory.addInterface(Pojo.class); factory.addAdvice(new RetryAdvice()); factory.setExposeProxy(true); // 暴露代理对象, 这个是关键 Pojo pojo = (Pojo) factory.getProxy(); pojo.foo(); // foo() 中的 this, 就会指向带对象 } }
网友评论