【Spring Boot】用Spring AOP优雅实现横切逻辑复用

文章目录

  • 前言
  • 一、什么是AOP
  • 二、AOP的核心概念
    • [2.1 连接点(Join Point)](#2.1 连接点(Join Point))
    • [2.2 通知(Advice)](#2.2 通知(Advice))
    • [2.3 切入点(Pointcut)](#2.3 切入点(Pointcut))
      • [2.3.1 execution定义匹配连接点](#2.3.1 execution定义匹配连接点)
      • [2.3.2 annotation定义匹配连接点](#2.3.2 annotation定义匹配连接点)
    • [2.4 切面](#2.4 切面)
    • [2.5 目标对象(Target Object)](#2.5 目标对象(Target Object))
    • [2.6 小结](#2.6 小结)
  • [三、Spring Boot中使用AOP](#三、Spring Boot中使用AOP)
    • [3.1 引入Spring Boot AOP依赖](#3.1 引入Spring Boot AOP依赖)
    • [3.2 编写切面类](#3.2 编写切面类)
      • [3.2.1 前置通知-@Before](#3.2.1 前置通知-@Before)
      • [3.2.2 返回通知-@AfterReturning](#3.2.2 返回通知-@AfterReturning)
      • [3.2.3 异常通知-@AfterThrowing](#3.2.3 异常通知-@AfterThrowing)
      • [3.2.4 后置通知-@After](#3.2.4 后置通知-@After)
      • [3.2.5 环绕通知-@Around](#3.2.5 环绕通知-@Around)
  • [四、Spring AOP的实现原理](#四、Spring AOP的实现原理)
    • [4.1 JDK动态代理](#4.1 JDK动态代理)
    • [4.2 CGLIB动态代理](#4.2 CGLIB动态代理)
  • [五、多个Spring AOP的执行顺序](#五、多个Spring AOP的执行顺序)
  • 总结

前言

在OOP的思想中,一切皆以对象为核心,将数据与操作数据的方法封装在一起,通过封装、继承、多态等特性实现高内聚、低耦合的代码结构。高内聚和低耦合正是我们开发程序所要遵循的两大终极目标。

试想一下,如果有一项业务开发的逻辑,此时我们实现它需要发现需要修改核心源码,这显然不利于程序的维护。那么有什么办法能实现不修改源码的情况下动态扩展功能呢,答案显而易见,通过代理模式,建立代理对象来代替对真实对象(real object)的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作。

当然在Spring中已经提供了一套成熟的解决方案,无需我们手动实现代理模式,它就是大名鼎鼎的Spring AOP。本篇文章我将和大家一起梳理AOP这种编程范式,讨论在Spring Boot中应用AOP编程。


一、什么是AOP

AOP全称Aspect Oriented Programming,直译过来是 面向切面编程。这其实是一种编程范式,它允许我们不修改源代码的情况下,给程序中动态得添加扩展功能,是作为面向对象的一个补充,方便我们去维护程序,提高模块的内聚性,降低模块间的耦合。通过预编译方式和运行期动态代理实现程序功能的统一维护。通过AOP,我们就能实现给特定的方法添加特点的逻辑,比如记录方法的执行时间,操作日志。事实上Spring中的事务也是Spring通过AOP实现的。

二、AOP的核心概念

2.1 连接点(Join Point)

连接点指的是程序执行过程中可以插入切面代码的特定位置,在Spring中简单来说就是可以被AOP控制的方法,即方法调用。

比方说下面这个StudentServiceImpl里的findAll方法,这个findAll方法的每一次调用就是一个连接点。Spring AOP中,默认情况下,IOC容器中Bean的 public方法可作为连接点

java 复制代码
@Service
public class StudentServiceImpl implements StudentService {
    private final StudentMapper studentMapper;
    @Override
    public List<Student> findAll() {
        return studentMapper.findAll();
    }
}

2.2 通知(Advice)

通知被定义为在特定连接点上执行的动作,像我们要织入的横切自定义逻辑,比如记录方法的执行时间,操作日志这种。Spring AOP中通知分为以下几类:

  • 前置通知(Before):在方法执行前运行。
  • 后置通知(After):无论方法是否异常,都会在方法结束后运行。
  • 返回通知(AfterReturning):方法成功返回后运行。
  • 异常通知(AfterThrowing):方法抛出异常时运行。
  • 环绕通知(Around):包围整个方法调用,可控制是否执行原方法

通知在Spring AOP中的作用是用来告诉定义的自定义逻辑应该放在什么时候执行。

2.3 切入点(Pointcut)

切入点是一个表达式,用来定义匹配连接点,也就是指定哪些连接点(Join Point)需要应用通知(Advice)。一般情况下,切入点的设置的越局限,通知的应用范围就越小,AOP执行的效率就越大。所以在实际生产环境下,最好避免对所有方法都织入逻辑。切入点其实就是指定自定义逻辑在哪里执行。

在Spring Boot中定义切入点是通过在空方法上通过@Pointcut注解来定义匹配连接点。

定义一个名为servicePointCut的切入点,匹配所有public修饰、返回值为任意类型的org.araby.blognovelink.service.impl.StudentServiceImpl类下的所有方法

java 复制代码
    @Pointcut("execution(public * org.araby.blognovelink.service.impl.StudentServiceImpl.*(..))")
    public void servicePointCut(){}

2.3.1 execution定义匹配连接点

通过execution定义匹配连接点格式是

java 复制代码
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
  • modifiers-pattern:访问修饰符public、private这种,可选;

  • ret-type-pattern:返回值类型,如void、String、*表示任意返回值,必选;

  • declaring-type-pattern:全类名,...表示当前包及子包,可选;

  • name-pattern:方法名称,*表示任意方法,必选;

  • param-pattern:方法参数列表,...表示任意参数),必选;

  • throws-pattern:异常类型,可选。

2.3.2 annotation定义匹配连接点

还有一种方式是通过注解来定义匹配连接点

自定义@Log注解:

java 复制代码
import java.lang.annotation.*;

@Target(ElementType.METHOD) // 仅作用于方法
@Retention(RetentionPolicy.RUNTIME) // 运行时保留,可通过反射获取
@Documented
public @interface Log {
    String value() default ""; // 注解属性,用于描述日志内容
}

定义切入点,匹配被@Log注解标记的方法

java 复制代码
    @Before("@annotation(org.araby.blognovelink.annotation.Log)")
    public void beforeAdvice(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        log.info("【前置通知】方法名:" + methodName + ",参数:" + Arrays.toString(args));
    }

最后在方法上添加注解就能织入自定义的逻辑

java 复制代码
    @Log
    @Override
    public Boolean update(Student student) {
        student.setUpdateTime(java.time.LocalDate.now());
        return studentMapper.update(student) > 0;
    }

2.4 切面

通知加切入点的组合就是切面。前面提到'通知'是设置我们自定义逻辑应该放在什么时候执行,切入点是设置自定义逻辑在哪里执行。这两者组合起来就是切面,即在哪些地方做什么,这就是切面

在Spring Boot中,切面类是由@Aspect 修饰,我们在里面定义切入点和通知。

比方说这个被@Aspect修饰的LogAspect类,它包含一个servicePointCut切入点,和一个beforeAdvice()前置通知方法。

java 复制代码
@Slf4j
@Aspect
@Component
public class LogAspect {
    @Pointcut("execution(public * org.araby.blognovelink.service.impl.StudentServiceImpl.*(..))")
    public void servicePointCut(){}

    @Before("servicePointCut()")
    public void beforeAdvice(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        log.info("【前置通知】方法名:" + methodName + ",参数:" + Arrays.toString(args));
    }
}

2.5 目标对象(Target Object)

目标对象既被一个或多个切面通知的对象,在Spring AOP中,目标对象必须是Bean对象,它和切面对象一样是交由Spring IOC容器管理,目标对象总是被代理包装,使用JDK动态代理或CGLIB。

Spring AOP正是通过代理模式来增强原始对象的能力。

2.6 小结

总的来说,在Spiring AOP中,通过切面(Aspect) 通过 切入点(Pointcut) 找到 连接点(Join Point),然后在目标对象(Target Object) 的方法中执行时织入通知(Advice),这样就能实现原始对象的功能增强。

三、Spring Boot中使用AOP

3.1 引入Spring Boot AOP依赖

dependencies层级下添加Spring Boot AOP依赖

xml 复制代码
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

3.2 编写切面类

在编写切面类之前,我们假设有个名为StudentServiceImpl的业务类作为目标对象,全类名为org.araby.blognovelink.service.impl.StudentServiceImpl。以下的切入点都匹配的是StudentServiceImpl的findById方法。
业务类

java 复制代码
@Service
public class StudentServiceImpl implements StudentService {
    private final StudentMapper studentMapper;

    @Autowired
    public StudentServiceImpl(StudentMapper studentMapper) {
        this.studentMapper = studentMapper;
    }
    @Override
    public Student findById(Integer id) {
        return studentMapper.findById(id);
    }
 }

然后我们编写一个切面类来实现前置通知,返回通知,异常通知,后置通知和环绕通知。

3.2.1 前置通知-@Before

如果我们想在目标对象的方法前执行逻辑,可以选用@Before注解修饰的前置通知,前置通知方法的参数包括JoinPoint ,这个正是连接点,包含目标对象方法的元数据。比方说可以通过joinPoint的getSignature()获取方法信息。

java 复制代码
    /**
     * 前置通知
     * @param joinPoint
     */
    @Before("servicePointCut()")
    public void beforeAdvice(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        log.info("【前置通知】方法名:" + methodName + ",参数:" + Arrays.toString(args));
    }
bash 复制代码
[INFO ] [2025-12-10 21:34:59.688] [http-nio-9090-exec-2] [REQ-7eaebf76034c4b0aa3ef5d8bc6da1536] o.araby.blognovelink.aop.LogAspect - 【前置通知】方法名:findById,参数:[1]

3.2.2 返回通知-@AfterReturning

标记返回通知的@AfterReturning注解包括一个额外的属性returning 。在下面的示例代码中相当于告诉 Spring,把目标方法的返回值,赋值给通知方法中名为Student的那个参数。也就是returning的值和通知方法的参数名称要一致,且参数类型需兼容目标方法的返回值类型。

java 复制代码
    /**
     * 返回通知
     * @param joinPoint
     * @param Student
     */
    @AfterReturning(value = "servicePointCut()",returning = "Student")
    public void afterReturning(JoinPoint joinPoint, Object Student){
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        log.info("【返回通知】方法名:" + methodName + ",返回值:" + Student);
    }
bash 复制代码
[INFO ] [2025-12-10 21:55:53.748] [http-nio-9090-exec-7] [REQ-2ac8142014ae4f53af7a0d71652071cf] o.araby.blognovelink.aop.LogAspect - 【返回通知】方法名:findById,返回值:Student(id=1, name=巢治文, age=28, sex=男, classNo=eu, createTime=2025-11-08, updateTime=2025-11-08)

3.2.3 异常通知-@AfterThrowing

AfterThrowing注解多了一个throwing属性,当目标方法执行异常触发。

java 复制代码
    /**
     * 异常通知
     * @param joinPoint
     * @param e
     */
    @AfterThrowing(value = "servicePointCut()",throwing = "e")
    public void afterThrowingAdvice(JoinPoint joinPoint,Exception e){
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        log.info("【异常通知】方法名:" + methodName + ",异常信息:" + e.getMessage());
    }
bash 复制代码
[INFO ] [2025-12-10 22:01:26.235] [http-nio-9090-exec-3] [REQ-33bd8436c6f5495bad13d924c7be1887] o.araby.blognovelink.aop.LogAspect - 【异常通知】方法名:findById,异常信息:发生异常

3.2.4 后置通知-@After

后置通知的方法内部包含joinPoint属性,用于获取连接点信息。该逻辑是在方法执行后触发。

java 复制代码
    /**
     * 后置通知
     * @param joinPoint
     */
    @After("servicePointCut()")
    public void afterAdvice(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        log.info("【后置通知】方法名:" + methodName + ",执行结束");
    }

3.2.5 环绕通知-@Around

环绕通知是这几个通知中最为强大的,因为它可以在方法执行的全部生命周期中应用。环绕通知方法有一个核心的ProceedingJoinPoint参数,在我们调用proceedingJoinPoint.proceed()的时候才实际执行目标方法,获取返回值就相当于实现了返回通知的效果。在此之前是前置通知逻辑,再次之后是后置通知逻辑。如果对proceedingJoinPoint.proceed()编写异常捕捉,那就是实现了异常通知。

比方说这里我们通过环绕通知实现了一个记录方法执行时间的逻辑。

java 复制代码
    /**
     * 环绕通知:记录方法执行时间
     * @param proceedingJoinPoint 连接点(目标方法)
     * @return 目标方法的返回值
     * @throws Throwable 目标方法抛出的异常
     */
    @Around("servicePointCut()")
    public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        String methodName = proceedingJoinPoint.getSignature().getName();
        String className = proceedingJoinPoint.getTarget().getClass().getName();
        Object[] args = proceedingJoinPoint.getArgs();
        Object result = null;

        // 开始时间
        long startTime = System.nanoTime();
        try {
            // 执行目标方法
            result = proceedingJoinPoint.proceed();
        } catch (Exception e) {
            log.info("【方法执行异常】类:{},方法:{},参数:{},异常信息:{}",
                    className, methodName, Arrays.toString(args), e.getMessage());
            throw e;
        } finally {
            // 结束时间
            long endTime = System.nanoTime();
            long durationNano = endTime - startTime; // 纳秒耗时
            double durationMs = durationNano / 1_000_000.0; // 转换为毫秒

            log.info("【方法执行时间】类:{},方法:{},参数:{},执行耗时:{}毫秒",
                    className, methodName, Arrays.toString(args),durationMs);
        }
        return result;
bash 复制代码
[INFO ] [2025-12-10 22:17:25.036] [http-nio-9090-exec-4] [REQ-1e52d539f11344509218706010f1e829] o.araby.blognovelink.aop.LogAspect - 【方法执行时间】类:org.araby.blognovelink.service.impl.StudentServiceImpl,方法:findById,参数:[1],执行耗时:50.0671毫秒

四、Spring AOP的实现原理

Spring AOP的底层实现依赖于动态代理技术,主要有两种代理方式:JDK动态代理和CGLIB动态代理。Spring会根据目标对象的类型自动选择合适的代理方式。

若目标对象实现了接口,Spring默认使用JDK动态代理,若目标对象没有实现接口,Spring会使用CGLIB动态代理。

application.yml添加配置spring.aop.proxy-target-class=true,可以强制使用CGLIB代理

4.1 JDK动态代理

JDK动态代理是基于Java反射机制实现的,它要求目标对象必须实现一个或多个接口。Spring通过java.lang.reflect.Proxy类创建代理对象,代理对象会实现目标对象的所有接口,并重写接口中的方法,在重写的方法中织入切面逻辑。也就是目标对象必须实现接口,否则无法使用。

4.2 CGLIB动态代理

CGLIB(Code Generation Library)是一个第三方字节码生成库,它通过继承目标对象的方式创建代理对象,生成目标对象的子类,并重写子类中的方法,织入切面逻辑。因此,CGLIB不要求目标对象实现接口,即使是没有实现任何接口的类,也能通过CGLIB生成代理。由于是基于继承实现的,所以目标对象的final方法无法被代理。

五、多个Spring AOP的执行顺序

多个AOP组合在一起,若是匹配到了相同的目标方法,可以通过@Order注解指定切面的执行顺序,值越小,优先级越高。我们要把这种优先级当作先进后出的栈结构。假设c的优先级高于b,b的优先级高于a。那么c的前置逻辑最先,后置逻辑最后。

前置通知、环绕通知的前置逻辑按@Order值从小到大执行,Order值越小越先执行;后置通知、返回通知、异常通知、环绕通知的后置逻辑按@Order值从大到小执行,Order值越小越后执行.

bash 复制代码
[INFO ] [2025-12-10 22:29:21.568] [http-nio-9090-exec-1] [REQ-ebd5296f7aaa4225bcea5f87a4008007] o.a.blognovelink.aop.AnotherAspect - 【另一个】【前置通知】方法名:findById,参数:[1]
[INFO ] [2025-12-10 22:29:21.568] [http-nio-9090-exec-1] [REQ-ebd5296f7aaa4225bcea5f87a4008007] o.araby.blognovelink.aop.LogAspect - 【前置通知】方法名:findById,参数:[1]
[INFO ] [2025-12-10 22:29:21.618] [http-nio-9090-exec-1] [REQ-ebd5296f7aaa4225bcea5f87a4008007] o.araby.blognovelink.aop.LogAspect - 【返回通知】方法名:findById,返回值:Student(id=1, name=巢治文, age=28, sex=男, classNo=eu, createTime=2025-11-08, updateTime=2025-11-08)
[INFO ] [2025-12-10 22:29:21.618] [http-nio-9090-exec-1] [REQ-ebd5296f7aaa4225bcea5f87a4008007] o.araby.blognovelink.aop.LogAspect - 【后置通知】方法名:findById,执行结束
[INFO ] [2025-12-10 22:29:21.618] [http-nio-9090-exec-1] [REQ-ebd5296f7aaa4225bcea5f87a4008007] o.araby.blognovelink.aop.LogAspect - 【方法执行时间】类:org.araby.blognovelink.service.impl.StudentServiceImpl,方法:findById,参数:[1],执行耗时:49.6057毫秒
[INFO ] [2025-12-10 22:29:21.618] [http-nio-9090-exec-1] [REQ-ebd5296f7aaa4225bcea5f87a4008007] o.a.blognovelink.aop.AnotherAspect - 【另一个】【返回通知】方法名:findById,返回值:Student(id=1, name=巢治文, age=28, sex=男, classNo=eu, createTime=2025-11-08, updateTime=2025-11-08)
[INFO ] [2025-12-10 22:29:21.618] [http-nio-9090-exec-1] [REQ-ebd5296f7aaa4225bcea5f87a4008007] o.a.blognovelink.aop.AnotherAspect - 【另一个】【后置通知】方法名:findById,执行结束
[INFO ] [2025-12-10 22:29:21.618] [http-nio-9090-exec-1] [REQ-ebd5296f7aaa4225bcea5f87a4008007] o.a.blognovelink.aop.AnotherAspect - 【另一个】【方法执行时间】类:org.araby.blognovelink.service.impl.StudentServiceImpl,方法:findById,参数:[1],执行耗时:50.2602毫秒

总结

本文详细介绍了Spring AOP的核心概念、在Spring Boot中的使用方式、实现原理及多切面执行顺序,AOP作为面向对象编程的补充,通过横切逻辑的分离,能有效简化日志、事务、权限等功能的开发,提升代码的可维护性和复用性。在实际项目中,合理使用 AOP可以让业务逻辑更专注于核心功能,同时实现横切需求的统一管理。

相关推荐
葫芦和十三11 小时前
图解 MongoDB 21|选举与 failover:Primary 是怎么选出来的
后端·mongodb·agent
GetcharZp11 小时前
26k Star 开源内网穿透神器 NetBird,一分钟实现全球设备互联!
后端
考虑考虑12 小时前
Mybatis实现批量插入
java·后端·mybatis
咖啡八杯13 小时前
GoF设计模式——中介者模式
java·后端·spring·设计模式
lizhongxuan15 小时前
多Agent之间的区别
后端
青石路17 小时前
记一次多JDK版本问题的排查,一坑套一坑,差点没爬上来
java
杨充17 小时前
1.面向对象设计思想
后端
IT_陈寒17 小时前
Java的Date类又坑了我一次,改用时间戳真香
前端·人工智能·后端
systemPro18 小时前
2.6亿条设备数据,历史查询从超时到50ms,我做了什么
后端
要阿尔卑斯吗18 小时前
提示词优化启示:为什么“按顺序输出“比“关键度评分“更有效
后端