Spring(二)-AOP
参考文献
- 路人甲-Spring系列
AOP
-
AOP是一种编程范式,通过将横切关注点(如日志记录、安全性检查等)与核心业务逻辑进行分离,可以使代码更加模块化、可维护性更高.
-
Spring框架中的AOP模块提供了一种方便的方式来实现AOP,它采用了动态代理和字节码增强技术,可以在运行时动态地将切面逻辑织入到目标对象的方法中,从而实现AOP的目的.
-
Aspect-Oriented Programming
,意为面向切面编程. -
作用:在程序中具有公共特性的某些类/某些方法上进行拦截, 在方法执行的前面/后面/执行结果返回后增加执行一些方法.
AOP
相关概念
- 目标对象(
target
): 目标对象将要被增强的对象,即包含主业逻辑的类对象. - 连接点(
join point
): 连接点,程序运行的某一个点,比如执行某个方法,在Spring AOP中join point
总是表示一个方法的执行. - 代理对象(
proxy
): AOP中会通过代理的方式,对目标对象生成一个代理对象,代理对象中会加入需要增强功能,通过代理对象来间接的访问目标对象,起到增强目标对象的效果. - 通知(
advice
): 是切面在连接点上执行的代码,用于实现横切关注点的功能- 通知中有两个重要信息:
- 方法的什么地方(之前,之后,包裹目标对象,方法抛出异常等)
- 执行什么操作
- 通知中有两个重要信息:
- 切入点(
point cut
): 用来指定需要将通知使用到哪些地方,比如需要用在哪些类方法上,切入点就是做这个配置的. - 切面(
aspect
): 通知(advice
)和切入点(point cut
)的组合.- 定义在哪些地方(
ponit cut
)执行什么操作(advice
), Spring
中定义为advisor
(通知器)
- 定义在哪些地方(
- 顾问(Advisor):其实它就是
Pointcut
与Advice
的组合,Advice
是要增强的逻辑,而增强的逻辑要在什么地方执行是通过Pointcut
来指定的,所以Advice
必需与Pointcut
组合在一起,这就诞生了Advisor
这个类,Spring Aop
中提供了一个Advisor
接口将Pointcut
与Advice
的组合起来.Advisor
有好几个称呼:顾问、通知器.
AOP Spring
中相关接口
Joinpoint
接口
1 | public interface Joinpoint { |
Invocation
接口
- 此接口表示程序中调用,调用是一个连接点,可以被拦截器拦截.
1 | public interface Invocation extends Joinpoint { |
MethodInvocation
接口
- 方法调用的描述,在方法调用时提供拦截器.方法调用是一个连接点,可以被方法拦截器拦截.
1 | public interface MethodInvocation extends Invocation { |
通知(Advice
)
- 通知中用来实现被增强的逻辑,通知中有2个关注点:方法的什么地方,执行什么操作.
Advice
接口
1 | public interface Advice { |
1 | classDiagram |
AfterReturningAdvice
接口
- 方法执行后通知,需要在目标方法执行之后执行增强一些逻辑,可以通过这个实现.
- 注意点: 目标方法正常执行后,才会回调这个接口,当目标方法有异常,那么这通知会被跳过.
1 | public interface AfterReturningAdvice extends AfterAdvice { |
MethodBeforeAdvice
接口
- 方法执行前通知,需要在目标方法执行前执行一些逻辑的,可以通过这个实现.
1 | public interface MethodBeforeAdvice extends BeforeAdvice { |
ThrowsAdvice
接口
1 | public interface ThrowsAdvice extends AfterAdvice { |
-
此接口上没有任何方法,因为方法由反射调用,实现类必须实现一下形式,前三个参数可选,最后一个参数为需要匹配的异常类型.
1
public void afterThrowing([Method, args, target], ThrowableSubclass)
1
2
3
4
5
6
7
8
9
10
11
12
13public void afterThrowing(RemoteException ex) throws Throwable {
}
/** Not valid, wrong number of arguments */
public void afterThrowing(Method m, Exception ex) throws Throwable {
}
// Full method signature
public void afterThrowing(Method m, Object[] args, Object target, IOException ex) {
}
MethodInterceptor
接口
- 方法拦截器,可以实现
AfterReturningAdvice/MethodBeforeAdvice/ThrowsAdvice
3种类型的通知.
1 |
|
切入点
- 通知(
Advice
)用来指定需要增强的逻辑.但是哪些类的哪些方法中需要使用这些通知呢?这个就是通过切入点来配置的,切入点对应在Spring
中一个接口.
Pointcut
接口
1 | public interface Pointcut { |
ClassFilter
接口
1 |
|
MethodMatcher
接口
1 | public interface MethodMatcher { |
MethodMatcher
过滤的整个过程- 调用
boolean matches(Method method, Class<?> targetClass)
方法,验证方法是否匹配 boolean isRuntime()
方法是否为true,如果为false,则以第一步的接口为准,否则继续向下- 调用
boolean matches(Method method, Class<?> targetClass, Object... args)
方法继续验证,这个方法多了一个参数,可以对目标方法传入的参数进行校验.
- 调用
顾问(Advisor
)
- 通知定义了需要做什么,切入点定义了在哪些类的哪些方法中执行通知.顾问则是将两者结合起来.
Advisor
接口
1 | public interface Advisor { |
1 | classDiagram |
IntroductionAdvisor
接口
- 一个Java类,没有实现A接口,在不修改Java类的情况下,使其具备A接口的功能.可以通过
IntroductionAdvisor
给目标类引入更多接口的功能.
1 | public interface IntroductionAdvisor extends Advisor, IntroductionInfo { |
PointcutAdvisor
接口
- 内部有个获取
Pointcut
方法,AOP中使用到都大部分Advisor
都属于这种类型的
1 | public interface PointcutAdvisor extends Advisor { |
通知包装器
-
负责将各种非
MethodInterceptor
类型的通知(Advice
)包装MethodInterceptor
类型. -
Aop
中所有的Advice
最终都会转换为MethodInterceptor
类型的,组成一个方法调用链,然后执行 -
3个包装器类:
MethodBeforeAdviceInterceptor
AfterReturningAdviceInterceptor
ThrowsAdviceInterceptor
MethodBeforeAdviceInterceptor
类
- 这个类实现了
MethodInterceptor
接口,负责将MethodBeforeAdvice
方法前置通知包装为MethodInterceptor
类型,创建这个类型的对象的时候需要传递一个MethodBeforeAdvice
类型的参数,重点是invoke
方法.
1 |
|
AfterReturningAdviceInterceptor
类
- 这个类实现了 MethodInterceptor接口,负责将 AfterReturningAdvice方法后置通知包装为MethodInterceptor类型,创建这个类型的对象的时候需要传递一个方法AfterReturningAdvice类型的参数,重点是invoke方法.
1 |
|
ThrowsAdviceInterceptor
类
1 | public class ThrowsAdviceInterceptor implements MethodInterceptor, AfterAdvice { |
- 异常通知,自定义处理异常的方法有几个特点
- 方法名称必须为 afterThrowing.
- 方法参数必须1个或4个,最后一个参数是 Throwable类型或其子类型.
- 可以在异常处理中记录一些异常信息,这个还是比较有用的,但是注意一点目标方法抛出的异常最后还是会向外继续抛出
创建代理
- 创建代理所需要的参数配置
- 根据代理参数获取
AopProxy
- 通过
AopProxy
获取代理对象
创建代理所需要的参数
- 创建代理所需要参数配置主要是通过
AdvisedSupport
这个类来做
根据代理参数获取AopProxy
TargetClassAware
接口
1 | public interface TargetClassAware { |
Advised
接口
1 | public interface Advised extends TargetClassAware { |
ProxyConfig
类
-
这个类比较关键,代理配置类,内部包含了创建代理时需要配置的各种参数
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
27
28
29
30
31
32
33
34
35
36
37
38
39public class ProxyConfig implements Serializable {
/** use serialVersionUID from Spring 1.2 for interoperability. */
private static final long serialVersionUID = -8409359707199703185L;
/**
* 标记是否直接对目标类进行代理,而不是通过接口产生代理
*/
private boolean proxyTargetClass = false;
/**
* 标记是否对代理进行优化。启动优化通常意味着在代理对象被创建后,增强的修改将不会生效,因此
* 默认值为false。
* 如果exposeProxy设置为true,即使optimize为true也会被忽略。
*/
private boolean optimize = false;
/**
* 标记是否需要阻止通过该配置创建的代理对象转换为Advised类型,默认值为false,表示代理对象
* 可以被转换为Advised类型
*/
boolean opaque = false;
/**
* 标记代理对象是否应该被aop框架通过AopContext以ThreadLocal的形式暴露出去。
* 当一个代理对象需要调用它自己的另外一个代理方法时,这个属性将非常有用。默认是是false,以
* 避免不必要的拦截。
*/
boolean exposeProxy = false;
/**
* 标记该配置是否需要被冻结,如果被冻结,将不可以修改增强的配置。
* 当我们不希望调用方修改转换成Advised对象之后的代理对象时,这个配置将非常有用。
*/
private boolean frozen = false;
// ... 略
}
根据配置获取AopProxy
1 | AdvisedSupport advisedSupport = new AdvisedSupport(); |
- 此阶段会根据
AdvisedSupport
中配置信息,判断具体是采用cglib
的方式还是采用jdk
动态代理方式获取代理对象.
AopProxy
接口
- 这个接口定义了一个方法,用来创建最终的代理对象,这个接口有2个实现类
CglibAopPoxy
: 采用cglib
的方式来创建代理对象JdkDynamicAopPoxy
: 采用jdk
动态代理的方式来创建代理对象
DefaultAopProxyFactory
1 | public class DefaultAopProxyFactory implements AopProxyFactory, Serializable { |
JdkDynamicAopProxy
- 被创建的代理对象默认会实现
SpringProxy,Advised,DecoratingProxy
3个接口 SpringProxy
这个接口中没有任何方法,只是起一个标记的作用,用来标记代理对象是使用Spring Aop
创建的- 代理对象都会默认实现
Advised
接口,所以可以通过这个接口动态变更代理对象中的通知. DecoratingProxy
接口中定义了一个getDecoratedClass
方法,用来获取被代理的原始目标对象的类型.
示例
1 |
|
Aop创建代理的方式
- 主要分为两大类
- 手动方式: 需要通过硬编码一个个创建代理
- 自动化方式: 也称为批量的方式,批量的方式用在Spring环境中,通过bean后置处理器来对符合条件的bean创建代理.
- 左边
ProxyCreateSupport
下面都是手动的方式,有3个类 - 右边
AbstractAutoProxyCreator
下面的类都是自动创建代理的方式
手动3种方式
ProxyFactory
方式
1 | // 1.创建代理所需参数配置(如:采用什么方式的代理、通知列表等) |
AspectJProxyFactory
方式
ProxyFactoryBean
方式
- 创建代理,有3个信息比较关键
- 需要增强的功能,这个放在通知(Advice)中实现
- 目标对象(target): 表示需要给哪个对象进行增强
- 代理对象(proxy): 将增强的功能和目标对象组合在一起,然后形成的一个代理对象,通过代理对象来访问目标对象,起到对目标对象增强的效果
- 使用
ProxyFactoryBean
步骤- 创建
ProxyFactoryBean
对象 - 通过
ProxyFactoryBean.setTargetName
设置目标对象的bean名称,目标对象是Spring
容器的一个bean
- 通过
ProxyFactoryBean.setInterceptorNames
添加要增强的通知 - 将
ProxyFactoryBean
注册到Spring容器中,假设名称为proxyBean
- 从
Spring
查找名称为proxyBean
的bean
,这个bean
就是生成好的代理对象
- 创建
1 | import org.aopalliance.intercept.MethodInterceptor; |
1 | AnnotationConfigApplicationContext context = new |
ProxyFactoryBean
中的interceptorNames
-
interceptorNames
用来指定拦截器的bean名称列表,常用的2种方式。- 批量的方式
- 非批量的方式
-
批量的方式,使用方法
1
proxyFactoryBean.setInterceptorNames("需要匹配的Bean的名称*");
-
需要匹配的bean名称后面跟上一个
*
,可以用来批量的匹配,如:interceptor*
,此时Spring
会从容器中找到下面2中类型的所有bean,bean名称以interceptor
开头的将作为增强器1
2org.springframework.aop.Advisor
org.aopalliance.intercept.Interceptor -
这个地方使用的时候需要注意,批量的方式注册的时候,如果增强器的类型不是上面2种类型,比如下面3种类型通知,需要将其包装为
Advisor
才可以,而MethodInterceptor
是Interceptor
类型的,可以不用包装为Advisor
类型的.1
2
3org.springframework.aop.MethodBeforeAdvice
org.springframework.aop.AfterReturningAdvice
org.springframework.aop.ThrowsAdvice
-
-
非批量的方式,使用方法
-
非批量的方式,需要注册多个增强器,需明确的指定多个增强器的bean名称,多个增强器按照参数中指定的顺序执行,如
1
proxyFactoryBean.setInterceptorNames("advice1","advice2");
-
advice1、advice2
对应的bean类型必须为下面列表中指定的类型,类型这块比匹配的方式范围广一些1
2
3
4
5MethodBeforeAdvice(方法前置通知)
AfterReturningAdvice(方法后置通知)
ThrowsAdvice(异常通知)
org.aopalliance.intercept.MethodInterceptor(环绕通知)
org.springframework.aop.Advisor(顾问)
-
AspectJ
使用步骤
- 创建一个类,使用
@Aspect
来标注 @Aspect
标注的类中,通过@Pointcur
定义切入点@Aspect
标注的类中,通过AspectJ
提供的一些通知相关的注解定义通知- 使用
AspectJProxyFactory
结合@Aspect
标注的类,来生成代理对象
@Pointcut
-
作用: 用来标注在方法上来定义切入点
-
定义:
@注解(value="表达式标签(表达式格式)")
1
")
表达式标签(10种)
标签 | 作用 |
---|---|
execution |
用于匹配方法执行的连接点 |
within |
目标对象target的类型是否和within 中指定的类型匹配 |
this |
通过AOP创建的代理对象的类型是否和this 中指定的类型匹配;注意判断的目标是代理对象;this 中使用的表达式必须为类型全限定名,不支持通配符 |
target |
判断目标对象的类型是否和指定的类型匹配;注意判断的是目标对象的类型;表达式必须是类型的全限定名,不支持通配符 |
args |
用于匹配当前执行的方法传入的参数为指定类型的执行方法 |
@within |
用于匹配所有持有指定注解类型的方法 |
@target |
用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解 |
@args |
用于匹配当前执行的方法传入的参数持有指定注解的执行 |
@annotation |
用于匹配当前执行方法持有指定注解的方法 |
bean |
Spring AOP扩展,用于匹配特定名称的Bean对象的执行方法 |
execution
1 | execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) |
- 其中带
?
号的modifiers-pattern?
,declaring-type-pattern?
,throws-pattern?
是可选项 ret-type-pattern,name-pattern, parameters-pattern
是必选项modifier-pattern?
修饰符匹配,如public
表示匹配公有方法ret-type-pattern
返回值匹配,*表示任何返回值,全路径的类名等declaring-type-pattern?
类路径匹配
表达式 | 描述 |
---|---|
public *.*(..) |
任何公共方法 |
* cn.holelin..IService.*() |
cn.holelin 包及所有子包下IService 接口中的任何无参方法 |
* cn.holelin..*.*(..) |
cn.holelin 包及所有子包下任何类的任何方法 |
* cn.holelin..IService.*(..)) && args(arg) |
cn.holelin 包及所有子包下IService 接口中的任何只有一个参数方法 |
* Service.*(String) |
匹配Service 中只有一个参数且参数类型是String 的方法 |
* Service.*(*,String) |
匹配Service 中只有2个参数且个第二参数类型是String 的方法 |
* Service.*(..,String) |
匹配Service 中最后一个参数类型是String 的方法 |
类型匹配语法
通配符 | 描述 |
---|---|
* |
匹配任何数量字符 |
.. |
匹配任何数量字符的重复,如在类型模式中匹配任何数量子包;而在方法参数模式中匹配任何数量参数(0个或者多个参数) |
+ |
匹配指定类型及其子类型;仅能作为后缀放在类型模式后边 |
within
-
匹配原则
1
target.getClass().equals(within表达式中指定的类型)
this
-
匹配原则
1
2// this(x),则代理对象proxy满足下面条件则会匹配
x.getClass().isAssignableFrom(proxy.getClass())
target
-
匹配原则
1
2// target(x),则目标对象target满足下面条件
x.getClass().isAssignableFrom(target.getClass())
within,this,target
对比
表达式标签 | 判断的对象 | 判断规则(x:指表达式中指定的类型) |
---|---|---|
within | target对象 | target.getClass().equals(within表达式中指定的类型) |
this | proxy对象 | x.getClass().isAssignableFrom(proxy.getClass()) |
target | target对象 | x.getClass().isAssignableFrom(target.getClass()) |
args
- args(参数类型列表)匹配当前执行的方法传入的参数是否为args中指定的类型;注意是匹配传入的参数类型,不是匹配方法签名的参数类型;参数类型列表中的参数必须是类型全限定名,不支持通配符;args属于动态切入点,也就是执行方法的时候进行判断的,这种切入点开销非常大,非特殊情况最好不要使用。
@within
-
调用目标方法的时候,通过java中 Method.getDeclaringClass()获取当前的方法是哪个类中定义的,然后会看这个类上是否有指定的注解。
1
被调用的目标方法Method对象.getDeclaringClass().getAnnotation(within中指定的注解类型) != null
@Aspect
中有5种通知
通知 | 描述 |
---|---|
@Before |
前置通知, 在方法执行之前执行 |
@Aroud |
环绕通知, 围绕着方法执行 |
@After |
后置通知, 在方法执行之后执行 |
@AfterReturning |
返回通知, 在方法返回结果之后执行 |
@AfterThrowing |
异常通知, 在方法抛出异常之后 |
通知获取被调方法信息
- 通知中想要获取被调用方法的信息,分2中情况
- 非环绕通知,可以将
org.aspectj.lang.JoinPoint
作为通知方法的第一个参数,通过这个参数获取被调用方法的信息. - 环绕通知,可以将
org.aspectj.lang.ProceedingJoinPoint
作为通知方法的第一个参数,通过这个参数获取被调用方法的信息.
- 非环绕通知,可以将
JoinPoint
1 | package org.aspectj.lang; |
ProceedingJoinPoint
1 | public interface ProceedingJoinPoint extends JoinPoint { |
Signature
-
注意
JoinPoint#getSignature()
这个方法,用来获取连接点的签名信息 -
通常情况,Spring中的Aop都是用来对方法进行拦截,所以通常情况下连接点都是一个具体的方法, Signature有个子接口
1
org.aspectj.lang.reflect.MethodSignature
-
JoinPoint#getSignature()
都可以转换转换为MethodSignature
类型,然后可以通过这个接口提供的一些方法来获取被调用的方法的详细信息。
几种通知对比
通知类型 | 执行时间点 | 可获得返回值 | 目标方法异常时是否会执行 |
---|---|---|---|
@Before |
方法执行之前 | 否 | 是 |
@Around |
环绕方法执行 | 是 | 自己控制 |
@After |
方法执行之后 | 否 | 是 |
@AfterReturning |
方法执行之后 | 是 | 否 |
@AfterThrowing |
方法发生异常后 | 否 | 是 |
@EnableAspectJAutoProxy
通知执行顺序
-
Spring Aop中4种通知(
Advice
)1
2
3
4org.aopalliance.intercept.MethodInterceptor
org.springframework.aop.MethodBeforeAdvice
org.springframework.aop.AfterReturningAdvice
org.springframework.aop.ThrowsAdvice-
所有的通知最终都需要转换为
MethodInterceptor
类型的通知,然后组成一个MethodInterceptor
列表,称之为方法调用链或者拦截器链,上面列表中后面3通过下面的转换器将其包装为MethodInterceptor
类型的通知1
2
3
4
5
6org.springframework.aop.MethodBeforeAdvice ->
org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor
org.springframework.aop.AfterReturningAdvice ->
org.springframework.aop.framework.adapter.AfterReturningAdviceInterceptor
org.springframework.aop.ThrowsAdvice ->
org.springframework.aop.framework.adapter.ThrowsAdviceInterceptor
-
5种通知对应的Advice类
通知类型 | 对应的Advice类 |
---|---|
@AfterThrowing |
org.springframework.aop.aspectj.AspectJAfterThrowingAdvice |
@AfterReturning |
org.springframework.aop.aspectj.AspectJAfterReturningAdvice |
@After |
org.springframework.aop.aspectj.AspectJAfterAdvice |
@Around |
org.springframework.aop.aspectj.AspectJAroundAdvice |
@Before |
org.springframework.aop.aspectj.AspectJMethodBeforeAdvice |