第二篇:Spring AOP——动态代理与切面编程的底层原理
- 第一篇:Spring IoC容器——控制反转的本质与Bean的生命周期
- 第二篇:Spring AOP——动态代理与切面编程的底层原理
- 第三篇:SpringMVC——一个HTTP请求在Spring中经历了什么?
- 第四篇:SpringBoot自动配置——约定大于配置的底层原理
- 第五篇:Spring事务管理——@Transactional的底层实现与失效场景
- 第六篇:Spring用了哪些设计模式?——从单例到代理,拆解框架中的经典设计
- 第七篇:Spring扩展点——如何优雅地介入Bean的创建流程
- 第八篇:Spring与微服务——从SpringBoot到SpringCloud的演进
前言
在上一篇文章中,我们拆解了Spring IoC容器——它解决了对象创建和依赖管理的问题。但有一个场景是IoC管不了的:横切关注点。
比如你写了一个Service,需要在每个方法执行前打印日志、校验权限、开启事务。如果把这些代码硬写进每个方法里,它们会像藤蔓一样缠绕在核心业务逻辑上。AOP就是用来解决这个问题的——把这些"非核心但必须做"的事从业务代码中剥离出去,放到一个单独的地方统一管理。
面试中,AOP是Spring的第二大基石:
“JDK动态代理和CGLIB有什么区别?Spring默认用哪个?”
“AOP在项目中怎么用的?事务、日志、权限校验分别是怎么实现的?”
“多个切面同时作用于一个方法时,执行顺序是怎样的?”
本文从代理模式出发,拆解Spring AOP的底层原理,最终落到项目中的实战应用。
本文核心问题:
- AOP解决了什么问题?OOP为什么做不到?
- JDK动态代理和CGLIB有什么区别?Spring默认用哪个?
- Spring AOP的切面、切点、通知、连接点分别是什么?
- 多个切面同时作用于一个方法时,执行顺序是怎样的?为什么说"先入后出"?
- AOP在项目中用在哪些场景?事务、权限校验分别是如何实现的?
- Spring AOP和AspectJ有什么区别?
读完本文,你将对Spring AOP拥有从底层原理到项目实战的完整理解。
一、AOP解决了什么问题?
疑问:OOP(面向对象编程)已经很成熟了,为什么还需要AOP?
回答:OOP擅长纵向拆分功能模块,但处理不了"横切"在多个模块中的公共行为。AOP专门解决这类横切关注点——把分散的重复代码抽离到一个地方统一管理。
1.1 没有AOP时
public class OrderService {
public void createOrder(Order order) {
log.info("开始创建订单"); // ← 日志代码
checkPermission("order:create"); // ← 权限代码
transaction.begin(); // ← 事务代码
try {
orderMapper.insert(order); // ← 核心业务
transaction.commit();
} catch (Exception e) {
transaction.rollback();
throw e;
}
log.info("订单创建完成"); // ← 日志代码
}
public void cancelOrder(Long orderId) {
log.info("开始取消订单"); // ← 同样的日志代码
checkPermission("order:cancel"); // ← 同样的权限代码
transaction.begin(); // ← 同样的事务代码
try {
orderMapper.updateStatus(orderId, "CANCELLED");
transaction.commit();
} catch (Exception e) {
transaction.rollback();
throw e;
}
log.info("订单取消完成"); // ← 同样的日志代码
}
}
问题:日志、权限、事务这些代码在每个方法中反复出现,和核心业务逻辑混在一起。修改日志格式需要改几十个方法,漏掉一个就出现不一致。业务代码被这些"辅助代码"撑得臃肿,核心逻辑反而不突出。
1.2 有AOP时
public class OrderService {
// 只有核心业务,干净、简洁
@RequirePermission("order:create")
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order);
}
@RequirePermission("order:cancel")
@Transactional
public void cancelOrder(Long orderId) {
orderMapper.updateStatus(orderId, "CANCELLED");
}
}
// 日志、权限、事务的逻辑都抽离到各自的切面中
@Aspect
public class LogAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object log(ProceedingJoinPoint pjp) throws Throwable {
log.info("开始执行: " + pjp.getSignature().getName());
Object result = pjp.proceed(); // 执行原方法
log.info("执行完成: " + pjp.getSignature().getName());
return result;
}
}
AOP把横切关注点从业务代码中剥离出去——业务代码只做业务,日志、权限、事务这些"辅助功能"由各自独立的切面统一处理。新增一个Service方法时不需要写一行重复的日志或权限代码。
二、AOP的底层实现——动态代理
疑问:Spring AOP是怎么做到"在方法前后自动插入逻辑"的?
回答:通过动态代理——在运行时动态生成一个代理对象包装目标对象,在代理对象中插入增强逻辑,再调用目标对象。
Spring AOP采用两种动态代理方式:JDK动态代理和CGLIB。
2.1 JDK动态代理
JDK动态代理要求目标对象必须实现一个接口。代理对象和被代理对象实现同一个接口,代理对象在方法调用前后插入增强逻辑。
public interface OrderService {
void createOrder(Order order);
}
public class OrderServiceImpl implements OrderService {
public void createOrder(Order order) {
orderMapper.insert(order);
}
}
// JDK动态代理(简化原理)
public class JdkProxy implements InvocationHandler {
private Object target; // 被代理的真实对象
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log.info("开始执行: " + method.getName()); // 前置增强
Object result = method.invoke(target, args); // 调用真实方法
log.info("执行完成: " + method.getName()); // 后置增强
return result;
}
}
核心:Proxy.newProxyInstance() 在运行时动态生成一个实现目标接口的代理类。调用代理对象的任何方法时,都会经过 invoke 方法——AOP的增强逻辑就是在这里插入的。
2.2 CGLIB
CGLIB不要求目标对象实现接口。它通过继承目标类生成一个子类,在子类中重写父类方法,在重写的方法中插入增强逻辑。
// CGLIB代理(简化原理)
public class CglibProxy implements MethodInterceptor {
private Object target;
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
log.info("开始执行: " + method.getName()); // 前置增强
Object result = method.invoke(target, args); // 调用父类方法
log.info("执行完成: " + method.getName()); // 后置增强
return result;
}
}
核心:CGLIB通过ASM字节码技术,在运行时动态生成目标类的子类。子类重写父类的所有非final方法,在重写的方法中先调用AOP拦截逻辑,再调用父类方法。
2.3 JDK动态代理 vs CGLIB
| 维度 | JDK动态代理 | CGLIB |
|---|---|---|
| 要求 | 必须实现接口 | 不要求接口,但不能是final类 |
| 代理方式 | 实现同一接口 | 生成子类继承目标类 |
| 性能 | 创建代理对象快,调用略有反射开销 | 创建慢(需生成字节码),调用快 |
| 限制 | 只能代理接口中定义的方法 | 无法代理final方法和static方法 |
2.4 Spring的代理选择策略
Spring Boot 2.x 默认策略:
AOP默认用CGLIB(即使目标对象实现了接口)
为什么?
CGLIB不需要接口,使用更简单
避免因接口缺失导致的代理失败
开发者不需要额外关心接口设计
也可以通过配置改回JDK动态代理:
spring.aop.proxy-target-class=false
面试常问:如果你的Bean实现了接口,Spring一定会用JDK动态代理吗?答案是——Spring Boot 2.x及之后默认用CGLIB,不看你有没有接口。如果想要JDK动态代理,需要显式配置。
三、AOP的核心概念——切面、切点、通知、连接点
疑问:面试总问"切面和切点的区别",怎么回答?
回答:四个概念各司其职——连接点是"潜在的目标",切点是"筛选规则",通知是"要执行的动作",切面是"规则+动作的组合体"。
3.1 四个核心概念
| 概念 | 通俗理解 | 代码示例 |
|---|---|---|
| 连接点 | 所有可能被增强的方法(潜在目标) | Service中的所有方法 |
| 切点 | 筛选哪些连接点需要被增强的表达式 | execution(* com.example.service.*.*(..)) |
| 通知 | 在切点的什么时机执行什么操作 | @Before、@After、@Around注解的方法 |
| 切面 | 切点 + 通知的组合体(筛选规则+执行动作) | @Aspect修饰的类 |
3.2 五种通知类型
@Aspect
public class LogAspect {
// @Before:目标方法执行前
@Before("execution(* com.example.service.*.*(..))")
public void before(JoinPoint joinPoint) {
log.info("开始执行: " + joinPoint.getSignature().getName());
}
// @After:目标方法执行后(无论成功还是异常)
@After("execution(* com.example.service.*.*(..))")
public void after(JoinPoint joinPoint) {
log.info("执行结束: " + joinPoint.getSignature().getName());
}
// @AfterReturning:目标方法执行成功返回后
@AfterReturning(value = "execution(* com.example.service.*.*(..))",
returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
log.info("执行成功,返回: " + result);
}
// @AfterThrowing:目标方法抛出异常后
@AfterThrowing(value = "execution(* com.example.service.*.*(..))",
throwing = "ex")
public void afterThrowing(JoinPoint joinPoint, Exception ex) {
log.error("执行异常: " + ex.getMessage());
}
// @Around:目标方法前后都执行(最强大的通知,包裹整个方法调用)
@Around("execution(* com.example.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
log.info("开始执行: " + pjp.getSignature().getName());
Object result = pjp.proceed(); // 手动调用目标方法
log.info("执行完成: " + pjp.getSignature().getName());
return result;
}
}
| 通知类型 | 执行时机 | 能否获取返回值 | 能否捕获异常 |
|---|---|---|---|
| @Before | 目标方法执行前 | 否 | 否 |
| @After | 目标方法执行后(类似finally) | 否 | 否 |
| @AfterReturning | 目标方法成功返回后 | 能(通过returning属性) | 否 |
| @AfterThrowing | 目标方法抛出异常后 | 否 | 能(通过throwing属性) |
| @Around | 包裹目标方法,前后都可执行 | 能(直接拿到返回值) | 能(try-catch) |
3.3 @Around和其他通知的执行顺序
同一个切面中,不同通知的执行顺序:
@Around 的前置部分
→ @Before
→ 目标方法执行
→ @AfterReturning(成功时)/ @AfterThrowing(异常时)
← @After(finally)
← @Around 的后置部分
四、多个切面的执行顺序——为什么"先入后出"?
疑问:两个切面同时作用于一个方法,谁先执行?
回答:多个切面按@Order注解的值从小到大顺序执行前置通知,后置通知逆序执行——“先入后出”。
4.1 两个切面的执行顺序
@Aspect
@Order(1)
public class LogAspect {
@Around("...")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
log.info("[Log] 前置");
Object result = pjp.proceed();
log.info("[Log] 后置");
return result;
}
}
@Aspect
@Order(2)
public class TransactionAspect {
@Around("...")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
transaction.begin();
log.info("[Tx] 前置");
Object result = pjp.proceed();
log.info("[Tx] 后置");
transaction.commit();
return result;
}
}
// 执行结果(@Order值小的先执行前置):
// [Log] 前置
// [Tx] 前置
// ... 目标方法执行 ...
// [Tx] 后置
// [Log] 后置
// 切面嵌套关系:
// @Order(1) 日志
// @Order(2) 事务
// 目标方法
4.2 一个实际例子——权限和事务的顺序
@Aspect
@Order(1) // 最先执行:先检查权限
public class PermissionAspect { ... }
@Aspect
@Order(2) // 中间:开启事务
public class TransactionAspect { ... }
@Aspect
@Order(3) // 最后:记录日志
public class LogAspect { ... }
执行流程:
Log 前置 → Tx 前置 → Permission 前置 → 目标方法 → Permission 后置 → Tx 后置 → Log 后置
最外层日志包裹所有操作,中间层事务包裹业务执行,最内层权限先通过验证才能进入业务。
五、AOP在项目中的实战应用
5.1 权限控制——方法级鉴权
// 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequirePermission {
String value(); // 权限码
}
// 切面实现
@Aspect
@Component
public class PermissionAspect {
@Around("@annotation(requirePermission)")
public Object checkPermission(ProceedingJoinPoint pjp,
RequirePermission requirePermission) throws Throwable {
// 获取当前用户
User currentUser = SecurityContextHolder.getContext().getUser();
// 获取用户权限集合(从Redis或数据库)
Set<String> permissions = permissionService.getPermissions(currentUser.getUserId());
// 检查是否拥有所需权限
if (!permissions.contains(requirePermission.value())) {
throw new AccessDeniedException("权限不足: " + requirePermission.value());
}
// 权限通过,继续执行
return pjp.proceed();
}
}
// 业务代码中使用
@RequirePermission("user:delete")
public void deleteUser(Long userId) {
userMapper.deleteById(userId);
}
面试可以这样讲:“项目中的权限控制就是用AOP实现的。自定义了@RequirePermission注解,写了切面在方法执行前拦截,从Redis获取当前用户的权限集合并比对。如果权限不足,抛异常被全局异常处理器捕获返回统一错误响应。”
5.2 事务管理——@Transactional
@Transactional是Spring AOP最经典的应用。Spring在TransactionInterceptor中实现事务的开启、提交、回滚逻辑,通过AOP包装被@Transactional注解的方法。
// 简化的事务拦截器逻辑
@Around("@annotation(transactional)")
public Object manageTransaction(ProceedingJoinPoint pjp,
Transactional transactional) throws Throwable {
TransactionStatus status = transactionManager.begin();
try {
Object result = pjp.proceed(); // 执行业务方法
transactionManager.commit(status); // 成功则提交
return result;
} catch (Exception e) {
transactionManager.rollback(status); // 异常则回滚
throw e;
}
}
六、Spring AOP vs AspectJ
| 维度 | Spring AOP | AspectJ |
|---|---|---|
| 实现方式 | 动态代理(运行时生成代理) | 编译时/类加载时字节码织入 |
| 性能 | 略有代理开销 | 接近原生调用 |
| 适用范围 | 仅Spring管理的Bean | 任何Java对象 |
| 使用复杂度 | 简单,配合Spring生态 | 需要额外编译器,配置较多 |
| 默认选择 | Spring Boot项目默认 | 需要显式集成 |
Spring Boot默认使用Spring AOP,因为动态代理已经满足绝大多数场景,不需要引入额外的编译工具链。
七、面试中这样回答
面试官:“AOP在项目中怎么用的?”
回答框架:
“项目中最典型的两处AOP应用。一是权限控制——用自定义注解@RequirePermission声明需要的权限,切面在方法执行前拦截,从Redis获取用户权限集合做比对。二是事务管理——@Transactional就是Spring基于AOP实现的事务拦截,切面在方法执行前后负责开启和提交/回滚事务。另外日志打印也是通过切面统一处理的,不散落在各个业务方法中。”
面试官:“JDK动态代理和CGLIB有什么区别?”
回答:
“JDK动态代理要求目标对象必须实现接口,代理对象和目标对象实现同一个接口——生成的代理对象是接口的实现者。CGLIB是通过继承目标类生成子类,在子类中重写父类方法并插入增强逻辑,不要求接口但不能是final类。Spring Boot 2.x及之后默认用CGLIB,无论目标对象是否实现接口。JDK代理创建快但有反射开销,CGLIB创建慢但调用快。”
总结
- AOP解决横切关注点——将分散在多个方法中的日志、权限、事务抽离到统一的切面中管理,业务代码只保留核心逻辑
- JDK动态代理要求接口,生成接口实现者;CGLIB通过继承生成子类,不要求接口但不能代理final类。Spring Boot默认用CGLIB
- 切面 = 切点(筛选规则)+ 通知(执行动作):@Before在前,@After在后,@Around包裹整个调用。@Around是最强大的通知类型
- 多切面按@Order值从小到大执行前置,后置逆序——形成"先入后出"的嵌套结构
- 项目中AOP的两个实际应用:权限注解的拦截校验、@Transactional的事务管理。两个都是通过@Around环绕通知实现的
- Spring AOP基于动态代理实现,依赖容器的Bean管理;AspectJ通过编译时字节码织入,范围更广但需要额外配置。两者各自对应不同的场景和复杂度需求
下一篇预告:Spring原理(三)——SpringMVC:一个HTTP请求在Spring中经历了什么?拆解DispatcherServlet的请求处理全链路,HandlerMapping、HandlerAdapter、ViewResolver各自的职责,以及拦截器和过滤器的区别。
更多推荐




所有评论(0)