【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可以让业务逻辑更专注于核心功能,同时实现横切需求的统一管理。

相关推荐
snow123f2 小时前
Lambda 表达式怎么用
java·开发语言·线程
梓䈑2 小时前
【C++】C++11(右值引用和移动语义、可变参数模板 和 包装器)
java·开发语言·c++
深海蓝山2 小时前
WebSocket(java版)服务示例
java·websocket·网络协议
Howe~zZ2 小时前
mybatis 报错解决方案ORA-01795: maximum number of expressions in a list is 1000
java·服务器·前端
LiamTuc2 小时前
Java 抽象类详解
java·开发语言
计算机学姐2 小时前
基于Python的高校后勤报修系统【2026最新】
开发语言·vue.js·后端·python·mysql·django·flask
南山乐只2 小时前
Spring Boot 2.x => 3.x 升级指南
java·spring boot·后端
Q_Q19632884752 小时前
python+django/flask+vue的智能房价分析与预测系统
spring boot·python·django·flask·node.js·php
任子菲阳2 小时前
学Java第五十五天——多线程&JUC
java·开发语言