在 SpringBoot 中,利用 AOP 实现拦截控制的方法有很多,个人觉得相对比较简洁、比较简单的方式是通过自定义注解实现拦截控制。这种实现方式只需要预先定义一个新的注解,并实现拦截控制的具体业务逻辑,当我们想要拦截某一个方法进行控制时,只需要在方法前加上该注解,通常不需要做过多的调整。在实际工程应用中,这种实现方式确实有效提升了开发效率。

AOP 的基本概念

AOP 是 Aspect-oriented Programming 的缩写,常译作”面向切面编程“,通俗理解就是在程序运行的链路中,从某一个横切面介入业务,进行拦截后实现自定义的业务,然后再恢复原有的业务链路。这是一种程序设计模式,实际上 SpringBoot 的设计在很多地方都使用了这个模式。

与 AOP 相关的常见概念还包括:

  • 注解@Aspect:将标记的类作为一个切面供容器进行读取,该切面包含 Point Cut(切点)和 Joint Point(连接点)。

  • Point Cut(切点):切面标记的位置

  • Joint Point(连接点):被拦截的目标方法,即原来的实现方法

  • Advice(增强):在 Advice 中定义了 Point Cut 需要完成的动作,包括在切点 Before、After、替代切点自身执行的代码模块。

    • @Around:环绕方法。贯穿整个方法执行的前后,可以决定是否执行,如何执行,执行后如何操作等。
    • @Before:在被拦截的目标方法执行前的动作,可以修改或者替换方法的入参。
    • @After:在切点方法执行后的动作。
    • @AfterReturning:在切点方法成功执行后的动作,可以对返回结果进行拦截修改。
    • @AfterThrowing:在切点方法执行中抛出了异常,可以抛出异常后的流程进行处理。

几个注解方法的增强顺序依次为 Around, Before, After, AfterReturning, AfterThrowing。

基本实现流程

引入maven依赖

在 pom.xml 中引入响应的依赖模块:

<dependency>
	<groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

新建一个自定义注解类

类实现代码大致如下:

package com.common.util.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AopAnnotation {
    // 注解标识值
    String value() default "";
    // 描述信息
    String description default "";
}

这里需要指定注解的目标为方法,根据实际需求,注解的目标也可以是参数或者类,在这里的例子展示我们暂时只指定方法。同样,指定注解在运行时生效。这个自定义注解就可以用来注解指定切点了。

定义 Aspect 切面类

这里说的切面类,就是通过上述注解拦截方法前后,执行业务逻辑控制的实现类。为了将切面类注入容器,需要在类的前面分别加上注解@Aspect 和 @Component。

以下是常用的实现模式,除了@Pointcut标注的方法是必需之外,其他方法都是可选的。该标注方法指定了当前 AOP 方法面向的切点路径,即通过指定的注解明确切点路径。

@Aspect
@Component
public class DemoAspect {

    // Service层切点
    @Pointcut("@annotation(aop.AopDemoAnnotation)")
    public void servicePointcut() {
        System.out.println("Pointcut: 不会被执行");
    }

    // 切点方法执行前运行
    @Before(value = "servicePointcut()")
    public void doBefore(JoinPoint joinPoint) {
        System.out.println("Before 方法执行 - Start time: " + System.currentTimeMillis());
    }

    @Around("servicePointcut()")
    public void doAround(ProceedingJoinPoint jointPoint) throws Throwable {
        System.out.println("Around 方法开始");

        // 获取当前访问的class类及类名
        Class<?> clazz = jointPoint.getTarget().getClass();
        String clazzName = jointPoint.getTarget().getClass().getName();

        // 获取访问的方法名
        String methodName = jointPoint.getSignature().getName();
        // 获取方法所有参数及其类型

        Object[] args = jointPoint.getArgs();
        Class[] argClz = ((MethodSignature) jointPoint.getSignature()).getParameterTypes();
        // 获取访问的方法对象
        Method method = clazz.getDeclaredMethod(methodName, argClz);

        // 判断当前访问的方法是否存在指定注解
        if (method.isAnnotationPresent(AopDemoAnnotation.class)) {
            AopDemoAnnotation annotation = method.getAnnotation(AopDemoAnnotation.class);

            // 获取注解标识值与注解描述
            String value = annotation.value();
            String desc = annotation.description();

            // 执行目标方法
            Object proceed = jointPoint.proceed();
            System.out.println("打印方法是执行返回结果:" + proceed.toString());
        }

        System.out.println("Around 方法结束");
    }

    // 切点方法执行后运行,不管切点方法执行成功还是出现异常
    @After(value = "servicePointcut()")
    public void doAfter(JoinPoint joinPoint) {
        System.out.println("After 方法执行 - Finish time: " + System.currentTimeMillis());
    }

    // 切点方法成功执行返回后运行
    @AfterReturning(value = "servicePointcut()", returning="returnValue")
    public void doAfterReturning(JoinPoint joinPoint, Object returnValue) {
        System.out.println("AfterReturning 方法执行 - Returned value: " + returnValue);
    }

    // 切点方法成功执行返回后运行
    @AfterThrowing(value = "servicePointcut()", throwing="throwing")
    public void doAfterThrowing(JoinPoint joinPoint, Object throwing) {
        System.out.println("AfterThrowing 方法执行 - throwing value: " + throwing);
    }
}

以上就是针对切点常用的几种增强方法,具体规则在注释中都有说明。其中,servicePointcut() 这个空方法相当于是一个标记,用来与被注解的服务方法进行关联,作为其他增强方法的注解 value。在 doAround() 方法中,列举了一些常用的获取相关对象的方法,这里通过成员变量方法获取注解的属性值,有另外一种写法可以直接将注解所属的类对象作为增强方法入参,具体如下:

@Aspect
@Component
public class DemoAspect {

	// Service层切点	
    @Pointcut("@annotation(com.common.util.annotation.AopAnnotation)")
	public void servicePointcut() {}
    
    // 入参命名要跟 @annotation里的命名一致
	@Around(value = "servicePointcut() && @annotation(anno)")
	public void doAround(ProceedingJoinPoint jointPoint, AopAnnotation anno) throws Throwable {
        
        // 获取注解标识值与注解描述
        String value = annotation.value();
        String desc = annotation.description();

        // 执行目标方法
        proceed = jointPoint.proceed();  
	}
}

这种方式直接把注解作为入参带入到方法当中,不过需要注意的是,在诸如 @Around 注解里面的 @annotation 注解的值,必须要跟注解入参的命名一致,这里为了便于说明,我都命名为 “anno”。如果命名不同,程序可能会无法获取到该注解对应的对象,造成业务异常。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐