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) | 被切面织入的对象(即业务逻辑对象) | OrderService、UserService 等业务类实例 |
| 代理对象(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 类。
核心原理:
- 代理类实现目标类的所有接口;
- 代理类的所有方法都会委托给
InvocationHandler.invoke()方法; - 在
invoke()方法中,先执行切面逻辑(如日志),再调用目标对象的原方法。
简化示例(模拟 Spring JDK 代理):
// 1. 目标接口
public interface UserService {
User getUserById(Long id);
}
// 2. 目标类(实现接口)
public class UserServiceImpl implements UserService {
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;
}
// 代理方法:所有接口方法调用都会进入这里
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,否则无法继承)。
核心原理:
- CGLIB 生成目标类的子类(代理类);
- 重写目标类的非
final方法,在重写方法中织入切面逻辑; - 调用代理类的方法时,先执行切面逻辑,再调用父类(目标类)的原方法。
简化示例(模拟 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 {
// 拦截方法:所有代理类的方法调用都会进入这里
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/>:<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:// 启用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):指定切面优先级,数字越小优先级越高(多个切面时控制执行顺序)
public class LoggingAspect {
// -------------------------- 1. 定义切点(复用切点表达式) --------------------------
// @Pointcut:定义切点,expression是切点表达式
public void servicePointcut() {} // 空方法,仅作为切点标识
// -------------------------- 2. 定义通知(增强逻辑) --------------------------
/**
* 前置通知:方法执行前执行
* @param joinPoint:连接点对象,可获取方法名、参数等信息
*/
// 引用上面定义的切点
public void beforeAdvice(JoinPoint joinPoint) {
// 获取方法名
String methodName = joinPoint.getSignature().getName();
// 获取方法参数
Object[] args = joinPoint.getArgs();
System.out.printf("【前置通知】方法 %s 开始执行,参数:%s%n", methodName, Arrays.toString(args));
}
/**
* 后置通知:方法执行后执行(无论是否异常,都会执行)
*/
public void afterAdvice(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.printf("【后置通知】方法 %s 执行结束%n", methodName);
}
/**
* 返回通知:方法正常执行完成后执行(异常时不执行)
* @param result:方法返回值,需通过returning参数指定变量名(与方法参数名一致)
*/
public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.printf("【返回通知】方法 %s 执行成功,返回值:%s%n", methodName, result);
}
/**
* 异常通知:方法抛出异常时执行(正常时不执行)
* @param exception:方法抛出的异常,需通过throwing参数指定变量名(与方法参数名一致)
*/
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()执行目标方法
*/
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 层业务类(被切面织入)
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 注解的方法
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 方法无法重写 |
避坑建议:
- 业务方法尽量用
public修饰; - 避免在
static/final方法中编写需要 AOP 增强的逻辑; - 若需代理 private 方法,可通过 “包装方法” 间接实现(如 public 方法调用 private 方法,代理 public 方法)。
总结:Spring AOP 的核心价值与应用场景
Spring AOP 通过 “横切逻辑抽取 + 动态代理织入”,解决了传统开发中横切关注点的冗余与混乱问题,其核心价值体现在:
- 解耦:横切逻辑与业务逻辑分离,修改日志、事务时无需改动业务代码;
- 复用:横切逻辑一次定义,可织入到多个模块的方法中;
- 灵活:通过切点表达式精确控制增强范围,支持多种通知类型;
- 低侵入:无需修改业务类代码,仅通过配置或注解即可实现增强。
典型应用场景:
- 日志记录:方法执行前后记录请求参数、返回值、执行时间;
- 事务管理:方法执行前开启事务,执行成功提交,异常回滚(Spring 声明式事务基于 AOP);
- 权限控制:方法执行前校验用户权限,无权限则抛出异常;
- 异常处理:统一捕获方法异常,记录错误日志并返回标准化响应;
- 缓存控制:方法执行前查询缓存,存在则返回缓存值,不存在则执行方法并缓存结果