通俗的语言讲好Spring AOP

通俗的语言讲好Spring AOP

_

用通俗的方式拆解Spring AOP,让你彻底搞懂它。

一、Spring AOP的核心本质(用比喻讲清楚)

先看一个生活场景:
你去餐厅点一份「番茄炒蛋」,厨师的核心业务是:备料→炒鸡蛋→炒番茄→混合→装盘。
但整个过程中还有一些通用辅助操作

  • 炒菜前:洗手、开抽油烟机(前置)
  • 炒菜中:随时调整火候(环绕)
  • 炒菜后:刷锅、清理灶台(后置)
  • 炒糊了:处理焦糊食材、道歉(异常)
  • 炒成功:擦干净盘子边缘(返回后)

如果厨师每做一道菜,都要把“洗手、开抽油烟机、刷锅”这些步骤写进“番茄炒蛋/青椒肉丝/宫保鸡丁”的操作手册里,会出现两个问题:

  1. 重复代码太多(所有菜品都要写这些步骤);
  2. 维护成本高(比如要改“洗手流程”,得改所有菜品的手册)。

Spring AOP的本质
把这些「通用辅助逻辑」(洗手、刷锅等)从「核心业务逻辑」(做菜)中抽离出来,通过“动态代理”的方式,在不修改核心业务代码的前提下,自动把辅助逻辑“织入”到核心业务的指定位置(比如炒菜前/后)。

对应到编程中:

  • 核心业务:Controller/Service中的业务方法(比如createOrder()pay());
  • 通用辅助逻辑:日志记录、事务管理、权限校验、性能监控等;
  • AOP:帮你把这些通用逻辑“自动贴”到业务方法的执行前后/异常时,无需在每个业务方法里重复写。

二、Spring AOP的核心概念(场景对应)

先把AOP的核心术语和上面的“做菜场景”一一对应,抽象概念瞬间变具体:

AOP术语中文做菜场景类比编程场景类比
Aspect切面「厨房通用操作手册」(包含洗手、刷锅等)封装通用逻辑的类(比如LogAspect、AuthAspect)
Joinpoint连接点做菜过程中所有可插入辅助操作的时机(备料前、装盘后等)业务方法执行过程中的所有时机(方法执行前/后/异常时)
Pointcut切入点筛选要执行辅助操作的时机(只给炒蛋类菜品执行洗手流程)筛选要织入切面的连接点(比如只给order包下的方法加日志)
Advice通知具体的辅助操作+执行时机(备料前洗手、装盘后刷锅)切面中的具体逻辑+执行时机(Before/After/Around等)
Weaving织入把洗手/刷锅步骤加到炒番茄炒蛋的流程里把切面逻辑动态融合到业务方法执行过程中
Target Object目标对象正在做菜的厨师被增强的业务类(比如OrderService)
Proxy代理对象带辅助流程的“增强版厨师”Spring生成的、包含切面逻辑的代理类

三、代码示例(Spring Boot + AOP)

用最常见的「日志记录」场景演示,让你直观看到AOP的用法:

1. 引入依赖(pom.xml)

首先要引入AOP的核心依赖:

<!-- Spring AOP核心依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2. 核心业务类(Target Object)

模拟订单服务的核心业务:

import org.springframework.stereotype.Service;

// 目标对象:核心业务类
@Service
public class OrderService {

    // 连接点:这个方法的执行时机(执行前/后/异常时)都是连接点
    public String createOrder(String orderId) {
        System.out.println("核心业务:创建订单,订单号=" + orderId);
        // 模拟异常场景(可注释掉测试异常通知)
        // if (orderId.equals("error")) {
        //     throw new RuntimeException("订单创建失败");
        // }
        return "订单创建成功:" + orderId;
    }

    public void payOrder(String orderId) {
        System.out.println("核心业务:支付订单,订单号=" + orderId);
    }
}

3. 定义切面类(Aspect)

抽离日志逻辑,指定切入点和通知:

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

// 1. 标记为切面类(Aspect)
@Aspect
// 2. 交给Spring管理
@Component
public class LogAspect {

    // 3. 定义切入点(Pointcut):筛选要增强的方法
    // 表达式含义:匹配OrderService类下的所有public方法
    @Pointcut("execution(public * com.example.demo.service.OrderService.*(..))")
    public void orderPointcut() {}

    // 4. 前置通知(Before):切入点方法执行前执行
    @Before("orderPointcut()")
    public void beforeAdvice(JoinPoint joinPoint) {
        // 获取方法名和参数
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        System.out.println("[前置通知] 方法" + methodName + "开始执行,参数:" + args[0]);
    }

    // 5. 返回通知(AfterReturning):方法正常返回后执行
    @AfterReturning(value = "orderPointcut()", returning = "result")
    public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("[返回通知] 方法" + methodName + "执行成功,返回值:" + result);
    }

    // 6. 异常通知(AfterThrowing):方法抛出异常时执行
    @AfterThrowing(value = "orderPointcut()", throwing = "e")
    public void afterThrowingAdvice(JoinPoint joinPoint, Exception e) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("[异常通知] 方法" + methodName + "执行失败,异常:" + e.getMessage());
    }

    // 7. 后置通知(After):方法无论成功/失败都会执行(类似finally)
    @After("orderPointcut()")
    public void afterAdvice(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("[后置通知] 方法" + methodName + "执行结束(无论成功/失败)");
    }

    // 8. 环绕通知(Around):最强的通知,可控制方法的执行(前置+执行+后置)
    // 注:一般不用和其他通知混用,避免逻辑混乱
    // @Around("orderPointcut()")
    // public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
    //     String methodName = joinPoint.getSignature().getName();
    //     System.out.println("[环绕前置] 方法" + methodName + "准备执行");
    //     // 执行核心业务方法
    //     Object result = joinPoint.proceed();
    //     System.out.println("[环绕后置] 方法" + methodName + "执行完成,返回值:" + result);
    //     return result;
    // }
}

4. 测试类

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

@SpringBootApplication
public class AopDemoApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(AopDemoApplication.class, args);
        OrderService orderService = context.getBean(OrderService.class);
        
        // 执行核心业务方法
        orderService.createOrder("O20251229001");
        // 测试异常场景:orderService.createOrder("error");
        System.out.println("------------------------");
        orderService.payOrder("O20251229001");
    }
}

5. 执行结果

[前置通知] 方法createOrder开始执行,参数:O20251229001
核心业务:创建订单,订单号=O20251229001
[返回通知] 方法createOrder执行成功,返回值:订单创建成功:O20251229001
[后置通知] 方法createOrder执行结束(无论成功/失败)
------------------------
[前置通知] 方法payOrder开始执行,参数:O20251229001
核心业务:支付订单,订单号=O20251229001
[返回通知] 方法payOrder执行成功,返回值:null
[后置通知] 方法payOrder执行结束(无论成功/失败)

四、新手容易误解的关键点

  1. 混淆连接点(Joinpoint)和切入点(Pointcut)

    • 错误认知:以为两者是同一个东西;
    • 正确理解:连接点是“所有可能的时机”(比如OrderService的所有方法的执行前/后),切入点是“筛选后的时机”(比如只选createOrder方法的执行前)。
    • 类比:连接点是餐厅所有菜品,切入点是“只选川菜”。
  2. 认为AOP只能做日志

    • 错误认知:AOP=日志记录;
    • 正确理解:日志只是AOP的常见场景,它还能做事务管理(Spring声明式事务底层就是AOP)、权限校验、性能监控、缓存控制等。
  3. 滥用环绕通知(Around)

    • 错误认知:环绕通知功能强,所有场景都用它;
    • 正确理解:环绕通知需要手动调用joinPoint.proceed()执行核心方法,逻辑复杂,新手容易漏写导致核心方法不执行。只有需要“控制核心方法是否执行/修改参数/修改返回值”时才用,普通日志/校验用Before/After即可。
  4. 以为AOP修改了原业务代码

    • 错误认知:AOP是改了OrderService的源码;
    • 正确理解:Spring AOP基于动态代理实现,运行时生成代理对象(Proxy),原业务类(Target Object)的代码完全没改,只是执行时走的是代理对象的逻辑。
  5. 切入点表达式写错导致切面不生效

    • 常见错误:包名写错、方法修饰符漏写(比如把public写成private)、参数匹配错误((..)表示任意参数,新手会写成()导致只匹配无参方法);
    • 避坑技巧:写表达式时先简化(比如先匹配所有方法execution(* *(..))),确认生效后再缩小范围。

总结

  1. 核心本质:Spring AOP是“面向切面编程”,把通用辅助逻辑(日志、事务等)从核心业务中抽离,动态织入到指定位置,解决代码重复、降低耦合。
  2. 核心逻辑:通过「切面(Aspect)」封装通用逻辑,「切入点(Pointcut)」筛选目标方法,「通知(Advice)」指定执行时机和逻辑,最终通过「织入」增强核心业务。
  3. 新手避坑:区分连接点和切入点、避免滥用环绕通知、注意切入点表达式的正确性,且AOP不会修改原业务代码。
springboot3 Feign 与springboot 2有什么区别? 2025-12-29
大白话讲清楚IOC概念 2025-12-29

评论区