0%

Spring AOP

Spring AOP

(Aspect-Oriented Programming)切面式编程如何理解切面呢,最开始听到这个词我也觉得非常抽象,后来在刷blibli的时候总能看到一些精彩片段,这些片段就是从完整的直播或者比赛视频中剪切的,他们把这种视频叫做切片,我突然就悟了!

给这些精彩片段就是我们需要额外处理的代码,视频片段的前面和后面就是我们说的“切面”

Spring中的切面编程,通常就是对方法的增强,在方法调用前后进行额外的操作。Spring AOP 只支持 Spring Bean 的方法切入,所以切点表达式只会匹配 Bean 类中的方法。 Spring AOP实现了 JDK动态代理,以使用目标类和Advice调用创建代理类,这些称为AOP代理类。

快速入门

我们先在SpringBoot中切一个方法吧,这里使用注解的方式切入

  • 在pom.xml中导入AOP依赖
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
  • 创建注解类
1
2
3
4
5
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {

}
  • 创建切面类,在切面类中写一个方法,加上@Around注解,在注解中写上新建的注解类路径
1
2
3
4
5
6
7
@Aspect
@Component
public class TestAspect {
@Around("@annotation(com.fsframe.annotaion.TestAnnotation)")
public Object aroundAnnotation(ProceedingJoinPoint proceedingJoinPoint) throws Exception {

}
  • 在切点初编写额外操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Aspect
@Component
public class TestAspect {
@Around("@annotation(com.fsframe.annotaion.TestAnnotation)")
public Object aroundAnnotation(ProceedingJoinPoint proceedingJoinPoint) throws Exception {
System.out.println("方法开始");
Object proceed = null;
try {
proceed = proceedingJoinPoint.proceed();
System.out.println("invoke :"+proceed.toString());
} catch (Throwable t) {
t.printStackTrace();
}
System.out.println("方法结束");
return proceed;
}
}
  • 在方法上添加@TestAnnotation注解
1
2
3
4
5
@TestAnnotation
public String test() {
System.out.println("hello world");
return "test";
}
  • 调用该方法,查看结果
1
2
3
4
方法开始
hello world
invoke :test
方法结束

切面组件Aspect

spring中设置切面通常有两种方式

  1. xml配置文件中声明bean,再在aop:config的aop:aspect中指定为切面
  2. 给类加@Component@Aspect 注解,快速入门就是使用这种方法设置

AOP 增强的类型

AOP 增强的方式有五种类型,分别是方法调用前、方法调用后、方法调用前后、抛出异常时、执行结束时,快速入门中使用的是Around ,在方法执行前后都进行了打印

  • Before Advice:在连接点之前执行的方法增强 @Before
  • After Advice:在连接点之后执行的方法增强 @AfterReturning
  • Around Advice:在连接点前后执行的增强,Around环绕着连接点 @Around
  • After Throwing:在连接点抛出异常时执行的方法增强 @AfterThrowing
  • After Returning:当方法执行成功时的增强 @After

切点表达式配置

回忆一下我们对方法进行增强的主要步骤

  1. 找到需要增强的地方(这个地方也叫切点)
  2. 编写具体增强的内容

在快速入门中,我们用这一个方法就完成了这两步

1
2
@Around("@annotation(com.fsframe.annotaion.TestAnnotation)")
public Object aroundAnnotation()

还有一种方式是通过两个方法:先定义切点、再定义增强内容,这样切点和增强方法就会解耦,可以提升代码的可维护性和可重复使用性,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Pointcut("@annotation(com.fsframe.annotaion.TestAnnotation)")
public void myPointcut() throws Exception {
}

@Around("myPointcut()") //注解内的值就是切点方法的名称
public Object aroundAnnotation2(ProceedingJoinPoint proceedingJoinPoint) throws Exception {
System.out.println("方法开始");
Object proceed = null;
try {
proceed = proceedingJoinPoint.proceed();
System.out.println("invoke :" + proceed.toString());
} catch (Throwable t) {
t.printStackTrace();
}
System.out.println("方法结束");
return proceed;
}

再次调用,可以看到输出结果是一样的

1
2
3
4
方法开始
hello world
invoke :test
方法结束

切点表达式

现在来介绍一下切点表达式,这个表达式的作用就是找到具体要切入的点,通常有这几种切入方式

  1. 使用 execution 表达式
    使用 execution 表达式是定义切点的最常见方式之一。通过 execution 表达式可以匹配方法的执行。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 匹配public方法
    execution(public * *(..))

    // 匹配名称以set开头的方法
    execution(* set*(..))

    // 匹配AccountService接口或类的方法
    execution(* com.xyz.service.AccountService.*(..))

    // 匹配service包及其子包的类或接口
    execution(* com.xyz.service..*(..))

    上述代码定义了一个切点 serviceMethods(),它匹配 com.example.service 包中所有类的所有方法。

  2. 使用 within 表达式
    使用 within 表达式可以定义基于类型的切点,匹配指定包或类型的所有方法。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    // 匹配service包的类
    within(com.xyz.service.*)

    // 匹配service包及其子包的类
    within(com.xyz.service..*)

    // 匹配AccountServiceImpl类
    within(com.xyz.service.AccountServiceImpl)

    上述代码定义了一个切点 inServicePackage(),它匹配 com.example.service 包中所有类的所有方法。

  3. 使用 thistarget 关键字
    使用 thistarget 关键字可以定义基于目标对象类型的切点,匹配特定类型或实现了特定接口的目标对象。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 匹配代理对象类型为service包下的类
    this(com.xyz.service.*)

    // 匹配代理对象类型为service包及其子包下的类
    this(com.xyz.service..*)

    // 匹配代理对象类型为AccountServiceImpl的类
    this(com.xyz.service.AccountServiceImpl)

    // 匹配目标对象类型为service包下的类
    target(com.xyz.service.*)

    // 匹配目标对象类型为service包及其子包下的类
    target(com.xyz.service..*)

    // 匹配目标对象类型为AccountServiceImpl的类
    target(com.xyz.service.AccountServiceImpl)

    上述代码分别定义了基于当前代理对象和目标对象类型的切点。

  4. 使用自定义注解
    可以定义自定义注解,并在切点表达式中使用该注解来标注需要被切入的方法。例如:

    1
    @Pointcut("@annotation(com.fsframe.annotaion.TestAnnotation)")

    快速入门中就是使用自定义注解的方式定义切点。

  5. 使用参数

    @args

    1
    2
    3
    4
    5
    // 匹配参数只有一个且参数类使用了Demo注解
    @args(cn.codeartist.spring.aop.pointcut.Demo)

    // 匹配参数个数至少有一个且为第一个参数类使用了Demo注解
    @args(cn.codeartist.spring.aop.pointcut.Demo,..)
  6. 组合多个切点
    可以通过逻辑运算符(如 &&||!)将多个切点组合起来,形成复合切点。例如:

    1
    @Pointcut("execution(* com.example.service.*.*(..)) && !execution(* com.example.service.internal.*.*(..))")

    上述代码定义了一个切点 publicServiceMethods(),它匹配 com.example.service 包中所有公共方法,但排除 com.example.service.internal 包中的方法。

Execution表达式说明

表达式模式:

1
execution(modifier? ret-type declaring-type?name-pattern(param-pattern) throws-pattern?)
  • modifier:匹配修饰符,publicprivate等,省略时匹配任意修饰符
  • ret-type:匹配返回类型,使用 * 匹配任意类型
  • declaring-type:匹配目标类,省略时匹配任意类型
    • .. 匹配包及其子包的所有类
  • name-pattern:匹配方法名称
    • * 匹配任意方法
    • set* 匹配以set开头的方法
  • param-pattern:匹配参数类型和数量
    • () 匹配没有参数的方法
    • (..) 匹配有任意数量参数的方法
    • (*) 匹配有一个任意类型参数的方法
    • (*,String) 匹配有两个参数的方法,并且第一个为任意类型,第二个为String类型的
  • throws-pattern:匹配抛出异常类型,省略时匹配任意类型

AOP术语

  • Aspect: 方面是一个模块,其中封装了advicepointcuts,并提供cross-cutting可以有许多方面。我们可以使用带有 @Aspect 批注的常规类来实现方面。
  • Pointcut: 切入点是一种表达式,它选择一个或多个执行Advice的连接点。我们可以使用expressionspatterns定义切入点。它使用与联接点匹配的不同类型的表达式。在Spring Framework中,使用 AspectJ 切入点表达语言。
  • Join point: 连接点是应用程序中应用 AOP方面的点。或者它是Advice的特定执行实例。在AOP中,连接点可以是方法执行,异常处理,更改对象变量值等。
  • Advice: Advice是我们在方法执行之前beforeafter采取的措施。该动作是在程序执行期间调用的一段代码。SpringAOP框架中有五种类型的Advice: 在Advicebefore, after, after-returning, after-throwing和around advice。 是针对特定join point的Advice。
  • Target object: 一个应用了Advice的对象称为target object。目标对象始终是proxied,这意味着在运行时将创建一个覆盖目标方法的子类,并根据其配置包含Advice。
  • Weaving: 这是将各个方面与其他应用程序类型进行linking aspects的过程。我们可以在运行时,加载时间编译时进行织造。