第二篇:Spring AOP——动态代理与切面编程的底层原理
前言
在上一篇文章中,我们拆解了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时
publicclassOrderService{publicvoidcreateOrder(Orderorder){log.info("开始创建订单");// ← 日志代码checkPermission("order:create");// ← 权限代码transaction.begin();// ← 事务代码try{orderMapper.insert(order);// ← 核心业务transaction.commit();}catch(Exceptione){transaction.rollback();throwe;}log.info("订单创建完成");// ← 日志代码}publicvoidcancelOrder(LongorderId){log.info("开始取消订单");// ← 同样的日志代码checkPermission("order:cancel");// ← 同样的权限代码transaction.begin();// ← 同样的事务代码try{orderMapper.updateStatus(orderId,"CANCELLED");transaction.commit();}catch(Exceptione){transaction.rollback();throwe;}log.info("订单取消完成");// ← 同样的日志代码}}问题:日志、权限、事务这些代码在每个方法中反复出现,和核心业务逻辑混在一起。修改日志格式需要改几十个方法,漏掉一个就出现不一致。业务代码被这些"辅助代码"撑得臃肿,核心逻辑反而不突出。
1.2 有AOP时
publicclassOrderService{// 只有核心业务,干净、简洁@RequirePermission("order:create")@TransactionalpublicvoidcreateOrder(Orderorder){orderMapper.insert(order);}@RequirePermission("order:cancel")@TransactionalpublicvoidcancelOrder(LongorderId){orderMapper.updateStatus(orderId,"CANCELLED");}}// 日志、权限、事务的逻辑都抽离到各自的切面中@AspectpublicclassLogAspect{@Around("execution(* com.example.service.*.*(..))")publicObjectlog(ProceedingJoinPointpjp)throwsThrowable{log.info("开始执行: "+pjp.getSignature().getName());Objectresult=pjp.proceed();// 执行原方法log.info("执行完成: "+pjp.getSignature().getName());returnresult;}}AOP把横切关注点从业务代码中剥离出去——业务代码只做业务,日志、权限、事务这些"辅助功能"由各自独立的切面统一处理。新增一个Service方法时不需要写一行重复的日志或权限代码。
二、AOP的底层实现——动态代理
疑问:Spring AOP是怎么做到"在方法前后自动插入逻辑"的?
回答:通过动态代理——在运行时动态生成一个代理对象包装目标对象,在代理对象中插入增强逻辑,再调用目标对象。
Spring AOP采用两种动态代理方式:JDK动态代理和CGLIB。
2.1 JDK动态代理
JDK动态代理要求目标对象必须实现一个接口。代理对象和被代理对象实现同一个接口,代理对象在方法调用前后插入增强逻辑。
publicinterfaceOrderService{voidcreateOrder(Orderorder);}publicclassOrderServiceImplimplementsOrderService{publicvoidcreateOrder(Orderorder){orderMapper.insert(order);}}// JDK动态代理(简化原理)publicclassJdkProxyimplementsInvocationHandler{privateObjecttarget;// 被代理的真实对象publicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{log.info("开始执行: "+method.getName());// 前置增强Objectresult=method.invoke(target,args);// 调用真实方法log.info("执行完成: "+method.getName());// 后置增强returnresult;}}核心:Proxy.newProxyInstance()在运行时动态生成一个实现目标接口的代理类。调用代理对象的任何方法时,都会经过invoke方法——AOP的增强逻辑就是在这里插入的。
2.2 CGLIB
CGLIB不要求目标对象实现接口。它通过继承目标类生成一个子类,在子类中重写父类方法,在重写的方法中插入增强逻辑。
// CGLIB代理(简化原理)publicclassCglibProxyimplementsMethodInterceptor{privateObjecttarget;publicObjectintercept(Objectobj,Methodmethod,Object[]args,MethodProxyproxy)throwsThrowable{log.info("开始执行: "+method.getName());// 前置增强Objectresult=method.invoke(target,args);// 调用父类方法log.info("执行完成: "+method.getName());// 后置增强returnresult;}}核心: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 五种通知类型
@AspectpublicclassLogAspect{// @Before:目标方法执行前@Before("execution(* com.example.service.*.*(..))")publicvoidbefore(JoinPointjoinPoint){log.info("开始执行: "+joinPoint.getSignature().getName());}// @After:目标方法执行后(无论成功还是异常)@After("execution(* com.example.service.*.*(..))")publicvoidafter(JoinPointjoinPoint){log.info("执行结束: "+joinPoint.getSignature().getName());}// @AfterReturning:目标方法执行成功返回后@AfterReturning(value="execution(* com.example.service.*.*(..))",returning="result")publicvoidafterReturning(JoinPointjoinPoint,Objectresult){log.info("执行成功,返回: "+result);}// @AfterThrowing:目标方法抛出异常后@AfterThrowing(value="execution(* com.example.service.*.*(..))",throwing="ex")publicvoidafterThrowing(JoinPointjoinPoint,Exceptionex){log.error("执行异常: "+ex.getMessage());}// @Around:目标方法前后都执行(最强大的通知,包裹整个方法调用)@Around("execution(* com.example.service.*.*(..))")publicObjectaround(ProceedingJoinPointpjp)throwsThrowable{log.info("开始执行: "+pjp.getSignature().getName());Objectresult=pjp.proceed();// 手动调用目标方法log.info("执行完成: "+pjp.getSignature().getName());returnresult;}}| 通知类型 | 执行时机 | 能否获取返回值 | 能否捕获异常 |
|---|---|---|---|
| @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)publicclassLogAspect{@Around("...")publicObjectaround(ProceedingJoinPointpjp)throwsThrowable{log.info("[Log] 前置");Objectresult=pjp.proceed();log.info("[Log] 后置");returnresult;}}@Aspect@Order(2)publicclassTransactionAspect{@Around("...")publicObjectaround(ProceedingJoinPointpjp)throwsThrowable{transaction.begin();log.info("[Tx] 前置");Objectresult=pjp.proceed();log.info("[Tx] 后置");transaction.commit();returnresult;}}// 执行结果(@Order值小的先执行前置):// [Log] 前置// [Tx] 前置// ... 目标方法执行 ...// [Tx] 后置// [Log] 后置// 切面嵌套关系:// @Order(1) 日志// @Order(2) 事务// 目标方法4.2 一个实际例子——权限和事务的顺序
@Aspect@Order(1)// 最先执行:先检查权限publicclassPermissionAspect{...}@Aspect@Order(2)// 中间:开启事务publicclassTransactionAspect{...}@Aspect@Order(3)// 最后:记录日志publicclassLogAspect{...}执行流程:
Log 前置 → Tx 前置 → Permission 前置 → 目标方法 → Permission 后置 → Tx 后置 → Log 后置 最外层日志包裹所有操作,中间层事务包裹业务执行,最内层权限先通过验证才能进入业务。五、AOP在项目中的实战应用
5.1 权限控制——方法级鉴权
// 自定义注解@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public@interfaceRequirePermission{Stringvalue();// 权限码}// 切面实现@Aspect@ComponentpublicclassPermissionAspect{@Around("@annotation(requirePermission)")publicObjectcheckPermission(ProceedingJoinPointpjp,RequirePermissionrequirePermission)throwsThrowable{// 获取当前用户UsercurrentUser=SecurityContextHolder.getContext().getUser();// 获取用户权限集合(从Redis或数据库)Set<String>permissions=permissionService.getPermissions(currentUser.getUserId());// 检查是否拥有所需权限if(!permissions.contains(requirePermission.value())){thrownewAccessDeniedException("权限不足: "+requirePermission.value());}// 权限通过,继续执行returnpjp.proceed();}}// 业务代码中使用@RequirePermission("user:delete")publicvoiddeleteUser(LonguserId){userMapper.deleteById(userId);}面试可以这样讲:“项目中的权限控制就是用AOP实现的。自定义了@RequirePermission注解,写了切面在方法执行前拦截,从Redis获取当前用户的权限集合并比对。如果权限不足,抛异常被全局异常处理器捕获返回统一错误响应。”
5.2 事务管理——@Transactional
@Transactional是Spring AOP最经典的应用。Spring在TransactionInterceptor中实现事务的开启、提交、回滚逻辑,通过AOP包装被@Transactional注解的方法。
// 简化的事务拦截器逻辑@Around("@annotation(transactional)")publicObjectmanageTransaction(ProceedingJoinPointpjp,Transactionaltransactional)throwsThrowable{TransactionStatusstatus=transactionManager.begin();try{Objectresult=pjp.proceed();// 执行业务方法transactionManager.commit(status);// 成功则提交returnresult;}catch(Exceptione){transactionManager.rollback(status);// 异常则回滚throwe;}}六、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各自的职责,以及拦截器和过滤器的区别。
