1. AOP 切面

1.1. 切面的定义

Spring AOP(Aspect-Oriented Programming,面向切面编程)是Spring框架提供的一种对OOP(Object-Oriented Programming,面向对象编程)的补充,是一种通过预编译方式和运行期动态代理实现程序功能的技术。它可以让我们将横切关注点(如日志记录、性能统计等)从纵向代码中解耦出来,以提高代码的模块化、可重用性和可维护性。

在Spring AOP中,切面(Aspect)是一个模块化的、跨越多个类的关注点的定义。比如一个日志切面可以定义日志记录的行为,在应用程序的各个模块中进行调用。切面由切点(Pointcut)和增强(Advice)组成。

  1. 切点
    切点是一个表达式,用于匹配需要织入增强的目标方法。常用的表达式语言是AspectJ表达式,它可以匹配方法的访问修饰符、返回值类型、方法名等。

  2. 增强
    定义了切面在切点匹配时所执行的具体行为,有以下几种类型:

    • 前置增强(Before Advice):在目标方法执行之前执行。
    • 后置增强(After Advice):在目标方法执行之后执行,无论是否产生异常。
    • 返回增强(After Returning Advice):在目标方法正常返回之后执行。
    • 异常增强(After Throwing Advice):在目标方法抛出异常时执行。
    • 环绕增强(Around Advice):在目标方法执行前后执行。
  3. 织入(Weaving)是将切面应用到目标对象并创建代理对象的过程。Spring提供了三种织入方式:

    • 编译时织入(Compile-time Weaving):在编译阶段,通过特定的编译器在编译期织入切面代码。
    • 类加载时织入(Load-time Weaving):在类加载阶段,通过特定的类加载器在加载类时织入切面代码。
    • 运行时织入(Runtime Weaving):在应用程序运行时,通过动态代理技术在运行期织入切面代码。

    Spring AOP支持多种织入方式,其中最常用的是运行时织入。它通过使用动态代理技术,在目标对象和切面之间创建一个代理对象,对目标方法进行增强。

需要注意的是,Spring AOP仅支持对Spring管理的Bean进行切面增强,对于不属于Spring容器管理的Bean,增强是无效的。

1.2. 切面简单实现

首先需要引入aspect包

1
2
3
4
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>

然后创建切面类,并加入容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component
@Aspect
public class MyAspect {

/**
* execution(返回值类型 包.类.方法(参数)
* * 代表所有,用于返回值代表所有返回类型,用于路径中间代表任意层级路径,用于方法代表所有方法
* 括号中表示参数,用..表示任意多个参数
*/
@Pointcut("execution(* com.kewen.learning.spring.tool.aspect.AspectService.*(..))")
public void aspect(){}

@Around("aspect()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("around 前置");
Object proceed = joinPoint.proceed();
System.out.println("around 后置");
return proceed;
}
}

1.3. 切面详解

1.3.1. 增强注解

包括前置增强(@Before)、返回增强(@AfterReturning)、异常增强(@AfterThrowing)、后置增强(@After)和环绕增强(@Around)。

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
@Aspect
@Component
public class LogAspect {

@Pointcut("execution(public * com.example.demo.service.*.*(..))")
public void pointcut() {
}
@Before("pointcut()")
public void before(JoinPoint joinPoint) {
System.out.println("Before Advice: " + joinPoint.getSignature().getName());
}
@AfterReturning(pointcut = "pointcut()", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
System.out.println("After Returning Advice: " + joinPoint.getSignature().getName() + ", result: " + result);
}
@AfterThrowing(pointcut = "pointcut()", throwing = "exception")
public void afterThrowing(JoinPoint joinPoint, Throwable exception) {
System.out.println("After Throwing Advice: " + joinPoint.getSignature().getName() + ", exception: " + exception.getMessage());
}
@After("pointcut()")
public void after(JoinPoint joinPoint) {
System.out.println("After Advice: " + joinPoint.getSignature().getName());
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("Around Before: " + joinPoint.getSignature().getName());
Object result = joinPoint.proceed();
System.out.println("Around After: " + joinPoint.getSignature().getName() + ", result: " + result);
return result;
}
}

1.3.2. 切面表达式解析

切面表达式有两种通配符,一种是.位于路径中或方法中,一种是*用以表示任意类型

  • com.example.HelloService.hello(..) 用以匹配hello方法,不区分重载(..匹配任意参数)
  • com.example.*.hello(..)用以匹配example包下所有类的hello方法(*匹配任意单个类或单个包名)
  • com.example..*.hello(..)用以匹配example及子包下所有类的hello方法(..匹配多层路径)
  • com.example..*(..)用以匹配example及子包下所有类的所有方法(..匹配多层路径和任意参数,*匹配单个方法名)
  • com.example..(..) 此种是错误的,..不能匹配方法名

1.3.2.1. execution表达式

execution 是用来匹配方法执行的切点表达式,它是最主要的切点匹配器。例如:

  • execution(* set*(..)):这个表达式将会匹配所有以 “set” 开头的方法。”“ 表示任何返回类型,”set“ 表示所有以 “set” 开头的方法,”..” 表示任何参数。
  • execution(* com.example.ClassName.methodName(..)):这个表达式将会匹配 com.example.ClassName 类的 methodName 方法,无论这个方法接受什么参数。
  • execution(* com.example.*.*(..)):这个表达式将会匹配 com.example 包下的所有类的所有方法。
  • execution(* *(..)):这个表达式将会匹配所有的方法,无论是哪个类的方法。
1
2
3
4
5
6
@Component
@Aspect
public class MyAspect {
@Pointcut("execution(* com.kewen.learning.spring.tool.aspect.AspectService.*(..))")
public void aspect(){}
}

1.3.2.2. within表达式

within 是用来匹配特定的路径,用法与execution类似,只是within不关注返回值和方法,只关注到包或类。

  • within(com.example.ClassName):这个表达式将会匹配 com.example.ClassName 类内的所有方法。
  • within(com.example.*):这个表达式将会匹配 com.example 包内的所有方法。
  • within(com.example..*):这个表达式将会匹配 com.example 包及其子包内的所有方法。

1.3.2.3. thistarget表达式

thistarget 主要用来在切面中引入被代理的对象,以便拿到其中的属性或方法进一步处理。
使用 this 匹配的是当前实例化的对象(即代理之后了的对象),而 target 匹配的是被代理的目标对象(原对象)。

例:

1
2
3
4
5
6
7
8
9
10
11
12
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
@Component
public class UserLoggingAspect {
@Before("execution(* com.kewen.learning.spring.tool.aspect.AspectService.*(..)) && this(thisAspectService) &&target(targetAspectService)")
public void before1(JoinPoint joinPoint,AspectService thisAspectService,AspectService targetAspectService){
System.out.println(thisAspectService.getClass().getName()); //代理之后的对象
System.out.println(thisAspectService.getClass().getName()); //为被代理的对象
}
}

如果没有this(thisAspectService),则thisAspectService对象无法引入,会报错

1.3.2.4. args表达式

args 是用来匹配方法参数为指定类型的执行方法的。

  1. 通配符(*):可以使用通配符作为参数类型的占位符,例如args(*)将匹配任意参数类型的方法。
  2. 单个参数类型:可以指定单个参数类型,例如args(java.lang.String)将匹配具有String类型参数的方法。
  3. 多个参数类型:可以指定多个参数类型,使用逗号分隔,例如args(java.lang.String, java.lang.Integer)将匹配具有String类型和Integer类型参数的方法。
  4. 包含子类型:可以使用”+”前缀来指示匹配参数类型及其子类型,例如args(+java.lang.Number)将匹配具有Number类型及其子类型参数的方法。
  5. 排除特定类型:可以使用”!”前缀来排除特定类型,例如args(!java.lang.Boolean)将排除具有Boolean类型参数的方法。

请注意,args表达式只匹配方法调用的参数类型,不考虑方法的返回类型、方法的目标对象等因素。因此,它通常与其他切点表达式组合使用,以更准确地选择切面建议的目标方法。

例:

1
2
3
4
5
6
7
8
@Aspect
@Component
public class MyAspect {
@Before("execution(* com.example.service.MyService.*(..)) && args(java.lang.String)")
public void beforeAdvice(JoinPoint joinPoint) {
//表示匹配MyService类中参数为String类型的任意方法
}
}

1.3.2.5. @annotation表达式

@annotation 用来匹配方法上的注解。

1
2
3
4
5
6
7
@Aspect
public class MyAspect {
// 带有Transactional注解的方法将执行切面
@Before("@annotation(org.springframework.transaction.annotation.Transactional)")
public void beforeAdvice(JoinPoint joinPoint) {
}
}

值得注意的是,@annotation只匹配直接在方法上声明的注解,而不包括继承自父类或接口的注解。如果想要匹配继承的注解,可以使用within表达式结合@annotation来定义更准确的切点。

1
2
3
4
5
6
7
8
@Aspect
public class MyAspect {
@Before("@within(com.example.annotation.MyAnnotation) && @annotation(org.springframework.transaction.annotation.Transactional)")
public void beforeAdvice(JoinPoint joinPoint) {
// 在带有MyAnnotation注解且带有@Transactional注解的类的方法之前执行
// 可以在此处执行任何与这些方法相关的切面逻辑
}
}

1.3.2.6. @within表达式

@within 用来匹配类上的注解,它支持匹配类继承上的注解

1
2
3
4
5
6
7
8
9
@Aspect
public class MyAspect {

@Before("@within(org.springframework.stereotype.Controller)")
public void beforeAdvice(JoinPoint joinPoint) {
// 在带有@Controller注解的类的方法之前执行
// 可以在此处执行与控制器层相关的切面逻辑
}
}

1.3.2.7. @target

@target 是用来匹配在目标对象上声明的注解。
@within的区别是 @within 可以执行继承的,而@target只能支持对象本身的,不支持继承的

1
2
3
4
5
6
7
8
9
@Aspect
public class MyAspect {

@Before("@target(org.springframework.stereotype.Service)")
public void beforeAdvice(JoinPoint joinPoint) {
// 在带有 @Service 注解的目标对象的方法之前执行
// 可以在此处执行与服务层相关的切面逻辑
}
}

1.3.2.8. @args

@args 是用来匹配传入的参数带有指定注解的方法的。

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
@MyAnnotation
public class MyClass {
// 类级别 MyAnnotation 注解
public void method1() {
}

public void method2() {
}
}

@Aspect
public class MyAspect {

@Before("@target(com.example.annotation.MyAnnotation)")
public void beforeTargetAdvice(JoinPoint joinPoint) {
// 在目标类上带有特定注解的方法之前执行
// 只匹配目标类本身的注解
}

@Before("@within(com.example.annotation.MyAnnotation)")
public void beforeWithinAdvice(JoinPoint joinPoint) {
// 在带有特定注解的类中的方法之前执行
// 包括继承和实现了该注解的类
}
}