Spring AOP

Spring AOP 深度解析:从原理到实战

Spring AOP(Aspect-Oriented Programming,面向切面编程)是 Spring 框架的核心特性之一,旨在解决横切关注点(如日志、事务、权限)在代码中分散、冗余的问题。它通过动态代理技术,将横切逻辑与业务逻辑解耦,实现 “一次定义,多处复用”。从 “核心价值→概念体系→动态代理→实战配置→避坑指南” 五个维度,彻底拆解 Spring AOP 的底层机制与使用方法。

为什么需要 Spring AOP?—— 解决横切关注点的痛点

在传统开发中,日志、事务、异常处理等横切关注点(跨越多个模块的通用逻辑)会嵌入到业务代码中,导致三大核心问题:

问题 具体表现 示例场景
代码冗余 相同的日志 / 事务逻辑重复出现在数十个方法中,修改时需逐一修改 每个 Service 方法都要写 log.info("方法开始执行")
代码混乱 业务逻辑与横切逻辑混杂,难以区分核心功能与辅助功能 一个 createOrder() 方法中,订单业务、日志、事务代码交织
维护困难 横切逻辑变更时,需修改所有相关业务类,风险高、效率低 日志格式从 “INFO” 改为 “DEBUG”,需修改所有日志语句

AOP 的解决方案:将横切逻辑横向抽取为 “切面(Aspect)”,通过动态代理技术,在不修改业务代码的前提下,将切面 “织入” 到目标方法的指定位置(如方法执行前、执行后),实现与业务逻辑的解耦。

Spring AOP 核心概念:理解 AOP 的 “语言体系”

AOP 有一套标准化的概念体系,是理解和使用 AOP 的基础。用 “日志切面” 案例对应解释每个概念:

概念 官方定义 日志切面案例对应
横切关注点 从业务逻辑中抽取的、跨越多个模块的通用逻辑(如日志、事务) 所有方法的 “执行日志记录” 逻辑
目标对象(Target) 被切面织入的对象(即业务逻辑对象) OrderServiceUserService 等业务类实例
代理对象(Proxy) 目标对象被织入切面后,Spring 生成的代理对象(包含目标方法 + 增强逻辑) OrderService 的代理对象,调用 createOrder() 时会先记录日志
连接点(Joinpoint) 程序执行过程中可插入切面的 “点”(Spring AOP 中仅支持方法执行 OrderService.createOrder()UserService.getUser() 等方法
切点(Pointcut) 对连接点的 “筛选规则”,定义哪些方法会被织入切面(即 “要增强哪些方法”) “所有 Service 类的 public 方法”
通知(Advice) 切面的具体增强逻辑(即 “要做什么”),按执行时机分为 5 种类型 “方法执行前记录日志”“方法异常时记录错误日志”
织入(Weaving) 将通知(Advice)应用到目标对象(Target)的过程(Spring 中是运行期织入 动态代理生成时,将日志逻辑嵌入 createOrder() 方法的执行流程
切面(Aspect) 切点(Pointcut)+ 通知(Advice)的组合(即 “对哪些方法,做什么增强”) “对所有 Service 的 public 方法,执行前记录日志”
通知器(Advisor) 简化版切面,仅包含 “一个切点 + 一个通知”(Spring 早期接口式 AOP 常用) “对 createOrder() 方法,执行后记录事务状态”

Spring AOP 的动态代理:AOP 的 “实现引擎”

Spring AOP 不修改目标类的字节码,而是在运行期通过动态代理生成代理对象,将切面逻辑织入。核心支持两种代理方式,由 DefaultAopProxyFactory 决定选择逻辑。

1. 代理方式的选择逻辑(源码解析)

Spring 通过 DefaultAopProxyFactory.createAopProxy() 方法选择代理方式:

public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
    // 条件1:是否优化代理、是否强制使用CGLIB(proxyTargetClass=true)、是否无用户提供的接口
    if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
        Class<?> targetClass = config.getTargetClass();
        if (targetClass == null) {
            throw new AopConfigException("TargetSource cannot determine target class");
        }
        // 条件2:目标类是接口 或 已是代理类 → 使用JDK动态代理
        if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
            return new JdkDynamicAopProxy(config);
        }
        // 否则 → 使用CGLIB动态代理(ObjenesisCglibAopProxy是CGLIB代理的优化实现)
        return new ObjenesisCglibAopProxy(config);
    } else {
        // 不满足条件1 → 使用JDK动态代理
        return new JdkDynamicAopProxy(config);
    }
}

核心结论

  • 若目标类实现了接口 → 默认用 JDK 动态代理
  • 若目标类未实现接口 → 用 CGLIB 动态代理
  • 若配置 proxyTargetClass=true(如 @EnableAspectJAutoProxy(proxyTargetClass=true)) → 强制用 CGLIB 代理(无论是否有接口)。

2. JDK 动态代理:基于接口的代理

JDK 动态代理是 JDK 原生支持的代理方式,要求目标类必须实现接口,核心依赖 InvocationHandler 接口和 Proxy 类。

核心原理:
  1. 代理类实现目标类的所有接口;
  2. 代理类的所有方法都会委托给 InvocationHandler.invoke() 方法;
  3. invoke() 方法中,先执行切面逻辑(如日志),再调用目标对象的原方法。
简化示例(模拟 Spring JDK 代理):
// 1. 目标接口
public interface UserService {
    User getUserById(Long id);
}

// 2. 目标类(实现接口)
public class UserServiceImpl implements UserService {
    @Override
    public User getUserById(Long id) {
        return new User(id, "张三"); // 业务逻辑
    }
}

// 3. 自定义InvocationHandler(织入日志切面)
public class LogInvocationHandler implements InvocationHandler {
    private final Object target; // 目标对象

    public LogInvocationHandler(Object target) {
        this.target = target;
    }

    // 代理方法:所有接口方法调用都会进入这里
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 1. 前置切面逻辑:日志记录
        System.out.println("方法 " + method.getName() + " 开始执行,参数:" + Arrays.toString(args));
        
        // 2. 调用目标对象的原方法
        Object result = method.invoke(target, args);
        
        // 3. 后置切面逻辑:记录返回结果
        System.out.println("方法 " + method.getName() + " 执行完成,结果:" + result);
        
        return result;
    }
}

// 4. 生成代理对象(模拟Spring操作)
public class JdkProxyDemo {
    public static void main(String[] args) {
        UserService target = new UserServiceImpl();
        // 生成代理对象:参数(类加载器、目标接口、InvocationHandler)
        UserService proxy = (UserService) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new LogInvocationHandler(target)
        );
        
        // 调用代理对象方法(会触发切面逻辑)
        proxy.getUserById(1L);
    }
}

输出结果

方法 getUserById 开始执行,参数:[1]
方法 getUserById 执行完成,结果:User(id=1, name=张三)

3. CGLIB 动态代理:基于继承的代理

CGLIB(Code Generation Library)是一个第三方代码生成库,无需目标类实现接口,通过继承目标类生成子类作为代理类(因此目标类不能是 final,否则无法继承)。

核心原理:
  1. CGLIB 生成目标类的子类(代理类);
  2. 重写目标类的非 final 方法,在重写方法中织入切面逻辑;
  3. 调用代理类的方法时,先执行切面逻辑,再调用父类(目标类)的原方法。
简化示例(模拟 Spring CGLIB 代理):
// 1. 目标类(无接口)
public class OrderService {
    public void createOrder(Order order) {
        System.out.println("创建订单:" + order.getId()); // 业务逻辑
    }
}

// 2. 自定义MethodInterceptor(CGLIB的拦截器,类似InvocationHandler)
public class LogMethodInterceptor implements MethodInterceptor {
    // 拦截方法:所有代理类的方法调用都会进入这里
    @Override
    public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        // 1. 前置切面逻辑:日志记录
        System.out.println("方法 " + method.getName() + " 开始执行,参数:" + Arrays.toString(args));
        
        // 2. 调用目标类的原方法(通过MethodProxy避免递归调用)
        Object result = methodProxy.invokeSuper(proxy, args);
        
        // 3. 后置切面逻辑:记录执行完成
        System.out.println("方法 " + method.getName() + " 执行完成");
        
        return result;
    }
}

// 3. 生成CGLIB代理对象(模拟Spring操作)
public class CglibProxyDemo {
    public static void main(String[] args) {
        // CGLIB增强器
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(OrderService.class); // 设置父类(目标类)
        enhancer.setCallback(new LogMethodInterceptor()); // 设置拦截器(切面逻辑)
        
        // 生成代理对象(子类实例)
        OrderService proxy = (OrderService) enhancer.create();
        
        // 调用代理对象方法(触发切面逻辑)
        proxy.createOrder(new Order(1L));
    }
}

输出结果

方法 createOrder 开始执行,参数:[Order(id=1)]
创建订单:1
方法 createOrder 执行完成

4. JDK 与 CGLIB 代理的核心区别

对比维度 JDK 动态代理 CGLIB 动态代理
依赖条件 目标类必须实现接口 目标类不能是 final,方法不能是 final
代理原理 实现目标接口,委托 InvocationHandler 继承目标类,重写方法,依赖 MethodInterceptor
性能(JDK 8+) 生成代理快,执行效率高 生成代理慢,执行效率略低(但差距极小)
适用场景 目标类有接口(如 Service 接口) 目标类无接口(如工具类)

Spring AOP 实战:注解与 XML 两种配置方式

Spring AOP 支持两种主流配置方式:注解驱动(推荐,Spring Boot 常用)XML 配置(传统方式)。无论哪种方式,核心都是 “定义切面(切点 + 通知)” 并确保切面是 Spring 容器的 Bean。

1. 前置准备:引入依赖

首先需要引入 Spring AOP 和 AspectJ 相关依赖(AspectJ 是 AOP 框架,Spring AOP 借用其注解和切点表达式语法):

<!-- Spring AOP 核心依赖 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>4.3.29.RELEASE</version>
</dependency>

<!-- Spring AspectJ 集成依赖(提供@Aspect等注解) -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>4.3.29.RELEASE</version>
</dependency>

2. 方式一:注解驱动配置(推荐)

通过 @Aspect 定义切面,@Before/@After 等注解定义通知,@EnableAspectJAutoProxy 启用自动代理。

步骤 1:启用 AspectJ 自动代理
  • XML 配置:在 spring-config.xml 中添加 <aop:aspectj-autoproxy/>

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="
               http://www.springframework.org/schema/beans
               http://www.springframework.org/schema/beans/spring-beans.xsd
               http://www.springframework.org/schema/aop
               http://www.springframework.org/schema/aop/spring-aop.xsd
               http://www.springframework.org/schema/context
               http://www.springframework.org/schema/context/spring-context.xsd">
    
        <!-- 扫描组件(确保切面和目标类被Spring管理) -->
        <context:component-scan base-package="com.zhanghe.study.spring4.beans.aoptest"/>
    
        <!-- 启用AspectJ自动代理:生成代理对象,织入切面 -->
        <aop:aspectj-autoproxy/>
    </beans>
  • Java 配置(Spring Boot 常用):在配置类上添加 @EnableAspectJAutoProxy

    @Configuration
    @ComponentScan(basePackages = "com.zhanghe.study.spring4.beans.aoptest")
    @EnableAspectJAutoProxy // 启用AspectJ自动代理
    public class AopConfig {
    }
步骤 2:定义切面(@Aspect)与通知

通过 @Aspect 标记切面类,@Pointcut 定义切点表达式,@Before/@AfterReturning 等定义通知:

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.Arrays;

// 1. @Component:确保切面是Spring容器的Bean(必须)
// 2. @Aspect:标记此类为切面(必须)
// 3. @Order(1):指定切面优先级,数字越小优先级越高(多个切面时控制执行顺序)
@Component
@Aspect
@Order(1)
public class LoggingAspect {

    // -------------------------- 1. 定义切点(复用切点表达式) --------------------------
    // @Pointcut:定义切点,expression是切点表达式
    @Pointcut("execution(public * com.zhanghe.study.spring4.beans.aoptest.*Service.*(..))")
    public void servicePointcut() {} // 空方法,仅作为切点标识

    // -------------------------- 2. 定义通知(增强逻辑) --------------------------
    /**
     * 前置通知:方法执行前执行
     * @param joinPoint:连接点对象,可获取方法名、参数等信息
     */
    @Before("servicePointcut()") // 引用上面定义的切点
    public void beforeAdvice(JoinPoint joinPoint) {
        // 获取方法名
        String methodName = joinPoint.getSignature().getName();
        // 获取方法参数
        Object[] args = joinPoint.getArgs();
        System.out.printf("【前置通知】方法 %s 开始执行,参数:%s%n", methodName, Arrays.toString(args));
    }

    /**
     * 后置通知:方法执行后执行(无论是否异常,都会执行)
     */
    @After("servicePointcut()")
    public void afterAdvice(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        System.out.printf("【后置通知】方法 %s 执行结束%n", methodName);
    }

    /**
     * 返回通知:方法正常执行完成后执行(异常时不执行)
     * @param result:方法返回值,需通过returning参数指定变量名(与方法参数名一致)
     */
    @AfterReturning(
        pointcut = "servicePointcut()",
        returning = "result" // 变量名必须与方法参数"result"一致
    )
    public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
        String methodName = joinPoint.getSignature().getName();
        System.out.printf("【返回通知】方法 %s 执行成功,返回值:%s%n", methodName, result);
    }

    /**
     * 异常通知:方法抛出异常时执行(正常时不执行)
     * @param exception:方法抛出的异常,需通过throwing参数指定变量名(与方法参数名一致)
     */
    @AfterThrowing(
        pointcut = "servicePointcut()",
        throwing = "exception" // 变量名必须与方法参数"exception"一致
    )
    public void afterThrowingAdvice(JoinPoint joinPoint, Exception exception) {
        String methodName = joinPoint.getSignature().getName();
        System.out.printf("【异常通知】方法 %s 执行异常,异常信息:%s%n", methodName, exception.getMessage());
    }

    /**
     * 环绕通知:方法执行前后都可执行,最灵活(可控制方法是否执行、修改参数/返回值)
     * @param proceedingJoinPoint:特殊的JoinPoint,可调用proceed()执行目标方法
     */
    @Around("servicePointcut()")
    public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        String methodName = proceedingJoinPoint.getSignature().getName();
        Object[] args = proceedingJoinPoint.getArgs();
        
        try {
            // 1. 前置逻辑
            System.out.printf("【环绕通知-前置】方法 %s 开始执行%n", methodName);
            
            // 2. 执行目标方法(必须调用proceed(),否则目标方法不会执行)
            Object result = proceedingJoinPoint.proceed(args);
            
            // 3. 后置逻辑(正常执行时)
            System.out.printf("【环绕通知-后置】方法 %s 执行完成%n", methodName);
            return result; // 返回目标方法的结果
        } catch (Exception e) {
            // 4. 异常逻辑
            System.out.printf("【环绕通知-异常】方法 %s 执行异常:%s%n", methodName, e.getMessage());
            throw e; // 重新抛出异常,不掩盖业务异常
        }
    }
}
步骤 3:定义目标对象(业务类)
// 目标类:Service 层业务类(被切面织入)
@Component
public class UserService {
    // 目标方法:会被 LoggingAspect 切面增强
    public User getUserById(Long id) {
        if (id == null) {
            throw new IllegalArgumentException("id 不能为null"); // 模拟异常
        }
        return new User(id, "张三"); // 正常业务逻辑
    }
}
步骤 4:测试 AOP 效果
public class AopTest {
    public static void main(String[] args) {
        // 初始化Spring容器
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
        
        // 获取代理对象(注意:这里获取的是UserService的代理对象,不是原对象)
        UserService userService = context.getBean(UserService.class);
        
        // 1. 调用正常方法(无异常)
        System.out.println("=== 调用正常方法 ===");
        userService.getUserById(1L);
        
        // 2. 调用异常方法(id为null)
        System.out.println("\n=== 调用异常方法 ===");
        try {
            userService.getUserById(null);
        } catch (Exception e) {
            // 捕获异常,避免程序退出
        }
    }
}

输出结果(注意通知执行顺序)

=== 调用正常方法 ===
【环绕通知-前置】方法 getUserById 开始执行
【前置通知】方法 getUserById 开始执行,参数:[1]
【环绕通知-后置】方法 getUserById 执行完成
【后置通知】方法 getUserById 执行结束
【返回通知】方法 getUserById 执行成功,返回值:User(id=1, name=张三)

=== 调用异常方法 ===
【环绕通知-前置】方法 getUserById 开始执行
【前置通知】方法 getUserById 开始执行,参数:[null]
【环绕通知-异常】方法 getUserById 执行异常:id 不能为null
【后置通知】方法 getUserById 执行结束
【异常通知】方法 getUserById 执行异常,异常信息:id 不能为null

3. 方式二:XML 配置(传统方式)

通过 <aop:config> 标签定义切面、切点和通知,无需注解(适合不允许使用注解的传统项目)。

步骤 1:配置目标对象和切面的 Bean
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/aop
           http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 1. 配置目标对象(业务类) -->
    <bean id="userService" class="com.zhanghe.study.spring4.beans.aoptest.UserService"/>

    <!-- 2. 配置切面 Bean(必须是Spring容器的Bean) -->
    <bean id="loggingAspect" class="com.zhanghe.study.spring4.beans.aoptest.LoggingAspect"/>

    <!-- 3. 配置AOP -->
    <aop:config>
        <!-- 3.1 定义切点:id是切点标识,expression是切点表达式 -->
        <aop:pointcut 
            id="servicePointcut" 
            expression="execution(public * com.zhanghe.study.spring4.beans.aoptest.*Service.*(..))"/>

        <!-- 3.2 定义切面:ref指向切面Bean,order指定优先级 -->
        <aop:aspect ref="loggingAspect" order="1">
            <!-- 3.3 配置通知:method是切面类的方法名,pointcut-ref引用切点 -->
            <aop:before method="beforeAdvice" pointcut-ref="servicePointcut"/>
            <aop:after method="afterAdvice" pointcut-ref="servicePointcut"/>
            <aop:after-returning 
                method="afterReturningAdvice" 
                pointcut-ref="servicePointcut" 
                returning="result"/> <!-- 与切面方法参数名一致 -->
            <aop:after-throwing 
                method="afterThrowingAdvice" 
                pointcut-ref="servicePointcut" 
                throwing="exception"/> <!-- 与切面方法参数名一致 -->
            <aop:around method="aroundAdvice" pointcut-ref="servicePointcut"/>
        </aop:aspect>
    </aop:config>
</beans>
步骤 2:切面类(无注解)
// 切面类:无任何注解,仅包含通知方法
public class LoggingAspect {
    // 通知方法与注解方式完全一致(beforeAdvice、afterAdvice等)
    // ...(代码同注解方式的通知方法,省略)
}

测试效果:与注解方式完全一致,XML 配置仅改变了 “切面定义的位置”,未改变 AOP 的执行逻辑。

4. 关键:切点表达式语法

切点表达式是 AOP 的 “筛选规则”,用于定义哪些方法会被织入切面。Spring AOP 支持多种切点标识符,核心是 execution(最常用)。

(1)execution 标识符(核心)

语法:execution([修饰符] 返回值类型 包名.类名.方法名(参数列表) [异常类型])

  • 修饰符:可选(如 public,可省略);
  • 返回值类型:必填(* 表示任意返回值);
  • 包名。类名:必填(* 表示任意类,.. 表示子包);
  • 方法名:必填(* 表示任意方法);
  • 参数列表:必填(() 表示无参,(..) 表示任意参数,(Long) 表示单个 Long 类型参数);
  • 异常类型:可选(如 throws Exception,仅匹配抛出该异常的方法)。

常用示例

切点表达式 含义
execution(* com.foo.service.*.*(..)) 匹配 com.foo.service 包下所有类的所有方法
execution(public User com.foo.service.UserService.*(Long)) 匹配 UserService 中返回值为 User、参数为 Long 的 public 方法
execution(* com.foo.service..*.*(..)) 匹配 com.foo.service 包及其子包下所有类的所有方法
execution(* *..*Service.*(..)) 匹配所有以 Service 结尾的类的所有方法
(2)其他常用标识符
标识符 语法示例 含义
within within(com.foo.service.*) 匹配指定包下的所有方法(比 execution 更简洁,不精确到方法)
bean bean(*Service) 匹配所有以 Service 结尾的 Bean 的所有方法(按 Bean 名称筛选)
@annotation @annotation(com.foo.annotation.Log) 匹配标注了 @Log 注解的所有方法(按注解筛选)
args args(Long, ..) 匹配第一个参数为 Long 类型、后续任意参数的方法(按参数类型筛选)

组合使用:通过 &&/||/! 组合多个表达式,如:

// 匹配 UserService 中标注了 @Log 注解的方法
@Pointcut("bean(userService) && @annotation(com.foo.annotation.Log)")
public void userServiceLogPointcut() {}

注意事项:无法被 AOP 代理的情况

Spring AOP 的动态代理有明确的限制,以下方法无法被增强,需避免踩坑:

代理方式 无法代理的方法类型 原因分析
JDK 动态代理 1. 非 public 修饰的方法(如 private、protected) 2. static 修饰的方法 JDK 代理基于接口,仅代理接口的 public 方法;static 方法属于类,不被接口继承
CGLIB 动态代理 1. private 修饰的方法 2. static 修饰的方法 3. final 修饰的方法 / 类 CGLIB 基于继承,private 方法无法重写;static 方法属于类;final 类无法继承,final 方法无法重写

避坑建议

  1. 业务方法尽量用 public 修饰;
  2. 避免在 static/final 方法中编写需要 AOP 增强的逻辑;
  3. 若需代理 private 方法,可通过 “包装方法” 间接实现(如 public 方法调用 private 方法,代理 public 方法)。

总结:Spring AOP 的核心价值与应用场景

Spring AOP 通过 “横切逻辑抽取 + 动态代理织入”,解决了传统开发中横切关注点的冗余与混乱问题,其核心价值体现在:

  1. 解耦:横切逻辑与业务逻辑分离,修改日志、事务时无需改动业务代码;
  2. 复用:横切逻辑一次定义,可织入到多个模块的方法中;
  3. 灵活:通过切点表达式精确控制增强范围,支持多种通知类型;
  4. 低侵入:无需修改业务类代码,仅通过配置或注解即可实现增强。

典型应用场景

  • 日志记录:方法执行前后记录请求参数、返回值、执行时间;
  • 事务管理:方法执行前开启事务,执行成功提交,异常回滚(Spring 声明式事务基于 AOP);
  • 权限控制:方法执行前校验用户权限,无权限则抛出异常;
  • 异常处理:统一捕获方法异常,记录错误日志并返回标准化响应;
  • 缓存控制:方法执行前查询缓存,存在则返回缓存值,不存在则执行方法并缓存结果