前言

面向切面编程(AOP, Aspect-Oriented Programming)是 Spring 框架的核心特性之一。它通过预编译方式和运行期动态代理实现程序功能的统一维护。在 Spring Boot 生态中,AOP 主要用于处理日志记录、性能统计、安全控制、事务处理、异常处理等横切关注点(Cross-cutting Concerns)。

本文将从 声明式(基于 @Aspect 注解) 和 编程式(基于 Advisor 底层 API) 两个维度,总结 Spring Boot 中 AOP 的实现方式及其适用场景。


一、 基于注解的用法(声明式 AOP)

这是 Spring Boot 业务开发中最常见、最高效的使用方式。它利用 AspectJ 的注解语法(如 @Aspect, @Pointcut, @Around)来定义切面,Spring 容器在启动时会自动解析这些注解并生成代理对象。

1. 核心组件

  • @Aspect:标记一个类为切面类。
  • @Pointcut:定义切点表达式,确定哪些类的哪些方法需要被拦截。
  • Advice 注解:定义增强逻辑的执行时机(@Before, @After, @Around, @AfterReturning, @AfterThrowing)。

2. 实现步骤

2.1 引入依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.2 定义切面类
@Aspect
@Component
@Slf4j
public class LoggingAspect {

    // 定义切点:匹配 com.example.service 包下所有类的所有 public 方法
    @Pointcut("execution(public * com.example.service.*.*(..))")
    public void serviceLayer() {}

    // 定义切点:匹配带有 @MyLog 注解的方法
    @Pointcut("@annotation(com.example.annotation.MyLog)")
    public void annotationLayer() {}

    // 环绕通知:最强大的通知类型,控制目标方法的执行
    @Around("serviceLayer() || annotationLayer()")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        
        // 执行目标方法
        Object result = joinPoint.proceed();
        
        long executionTime = System.currentTimeMillis() - start;
        log.info("{} executed in {} ms", joinPoint.getSignature(), executionTime);
        
        return result;
    }
}

3. 技术特性

  • 实现机制:Spring AOP 默认使用 CGLIB(针对类)或 JDK 动态代理(针对接口)生成代理对象。
  • 自动装配AopAutoConfiguration 会自动查找容器中标注了 @Aspect 的 Bean,并将其转换为底层的 Advisor 列表。
  • 适用场景:绝大多数标准业务场景,如统一日志、鉴权、全局异常处理等,逻辑相对静态,切点规则在编译期即可确定。

二、 非基于注解的用法(编程式 AOP / 底层 API)

在中间件开发、框架集成或动态规则场景下,@Aspect 的方式可能显得不够灵活。此时,我们需要直接操作 Spring AOP 的底层接口:AdvisorPointcutAdvice

这种方式的核心在于:手动构建 Advisor 并注入 Spring 容器。Spring 的 AnnotationAwareAspectJAutoProxyCreator 会自动扫描容器中的所有 Advisor Bean,并将其应用到匹配的 Bean 上。

1. 核心组件

  • Advice (通知):具体要执行的拦截逻辑,通常实现 MethodInterceptor 接口。
  • Pointcut (切点):定义类和方法的匹配规则,常用实现如 AspectJExpressionPointcutAnnotationMatchingPointcut
  • Advisor (顾问):Spring AOP 的基本单元,封装了 PointcutAdvice。最常用实现是 DefaultPointcutAdvisor

2. 实现步骤

2.1 定义拦截逻辑 (MethodInterceptor)
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

public class PerformanceInterceptor implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("Method " + invocation.getMethod().getName() + " starts.");
        Object result = invocation.proceed();
        System.out.println("Method finishes.");
        return result;
    }
}
2.2 定义并注册 Advisor (@Configuration)

我们不再使用 @Aspect,而是直接通过 Java Config 组装 Bean。

@Configuration
public class AopConfiguration {

    @Bean
    public DefaultPointcutAdvisor performanceAdvisor() {
        // 1. 定义切点 (Pointcut)
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        // 依然可以使用 AspectJ 表达式,或者使用其他编程式 Pointcut 实现
        pointcut.setExpression("execution(* com.example.service.*.*(..))");

        // 2. 定义通知 (Advice)
        MethodInterceptor advice = new PerformanceInterceptor();

        // 3. 组装 Advisor
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
        advisor.setPointcut(pointcut);
        advisor.setAdvice(advice);
        
        // 设置优先级
        advisor.setOrder(1); 
        return advisor;
    }
}

3. 进阶:完全自定义 Pointcut

除了使用字符串表达式,编程式 AOP 允许使用强类型的 Pointcut,例如 AnnotationMatchingPointcut(这也是很多开源框架常用的方式)。

@Bean
public DefaultPointcutAdvisor annotationAdvisor() {
    // 匹配类上或方法上带有 @MyLog 注解的切点
    Pointcut pointcut = new AnnotationMatchingPointcut(null, MyLog.class);
    
    return new DefaultPointcutAdvisor(pointcut, new PerformanceInterceptor());
}

4. 技术特性与适用场景

  • 高度定制:可以在运行时动态构建切点规则(例如从数据库读取规则并生成 Pointcut),而不受限于硬编码的注解。
  • 无侵入性:不需要在业务代码上加注解,也不需要扫描特定的包路径,适合开发通用的 SDK 或 Starter。
  • 底层暴露:这是 Spring 事务 (@Transactional)、缓存 (@Cacheable) 等功能的底层实现方式。

三、 原理揭秘:注解 (@Aspect) 与底层 (Advisor) 的等效性

在理解了“声明式”和“编程式”两种用法后,我们需要深入 Spring AOP 的底层运行机制。事实上,@Aspect 并非独立于 Advisor 之外的另一套体系,它仅仅是 Spring 提供的一种高级抽象和配置方式。

1. 底层转换机制

在 Spring 容器启动过程中,核心组件 AnnotationAwareAspectJAutoProxyCreator(一种 BeanPostProcessor)承担了关键职责。它的工作流程如下:

  1. 扫描:它会在容器中寻找所有标注了 @Aspect 注解的 Bean。
  2. 解析:解析该类中所有的通知注解(@Before, @After, @Around 等)。
  3. 提取与转换:对于每一个通知注解,Spring 都会将其提取出来,并结合 @Pointcut 定义的表达式,实例化为一个独立的 Advisor 对象(具体实现类通常为 InstantiationModelAwarePointcutAdvisorImpl)。
  4. 注册:这些生成的 Advisor 对象会被加入到代理工厂的拦截器链中,最终作用于目标 Bean。

结论:一个包含多个通知方法(Advice Methods)的 @Aspect 类,在运行时会被拆解为多个 Advisor 实例。Advisor 始终是 Spring AOP 运行时的原子执行单元。

2. 代码等效性演示

为了验证上述结论,我们将展示一个标准的 @Aspect 切面类,并给出其完全等效的 Advisor 编程式写法。

2.1 源头:基于注解的切面

这是一个典型的业务切面,包含一个切点、一个前置通知和一个环绕通知。

@Aspect
@Component
public class BusinessLogAspect {

    // 1. 定义切点:所有 Service 层的 public 方法
    @Pointcut("execution(public * com.example.service.*.*(..))")
    public void serviceLayer() {}

    // 2. 前置通知
    @Before("serviceLayer()")
    public void doBefore() {
        System.out.println("[Aspect] Before method execution");
    }

    // 3. 环绕通知
    @Around("serviceLayer()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        System.out.println("[Aspect] Execution time: " + (System.currentTimeMillis() - start) + "ms");
        return result;
    }
}
2.2 等效:基于 Advisor 的手动实现

如果不使用 @Aspect 注解,要达到完全相同的运行时效果,我们需要手动定义 Bean 来组装 Pointcut 和 Advice。

注意:下面的配置类在 Spring 容器看来,与上面的 BusinessLogAspect 产生的效果是等价的。

@Configuration
public class AdvisorConfiguration {

    // 1. 定义公共切点 (AspectJExpressionPointcut)
    // 对应 @Pointcut
    @Bean
    public AspectJExpressionPointcut serviceLayerPointcut() {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression("execution(public * com.example.service.*.*(..))");
        return pointcut;
    }

    // 2. 构建前置通知的 Advisor
    // 对应 @Before
    @Bean
    public Advisor beforeAdvisor(AspectJExpressionPointcut pointcut) {
        // 定义通知逻辑 (MethodBeforeAdvice)
        MethodBeforeAdvice advice = (method, args, target) -> {
            System.out.println("[Advisor] Before method execution");
        };
        
        // 组装 Pointcut 和 Advice
        return new DefaultPointcutAdvisor(pointcut, advice);
    }

    // 3. 构建环绕通知的 Advisor
    // 对应 @Around
    @Bean
    public Advisor aroundAdvisor(AspectJExpressionPointcut pointcut) {
        // 定义通知逻辑 (MethodInterceptor)
        // MethodInterceptor 是 Spring AOP 环绕通知的底层接口
        MethodInterceptor interceptor = invocation -> {
            long start = System.currentTimeMillis();
            // 执行目标方法
            Object result = invocation.proceed();
            System.out.println("[Advisor] Execution time: " + (System.currentTimeMillis() - start) + "ms");
            return result;
        };

        // 组装 Pointcut 和 Advice
        // 注意:这里复用了同一个 pointcut 对象,与 @Aspect 中引用同一个 @Pointcut 方法逻辑一致
        return new DefaultPointcutAdvisor(pointcut, interceptor);
    }
}

3. 对比分析

通过上述代码对比,我们可以清晰地看到映射关系:

  • 切点映射:注解中的切点表达式字符串 →\rightarrow AspectJExpressionPointcut 对象。
  • 通知映射
    • @Before 方法体 →\rightarrow MethodBeforeAdvice 接口实现。
    • @Around 方法体 →\rightarrow MethodInterceptor 接口实现。
  • 结构映射
    • @Aspect 类是多个逻辑的聚合体。
    • Spring 底层将其拆解为多个 DefaultPointcutAdvisor(或其内部实现),每个 Advisor 只负责一个具体的切面逻辑(1 Pointcut + 1 Advice)。

理解这一层转换关系,有助于开发者在遇到复杂的 AOP 失效问题或需要进行底层框架扩展时,能够跳出注解的限制,直接在 Advisor 层级进行调试和控制。


四、 架构视角:Spring AOP 与 Spring MVC Interceptor 的辩证关系

在 Spring 生态中,开发者经常会混淆 切面(AOP)拦截器(Interceptor)。两者在功能上确实存在重叠,都体现了“拦截器模式”或“责任链模式”的设计思想,即在核心逻辑执行前后插入横切逻辑。

然而,在 Spring 的架构分层中,它们处于完全不同的维度。本章将从底层实现、作用范围及执行时序三个方面理清它们的关系。

1. 概念上的同源与异构

从广义的设计模式来看,AOP 的 Advice(通知)本质上也是一种拦截器。

  • 同源性

    • Spring AOP 的底层实现:基于 org.aopalliance.intercept.MethodInterceptor 接口。当 Spring 代理工厂生成代理对象时,会将 Advice 封装为 MethodInterceptor 组成的拦截器链。
    • Spring MVC 的拦截器:基于 org.springframework.web.servlet.HandlerInterceptor 接口。
  • 异构性(关键区别)

    • AOP (MethodInterceptor):面向 Java 方法调用。它关注的是类的粒度,通过动态代理(CGLIB/JDK)嵌入到 Bean 的方法执行过程中。
    • MVC Interceptor (HandlerInterceptor):面向 HTTP 请求。它关注的是 URL 的粒度,由 DispatcherServlet 在分发请求的过程中显式回调。

2. 核心差异对比表

为了更直观地理解,我们可以通过以下技术指标进行对比:

维度 Spring AOP (@Aspect) Spring MVC Interceptor
所属层级 业务逻辑层 (Service/Component) Web 接入层 (Controller)
拦截粒度 方法级 (Method) 请求级 (Request/URL)
核心机制 动态代理 (Dynamic Proxy) 反射回调 (Reflection Callback)
管理容器 Spring IOC 容器 DispatcherServlet (Web 上下文)
参数获取 JoinPoint (参数对象、方法名、返回值) HttpServletRequest, HttpServletResponse
适用场景 事务、缓存、业务日志、细粒度权限 登录校验、跨域设置、通用 HTTP 头处理
生效条件 必须是 Spring 管理的 Bean 请求必须经过 DispatcherServlet

3. 执行顺序:洋葱模型

在一个标准的 Spring Boot Web 请求中,Filter、Interceptor 和 AOP 的执行顺序呈现出一种嵌套的“洋葱模型”。

假设一个 HTTP 请求到达 Controller 的方法,AOP套在Controller方法上,其调用栈如下:

  1. Servlet 容器 (Tomcat) 接收请求。
  2. Filter (过滤器)doFilter (进入)
  3. DispatcherServlet:开始分发。
  4. HandlerInterceptor (拦截器)preHandle (进入)
    • 此时请求尚未到达具体的 Controller 方法对象。
  5. Spring AOP (切面)@Around / @Before (进入)
    • 此时进入了 Bean 的代理对象内部。
  6. Controller Method (目标方法):执行核心业务逻辑。
  7. Spring AOP (切面)@AfterReturning / @After (退出)
  8. HandlerInterceptor (拦截器)postHandle / afterCompletion (退出)
  9. Filter (过滤器)doFilter (退出)

五、 Spring AOP 的“自调用”隐形陷阱

有许多注解都是基于AOP开发的,比如 @Transactional@Cacheable。在 Spring 开发中,我们经常遇到一种“灵异现象”:明明在方法上加了 @Transactional 做事务控制,或者加了 @Cacheable 做缓存,但在同一个类中调用该方法时,这些注解却统统失效了,数据库依然被频繁查询,事务回滚也没有发生。

这并非 Spring 的 Bug,而是经典的 “自调用(Self-Invocation)”陷阱

1. 现象还原:失效的代码

假设我们有一个 ClientService,其中 getClientById 方法开启了缓存,而 validateClient 需要在内部调用它:

@Service
public class ClientService {

    // 这是一个内部调用入口
    public void validateClient(String id) {
        // 【陷阱发生处】直接调用内部方法
        // 相当于 this.getClientById(id);
        OAuth2ClientDO client = getClientById(id); 
        
        if (client == null) {
            throw new RuntimeException("Client not found");
        }
    }

    // 这个方法配置了缓存
    @Cacheable(value = "clients", key = "#id")
    public OAuth2ClientDO getClientById(String id) {
        System.out.println("--- 查询数据库 ---"); // 用于测试是否走了缓存
        return clientMapper.selectById(id);
    }
}

运行结果:当你调用 validateClient 时,你会发现控制台每次都打印 --- 查询数据库 ---,缓存注解 @Cacheable 完全没有生效。

2. 原理深度解析:谁偷走了我的代理?

要理解这个问题,必须通过 Spring AOP 的底层原理——动态代理(Dynamic Proxy) 来看。

当我们使用 Spring 注入一个 Bean 时,Spring 注入给我们的其实不是在这个类本身,而是一个 “代理对象(Proxy)”。这个 Proxy 持有原始对象的引用,并在方法调用前后织入 AOP 逻辑(如:查找缓存、开启事务)。

  • 外部调用(External Call)
    当 Controller 调用 service.getClientById() 时,实际上是在调用 Proxy。Proxy 拦截请求,先检查 Redis 缓存,如果有则直接返回,没有才去调用真实对象。这是符合预期的。
  • 内部调用(Self-Invocation)
    当我们在 validateClient 方法内部写 getClientById(id) 时,本质上执行的是 this.getClientById(id)
    这里的 this 指向的是 目标对象(Target Object)本身,而不是 Proxy 类。
    结果:代码直接在这个“裸”对象内部跳转,完全绕过了 Proxy 这个“守门员”,AOP 的增强逻辑自然也就无法执行。

3. 解决方案:如何让“自调用”也能走代理?

既然知道了原因(绕过了 Proxy),解决思路就是:强制通过 Proxy 来调用内部方法

这里介绍三种常见的解决方案,其中“获取自引用”是很多成熟框架(如你看到的 Yudao 源码)采用的方案。

方案 A:获取自引用(getSelf 模式)—— 推荐

通过一个 getSelf() 方法或者 AopContext 获取当前的代理对象。

public void validateClient(String id) {
    // 【修正】不直接调,而是通过“代理对象”去调
    OAuth2ClientDO client = getSelf().getClientById(id); 
}

/**
 * 获取当前 Bean 的代理对象
 */
private ClientService getSelf() {
    // 方式1:通过 AopContext (需在配置开启 exposeProxy=true)
    return (ClientService) AopContext.currentProxy();
    
    // 方式2:或者利用 ApplicationContext.getBean(ClientService.class)
}

方案 B:自我注入(Self-Injection)

利用 Spring 的依赖注入,将自己注入给自己。

@Service
public class ClientService {

    // 注入自己(代理对象)
    @Resource
    @Lazy // 加上 @Lazy 防止循环依赖报错
    private ClientService self;

    public void validateClient(String id) {
        // 通过注入的代理对象调用
        self.getClientById(id);
    }
}

方案 C:代码重构(拆分 Service)

如果内部调用逻辑过于复杂,最符合设计模式的做法是将 getClientById 抽取到另一个独立的 Service(如 ClientQueryService)中。


CGLIB如何实现

  • 在SpringBoot较新版本全量使用CGLIB代理来实现AOP,早期使用JDK动态代理
  • CGLIB生成一个代理类的字类,并重写其中的非final方法,在方法中先执行增强逻辑advice,再通过super.method()调用父类原始逻辑
  • JDK动态代理的方式利用反射机制,有性能损耗
  • CGLIB并不依赖java编译器,并不是“先生成子类再编译为.class”,而是使用FastClass机制代替反射,更快。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐