我们知道,使用面向对象编程(OOP)有一些弊端,当需要为多个不具有继承关系的对象引入同一个公共行为时,例如日志、安全检测等,我们只有在每个对象里引用公共行为,这样程序中就产生了大量的重复代码,程序就不便于维护了,所以就有了一个对面向对象编程的补充,即面向切面编程(AOP)。
此外,切面编程落实到软件工程其实是为了更好地模块化,而不仅仅是为了减少重复代码。通过 AOP 等机制,我们可以把横跨多个不同模块的代码抽离出来,让模块本身变得更加内聚,进而业务开发者可以更加专注于业务逻辑本身。从迭代能力上来看,我们可以通过切面的方式进行修改或者新增功能,这种能力不管是在问题诊断还是产品能力扩展中,都非常有用。
Spring 2.0 版本采用 @AspectJ 注解,对 POJO 进行标注,从而定义一个包含切点信息和增强横切逻辑的切面。Spring 可以将这个切面织入到匹配的目标 Bean 中。@AspectJ 注解使用 AspectJ 切点表达式语法进行切点定义,可以通过切点函数、运算符、通配符等高级功能进行切点定义,拥有强大的连接点描述能力。
AOP 相关术语
连接点(Joinpoint)
一个类或一段程序代码拥有一些具有边界性质的特定点,这些代码中的特定点就被称为“连接点”。Spring 仅支持方法的连接点,即仅能在方法调用前、方法调用后、方法抛出异常时这些程序执行点织入增强。
切点(Pointcut)
连接点相当于数据库中的记录,而切点相当于查询条件。在 Spring 中,切点通过 @Pointcut 注解进行描述,它使用类和方法作为连接点的查询条件,可以通过指定具体的类名和方法名来实现,或者也可以使用正则表达式来定义条件。
增强(Advice)
增强是织入目标类连接点上的一段程序代码,同时包含用于定位连接点的方位信息。所以 Spring 提供的增强接口都是带方位名的,如 BefoeAdvice、AfterReturningAdvice、ThrowsAdvice 等。只有结合切点和增强,才能确定特定的连接点并实施增强逻辑。具体的 Spring Advice 结构可参考下面的示意图。
其中,BeforeAdvice 和 AfterAdvice 包括它们的子接口是最简单的实现。而 Interceptor 则是所谓的拦截器,用于拦截住方法(也包括构造器)调用事件,进而采取相应动作,所以 Interceptor 是覆盖住整个方法调用过程的 Advice。通常将拦截器类型的 Advice 叫作 Around,在代码中可以使用 @Around 来标记。
public interface MethodBeforeAdvice extends BeforeAdvice {
/**
* 在目标类方法调用前执行,但如果该方法发生异常,将阻止目标类方法的执行
*
* method:目标类的方法
* args:目标类方法的入参
* target:目标类实例
*/
void before(Method method, Object[] args, @Nullable Object target) throws Throwable;
}
public interface AfterReturningAdvice extends AfterAdvice {
/**
* 在目标类方法调用后执行,假设在后置增强中抛出异常,如果是目标方法生命的异常,则归并到目标方法中
* 如果不是,则Spring将其转为运行期异常抛出
*
* returnValue:目标实例方法返回的结果
* method:目标类的方法
* args:目标实例方法的入参
* target:目标类实例
*/
void afterReturning(@Nullable Object returnValue, Method method, Object[] args, @Nullable Object target) throws Throwable;
}
public interface MethodInterceptor extends Interceptor {
/**
* 截取目标类方法的执行,并可在前后添加横切逻辑
* MethodInvocation不但封装了目标方法及其入参数组,还封装了目标方法所在的实例对象,通过getArguments()方法可以获取目标方法的入参数组,通过proceed()方法反射调用目标实例相应的方法。
*/
void invoke(MethodInvocation invocation) throws Throwable;
}
代理(Proxy)
一个类被 AOP 织入增强后,就产生了一个结果类,它是融合了原类和增强逻辑的代理类。根据不同代理方式,代理类既可能是和原类具有相同接口的类(JDK 动态代理),也可能是原类的子类(Cglib),所以可以采用与调用原类相同的方式调用代理类。
切面(Aspect)
切面由切点和增强组成,它即包括横切逻辑的定义,也包括连接点的定义。在实现形式上,既可以是 XML 文件中配置的普通类,也可以在类代码中用 @Aspect 注解去声明。在运行时,Spring AOP 框架会创建类似 Advisor 来指代它,其内部会包括切入的时机(Pointcut)和切入的动作(Advice)。
AspectJ 基础使用
1. 切点表达式函数
AspectJ 的切点表达式由关键字和操作参数组成,如切点表达式 execution( speak(..)),为了描述方便,我们通常将 execution() 称作函数,而将 speak(..) 称为函数的入参。Spring 中常用的切点函数如下:
类别 | 函数 | 入参 | 说明 |
---|---|---|---|
方法切点函数 | execution() | 方法匹配模式串 | 表示满足某一匹配模式的所有目标类方法连接点。如 execution( speak(..))* 表示所有目标类中的 speak() 方法 |
@annotation() | 方法注解类名 | 表示标注了特定注解的目标类方法连接点。如 @annotation(com.example.NeedTest) 表示任何标注了 @NeedTest 注解的目标类方法 | |
方法入参切点函数 | args() | 类名 | 通过判别目标类方法运行时入参对象的类型定义指定连接点。如 args(com.example.NeedTest) 表示所有有且仅有一个按类型匹配于 NeedTest 入参的方法 |
@args() | 类型注解类名 | 通过判别目标类方法运行时入参对象的类是否标注特定注解来指定连接点。如 @args(com.example.NeedTest) 表示这样的一个目标方法:它有一个入参且入参对象的类标注 @NeedTest 注解 | |
目标类切点函数 | within() | 类名匹配串 | 表示特定域下的所有连接点。如 within(com.example.*Test) 表示 com.example 包中的所有以 Test 结尾的类的所有方法 |
target() | 类名 | 表示特定类型下的所有连接点。如通过 target(com.example.NeedTest) 定义的切点,NeedTest 及 NeedTest 实现类的所有连接点都匹配这个切点 | |
@within() | 类型注解类名 | 假如目标类按类型匹配于某个类 A,且类 A 标注了特定注解,则目标类的所有连接点都匹配这个切点。如 @within(com.example.NeedTest) 定义的切点,假如类 A 标注了 @NeedTest 注解,则 A 及 A 实现类的所有连接点都匹配这个切点 |
within() 函数定义的连接点是针对目标类而言的,但和 execution() 函数不同的是,within() 所指定的连接点最小范围只能是类,而 execution() 所指定的连接点可以大到包,小到方法入参。
其中,execution() 函数的具体入参如下:
execution(<修饰符模式>? <返回类型模式> <方法名模式>(<参数模式>) <异常模式>?)
使用示例如下:
- execution(public (..)):匹配所有目标类的 public 方法,第一个 代表返回类型,第二个 代表方法名,而 .. 代表任意入参的方法。
execution( To(..)):匹配目标类所有以 To 为后缀的方法。
execution( com.example.NeedTest.(..)):匹配 NeedTest 接口的所有方法。
execution( com.example.NeedTest+.(..)):匹配 NeedTest 接口及其实现类的方法,它不但匹配接口定义的方法,同时还匹配实现类中定义的不在接口中的方法。
execution( com.example..*(..)):匹配 com.example 包下所有类的所有方法。
execution( com.example..(..)):匹配 com.example 包及其子包下所有类的所有方法。
execution( joke(String,)):匹配目标类中的 joke() 方法,该方法有两个入参,第一个入参为 String,第二个入参可以是任意类型。
- execution(* joke(String,)):匹配目标类中的 joke() 方法,该方法第一个入参为 String,后面可以有任意个入参且入参类型不限。**
- execution(* joke(Object+)):匹配目标类中的 joke() 方法,该方法拥有一个入参,且入参是 Object 类型或该类型的子类。
可以看到,AspectJ 语法支持 3 种通配符,具体含义如下:
- *:匹配任意字符,但它只能匹配上下文中的一个元素。
- ..:匹配任意字符,可以匹配上下文中的多个元素。
- +:表示按 Class 类型匹配指定类的所有子类,必须跟在类名后面。
此外,切点表达式由切点函数组成,不同的切点函数之前还可以进行逻辑运算,组成复合切点。Spring 支持以下三种切点运算符:
- &&:与操作符,相当于切点的交集运算。
- ||:或操作符,相当于切点的并集运算。
- !:非操作符,相当于切点的反集运算。
2. 增强注解类
2.1 @Before
前置增强,相当于 BeforeAdvice 接口。@Before 注解有两个成员:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Before {
// 该成员用于定义切点
String value();
// 由于无法通过 Java 反射机制获取方法入参名,所以如果需要在运行期解析切点,就必须通过这个成员指定注解所标注增强方法的参数名
String argNames() default "";
}
2.2 @Around
环绕增强,相当于 MethodInterceptor 接口。@Around 注解有两个成员:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Around {
// 该成员用于定义切点
String value();
// 同上
String argNames() default "";
}
2.3 @AfterReturning
后置增强,相当于 AfterReturningAdvice 接口。@AfterReturning 注解有四个成员:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AfterReturning {
// 该成员用于定义切点
String value() default "";
// 切点表达式。如果显示指定 pointcut 值,那么它将覆盖 value 的设置值
String pointcut() default "";
// 将目标对象方法的返回值绑定给增强的方法
String returning() default "";
// 同上
String argNames() default "";
}
使用示例如下:
@Aspect
@Component
public class AspectInfo {
// returning指定的返回值名称必须和入参的返回值名称相同,这个后置增强只有在连接点返回
// 该方法指定的类型时才匹配,增强方法通过retVal可以访问方法的返回值信息
@AfterReturning(value = "execution(* com.example.*.*(..))", returning = "retVal")
public void print(int retVal) {
System.out.println("method return value :" + retVal);
}
}
2.4 @AfterThrowing
抛出异常时增强,相当于 ThrowsAdvice 接口。@AfterThrowing 注解有四个成员:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AfterThrowing {
// 该成员用于定义切点
String value() default "";
// 切点表达式。如果显示指定 pointcut 值,那么它将覆盖 value 的设置值
String pointcut() default "";
// 将抛出的异常绑定到增强方法中
String throwing() default "";
// 同上
String argNames() default "";
}
使用示例如下:
@Aspect
@Component
public class AspectInfo {
// throwing指定的异常名必须和入参的异常名相同,这个异常增强只有在连接点抛出异常
// instanceof IllegalArgumentException时才匹配,增强方法通过iae可以访问抛出的异常对象
@AfterThrowing(value = "execution(* com.example.*.*(..))", throwing = "iae")
public void print(IllegalArgumentException iae) {
System.out.println("exception :" + illegalArgument.getMessage());
}
}
2.5 @After
Final 增强,不管是抛出异常还是正常退出,该增强都会得到执行。可以把它看成 ThrowsAdvice 和 AfterReturningAdvice 的混合物,一般用于释放资源,相当于 try-finally 的控制流。@After 注解有两个成员:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface After {
// 该成员用于定义切点
String value();
// 同上
String argNames() default "";
}
3. 切点命名
为了在切面中重用切点,我们还可以对切点进行命名,以便在其他地方直接引用已经定义过的切点。切点直接声明在增强方法处的这种方式称为匿名切点,匿名切点只能在声明处使用。如果希望在其他地方重用一个切点,则可以通过 @Pointcut 注解及切面类方法对切点进行命名,方法名称即为切点名称,引用时通过 类名.方法名 即可,但如果有重复类名,则启动会报错。
定义切点:
@Component
public class AspectPointcut {
@Pointcut("execution(* com.example.mybatis.aop.*.*(..))")
private void inPackage() {}
@Pointcut("execution(* speak(..))")
protected void speak() {}
@Pointcut("inPackage() && speak()")
public void inPackageSpeak() {}
}
在其他类使用该切点:
@Aspect
@Component
public class AspectInfo {
@Around("AspectPointcut.inPackageSpeak()")
public Object print(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("before speak");
Object result = joinPoint.proceed();
System.out.println("after speak");
return result;
}
}
4. 增强织入顺序
当一个连接点匹配多个切点时,这时需要考虑增强织入顺序的问题了。我们知道,切面本身是一个 Bean,Spring 对不同切面增强的执行顺序是由 Bean 的优先级决定的,具体规则是:
- 入操作(Around(连接点执行前)、Before),切面优先级越高,越先执行。一个切面的入操作执行完,才轮到下一切面,所有切面入操作执行完,才开始执行连接点(方法)。
- 出操作(Around(连接点执行后)、After、AfterReturning、AfterThrowing),切面优先级越低,越先执行。一个切面的出操作执行完,才轮到下一切面,直到返回到调用点。
- 同一切面的 Around 比 After、Before 先执行。
- 如果增强位于不同的切面类中,且这些切面类都实现了 org.springframework.core.Ordered 接口,则由接口方法的顺序决定。默认情况下 Bean 的优先级为最低优先级,其值是 Integer 的最大值。注意,值越大优先级反而越低
5. 访问连接点信息
如何在增强中访问连接点上下文的信息呢?AspectJ 使用 JoinPoint 接口表示目标类连接点对象。如果是环绕增强,则使用 ProceedingJoinPoint 表示连接点对象,该类是 JoinPoint 的子接口。任何增强方法都可以通过将第一个入参声明为 JoinPoint 类型来访问连接点的上下文信息。
JoinPoint 接口方法
方法返回值 | 方法名 | 说明 |
---|---|---|
java.lang.Object[] | getArgs() | 获取连接点方法运行时的入参列表 |
Signature | getSignature() | 获取连接点的方法签名对象 |
java.lang.Object | getTarget() | 获取连接点所在的目标对象 |
java.lang.Object | getThis() | 获取代理对象本身 |
ProceedingJoinPoint 接口方法
方法返回值 | 方法名 | 说明 |
---|---|---|
java.lang.Object | proceed() | 通过反射执行目标对象的连接点处的方法 |
java.lang.Object | proceed(Object[] args) | 通过反射执行目标对象连接点处的方法,但使用新的入参替换原来的入参 |
使用示例
在 Spring Boot 项目中引入如下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
AOP 切点和增强逻辑具体如下:
/**
* 使用@Aspect注解将一个类标识为一个切面
* 通过@Pointcut注解定义一个切点表达式
* 通过@Before、@Around、@After等注解绑定切点,实现增强逻辑
*/
@Aspect
@Component
public class AopConfig {
// 该切点为【org.xl.springboot.aop】包下的所有类及其子包下的所有类的【say】方法
@Pointcut("execution(* org.xl.springboot.aop..*(..)) && execution(* say(..))")
private void packagePoint() {}
// 该切点为【AopAnnotation】注解修饰的地方
@Pointcut("@annotation(org.xl.springboot.aop.AopAnnotation)")
private void annotationPoint() {}
@Resource
private AopTest aopTest;
// 环绕增强,增强的切点为以上两个
@Around("packagePoint() || annotationPoint()")
public Object innerRiskExecute(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("Before to invoke Test");
Object result = joinPoint.proceed();
System.out.println("After to invoke Test");
return result;
}
}