Spring Boot AOP详解


Spring AOP(Aspect-Oriented Programming,面向切面编程)是 Spring 框架中的一个重要特性,用于实现横切关注点的模块化和重用。它通过将横切关注点(如日志记录、性能监控、事务管理等)从业务逻辑中分离出来,以便将它们应用到多个模块中的相同位置,实现代码的重用和解耦。

一、概念介绍

Spring AOP 中涉及四个的核心概念:切面、连接点、通知和切点。

1. 切面

切面 (Aspect) 是一个跨越多个对象的模块化单元,它定义了横切关注点和它们的行为,一个切面可以包含多个通知和切点。

2. 连接点

连接点 (Join Point) 是在应用程序执行过程中能够插入切面的点,通常连接点是方法的执行点,但也可以是异常抛出时或字段访问时等。

3. 通知

通知 (Advice) 是切面在连接点处执行的代码,可以在连接点之前、之后或周围执行,以实现不同的行为。

常见的通知类型包括前置通知(在连接点之前执行)、后置通知(在连接点之后执行)、异常通知(在连接点抛出异常时执行)和环绕通知(在连接点前后都执行)。

4. 切点

切点 (Pointcut) 定义了在哪些连接点上应用通知,通过指定切点表达式或使用注解来确定切点的位置。

二、切点定义

Spring AOP 可以通过 XML 配置、注解或基于 Java 的配置类来定义切面和通知。在运行时通过动态代理技术将通知织入到目标对象的方法中,实现切面的功能。

通过使用 Spring AOP 可以实现横切关注点的集中管理和复用,提高代码的可维护性和灵活性,帮助开发人员解决各种横切关注点的需求,如日志记录、性能监控、事务管理、安全性检查等。

1. 模式定义

Spring 中通过 @Pointcut 定义切点,并在类上使用 @Aspect 注解开启切面,其基本格式如下:

@Aspect
@Component
public class AspectConfig {
    
    @Pointcut("<Action> (<Pattern> <Return> <Package>(<Params>))")
    public void pointcut() {

    }
}

上述各参数对应描述信息参考下表:

参数 说明
Action 表示作用类型,最常用的为 execution ,后续将详细介绍。
Pattern 指定作用对象的修饰符,如方法的 public、private 等,为 * 表示所有,缺省时为 *。
Return 指定作用对象的返回类型,为 * 表示所有。
Package 指定作用的对象,通常为方法类所在包,缺省时为 *。
Params 指定方法的行参类型,如 String 等,设置为 .. 表示允许零个或多个参数。

2. 方法切点

execution 方式作用域细化到方法,也是最常用的方式。

如下切点定义示例作用域为 xyz.ibudai.controller 包中所有类的方法。

@Pointcut("execution (public * xyz.ibudai.controller.*.*(..))")
public void pointcut() {
}

例如在包 xyz.ibudai.controller 路径下存在两个接口类 Resource1Resource2,每个类中各有 3 个接口,那上述切点将同时监控这 6 个方法。

3. 类切点

within 作用域细化到类,即只能监听类中所有方法,无法细化到指定方法。

如下示例定义了监控 Resource1 类中所有方法。

@Pointcut("within(xyz.ibudai.controller.Resource1)")
public void pointcut() {
}

4. 参数切点

args() 用于定义参数切面,其参数说明如下:

  • args(): 不带参数。
  • args(..): 任意参数。
  • args(<Class>): 指定参数类型。
// 监听无参方法
@Pointcut("args()")
public void pointcut1() {
}

// 监听任意方法
@Pointcut("args(..)")
public void pointcut2() {
}

// 监听指定入参形式方法
@Pointcut("args(java.lang.String)")
public void pointcut3() {
}

5. 注解切点

通过 @annotation 定义注解切面,当方法上标识了对应注解则将被切面监控。

例如定义了注解 @TestAnnotation,通过定义注解切点即可监控项目中所有标识了该注解的方法。

而在具体的声明中存在两种方式,让我们先看第一种声明方式:

public class AspectConfig {

    @Pointcut("@annotation(xyz.ibudai.anntation.TestAnnotation)")
    public void pointcut1() {
    }

    @Around("pointcut1()")
    public Object around(ProceedingJoinPoint joinPoint) {
        try {
            return joinPoint.proceed();
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }
}

在上面的声明方式中,我们只能获取目标方法信息,而无法获取标识的注解信息。

让我们来看下另一种定义方法,其中第一个 @annotation 配置的参数即环切方法的注解入参,通过该声明方式不仅能监控目标方法,同时还能获取方法注解信息,因此该方式也更为常用。

public class AspectConfig {

    @Pointcut("@annotation(testAnnotation) && @annotation(xyz.ibudai.aop.anntation.TestAnnotation)")
    public void pointcut2(TestAnnotation testAnnotation) {
    }

    @Around(value = "pointcut2(testAnnotation)", argNames = "joinPoint, testAnnotation")
    public Object around(ProceedingJoinPoint joinPoint, TestAnnotation testAnnotation) {
        try {
            return joinPoint.proceed();
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }
}

三、通知事件

通知是切面在连接点处执行的代码,可以在连接点之前或之后执行,以实现不同的操作。

1. @Before

通过 @Before 注解作用于方法上定义前置通知,逻辑将在连接点之前执行。

@Before("pointcut()")
public void before(JoinPoint joinPoint) {
    System.out.println("Before advice: " + joinPoint);
}

2. @Around

@AroundAOP 中的一大核心,同时也是绝大数场景中业务的切入点,环绕通知在连接点前后都执行。

@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) {

    try {
        // 放行方法
        return joinPoint.proceed();
    } catch (Throwable e) {
        throw new RuntimeException(e);
    }
}

在环切方法中有一个十分重要的对象即 ProceedingJoinPoint,通过其即可获取方法的信息以及控制方法的执行,常用方法以及描述参考下表。

方法 描述
getKind() 返回 JoinPoint.Kind 枚举值,表示了连接点的类型。
getTarget() 获取目标对象,即被增强的对象。
getSignature() 获取目标方法的签名信息,如方法名、参数类型等。
getArgs() 获取目标方法的入参数组,即传入的对象数据。
proceed() 执行调用目标方法,返回执行结果。

3. @AfterThrowing

通过 @AfterThrowing 注解定义异常通知,将在连接点抛出异常时执行。

@AfterThrowing(value = "pointcut()", throwing = "throwable")
public void afterThrowing(JoinPoint joinPoint, Exception throwable) {
    System.out.println("切面异常, 方法: {}", ((MethodSignature) joinPoint.getSignature()).getMethod().toString());
    System.out.println("After throwing " + joinPoint + ", throw message: " + throwable.getMessage());
}

4. @AfterReturning

通过 @AfterReturning 用于定义后置通知,在目标方法成功执行并返回结果后执行,即在方法返回之后执行。

@AfterReturning(value = "pointcut()", returning = "obj")
public void afterReturning(JoinPoint joinPoint, Object obj) {
    System.out.println("After returning, " + joinPoint + ", return: " + obj);
}

5. @After

通过 @After 定义后置通知,无论目标方法是正常返回还是抛出异常,都会在方法执行之后执行。

其使用方式与 @AfterReturning 相同,仅作用域不同,@AfterReturning 可以理解为 @After 的真子集。

四、项目实战

以常见的埋点日志为例,当工程的方法标识特定注解时,将方法执行信息如入参、返回值等信息进行记录。

1. 注解定义

定义埋点日志注解 @BuriedLog,设置作用域为方法,用于后续标识目标方法。

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface BuriedLog {

    String value() default "";
}

2. 测试方法

准备一个测试方法,并在方法上声明 @BuriedLog,当该方法被调用时将触发切面。

@RestController
@RequestMapping("/api/aop/log")
public class Resource1 {

    /**
     * 在方法声明注解标识方法信息
     */
    @BuriedLog("测试方法")
    @GetMapping("get")
    public String test1(@RequestParam("msg") String msg) {
        return "test1 return: " + msg;
    }
}

3. 切点定义

详细的切点定义这里就不再展开介绍了,下面定义了针对 @BuriedLog 注解的切点。

@Aspect
@Component
public class AspectConfig {
    
    @Pointcut("@annotation(buriedLog) && @annotation(xyz.ibudai.aop.anntation.BuriedLog)")
    public void pointcut(BuriedLog buriedLog) {
    }
}

4. 实体定义

定义实体类 LogDetail 用于记录目标方法的各类信息。

public class LogDetail {

    /** 目标类名 */
    private String className;

    /** 目标方法名 */
    private String methodName;

    /** 注解描述信息 */
    private String describe;

    /** 方法入参 */
    private Object[] params;

    /** 方法执行结果 */
    private Object result;
}

5. 方法环切

接下来到了方法环切也是整个流程中的重点,让我们先拆解为下面几个步骤:

  • 根据切点监控拦截对应方法;
  • 获取目标执行方法信息;
  • 记录方法入参、回参以及方法注解标识;
  • 返回结果并退出环切;

那么下面就让我们来看下具体应该如何实现,注意最后一定要返回 proceed() 的执行结果。

@Aspect
@Component
public class AspectConfig {
    
    @Pointcut("@annotation(buriedLog) && @annotation(xyz.ibudai.aop.anntation.BuriedLog)")
    public void pointcut(BuriedLog buriedLog) {
    }

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint, BuriedLog buriedLog) {
        Object result;
        try {
            // 获取切面类型
            String kind = joinPoint.getKind();
            // 获取目标类
            Object target = joinPoint.getTarget();
            // 获取目标方法入参
            Object[] args = joinPoint.getArgs();

            // 获取目标方法签名
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            // 获取目标方法
            Method method = signature.getMethod();
            // 方法注解信息
            String describe = buriedLog.value();

            // 执行方法调用
            result = joinPoint.proceed();
            // 构建日志实体
            LogDetail logDetail = LogDetail.builder()
                    .kind(kind)
                    .className(target.getClass().getName())
                    .methodName(method.getName())
                    .describe(describe)
                    .params(args)
                    .result(result)
                    .build();
            System.out.println(logDetail);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
        // 返回结果
        return result;
    }
}

完成后启动项目并调用 get 测试接口。

通过断点可以查看到构建日志实体按照我们的预期包含了方法对应的信息。


文章作者: 烽火戏诸诸诸侯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 烽火戏诸诸诸侯 !
  目录