AOP实战这块建议去读一下美团线上使用AOP的日志实战非常经典纯正干货值得借鉴。如果想完整知道AOP的一些知识和应用场景以及坑或者想在面试中拿到高分的那也可以看下我写的这篇内容相对比较完整的。好我们开始吧。Transactional加在方法上事务自动生效。Async加在方法上异步执行。Cacheable加在方法上结果自动缓存。这些注解背后都是同一套机制Spring AOP的代理和拦截器链。面试里AOP的考察一般分三个层面概念层切面、通知、切入点是什么原理层代理对象怎么创建的、拦截器链怎么执行的、JDK代理和CGLIB怎么选的实战层你在项目中用AOP做过什么。前两层大部分人能答上一些第三层往往只说一句「做过日志切面」就没了面试官想听的是具体的设计思路以及实战。下面的内容会把这三个层面都过一遍从AOP要解决的问题讲起到源码级的实现原理再到两个可以直接用在项目里的实战方案。横切关注点AOP要解决什么问题看一个典型的业务方法public void transfer(String fromAccount, String toAccount, BigDecimal amount) { // 权限检查 if (!permissionService.check(fromAccount)) { throw new BusinessException(无权操作); } // 记录日志 log.info(转账开始, from{}, to{}, amount{}, fromAccount, toAccount, amount); // 事务管理 TransactionStatus status txManager.getTransaction(def); try { accountDao.deduct(fromAccount, amount); accountDao.increase(toAccount, amount); txManager.commit(status); } catch (Exception e) { txManager.rollback(status); throw e; } log.info(转账完成, from{}, to{}, fromAccount, toAccount); }整个方法里真正处理业务的只有两行扣减转出方余额和增加转入方余额。权限检查、日志记录、事务管理占了绝大部分代码。这些逻辑在支付方法、退款方法、充值方法里又各出现了一遍代码几乎一样。这类跟具体业务无关但在每个业务方法里都要出现的逻辑有个名字叫横切关注点。它们横跨在多个业务模块之上不属于某一个特定的业务。横切关注点概念图AOP的思路是把横切关注点从业务代码中抽出来放到独立的模块里维护在运行时自动织入到目标方法的前后。抽取之后业务方法只剩核心逻辑Transactional public void transfer(String fromAccount, String toAccount, BigDecimal amount) { accountDao.deduct(fromAccount, amount); accountDao.increase(toAccount, amount); }事务、日志、权限这些逻辑各自定义在独立的切面中框架在运行时自动把它们应用到目标方法上。业务代码干净了横切逻辑也集中到一个地方统一维护。用小区门卫理解代理机制Spring AOP是基于代理实现的理解了代理机制后面所有的概念和问题都能自然串联起来。可以把Spring AOP想成小区门卫的模式。小区里住着各种住户对应你写的各种Service类。外来访客想找某个住户办事不能直接走进小区必须先到门卫那里登记。门卫检查来访者的身份证件确认没问题后放行。访客离开时门卫登记离开时间。如果来访者身份可疑门卫可以直接拒绝进入。映射到Spring AOP住户 你写的Service类目标对象门卫 Spring在运行时生成的代理对象外来访客通过门卫进小区 其他Bean调用你的方法时调到的是代理对象门卫检查证件后放行 Before通知目标方法执行前运行门卫登记离开时间 After通知目标方法执行后运行门卫拒绝来访者进入 Around通知可以控制目标方法是否执行多个门卫有值班顺序 Order注解控制多个切面的优先级这个比喻还能解释Spring AOP中最经典的一个坑小区里住户之间互相串门是不需要经过门卫的。对应到代码里同一个类内部用this调用自己的方法这个调用不经过代理切面逻辑不会生效。后面会详细讲这个问题。门卫只管小区大门管不了住户家里发生什么。Spring AOP也一样只能拦截Spring Bean的方法调用不能拦截字段访问、构造方法、静态方法。这是Spring AOP和AspectJ的核心区别。代理机制示意图Transactional的AOP实现Transactional是AOP在Spring里最典型的应用。追踪一下它的底层实现能帮助理解AOP的整个工作链路。Spring的事务配置类ProxyTransactionManagementConfiguration注册了三个关键的BeanTransactionAttributeSource负责识别哪些方法标了Transactional以及注解上配了什么属性。TransactionInterceptor是真正干活的拦截器它实现了MethodInterceptor接口会被插入到AOP的拦截器链里。BeanFactoryTransactionAttributeSourceAdvisor是Advisor负责把切入点哪些方法需要事务管理和通知TransactionInterceptor绑在一起。当代理对象拦截到一个标了Transactional的方法调用时TransactionInterceptor的invoke方法被执行。它从MethodInvocation中取出目标类信息然后委托给父类TransactionAspectSupport的invokeWithinTransaction方法处理。核心流程省略了非关键分支// 创建事务 TransactionInfo txInfo createTransactionIfNecessary(ptm, txAttr, joinpointIdentification); Object retVal; try { // 这里是一个环绕通知调用拦截器链中的下一个拦截器 // 最终会执行到目标方法 retVal invocation.proceedWithInvocation(); } catch (Throwable ex) { // 异常处理根据回滚规则决定回滚还是提交 completeTransactionAfterThrowing(txInfo, ex); throw ex; } finally { cleanupTransactionInfo(txInfo); } // 正常返回提交事务 commitTransactionAfterReturning(txInfo); return retVal;源码注释里有这么一句This is an around advice。Transactional用的就是环绕通知的模式。TransactionInterceptor在目标方法执行前开启事务在正常返回后提交事务在捕获到异常后根据rollbackFor规则决定回滚还是提交。Transactional调用链流程图理解了这个链路Transactional很多看似奇怪的行为就有了合理的解释内部调用不生效this.methodB()不走代理TransactionInterceptor根本没有机会执行默认只回滚运行时异常completeTransactionAfterThrowing里的判断逻辑默认配置下只有RuntimeException和Error会触发回滚受检异常不会protected方法在Spring 6.0之前不生效AnnotationTransactionAttributeSource默认只扫描public方法Spring 6.0开始放宽了这个限制构造参数传false表示接受非public方法理解了代理机制Transactional、Async、Cacheable这些注解的行为和限制就不再是需要背的条目而是能从同一个机制推导出来的结论。代理对象的创建过程代理对象是在Bean初始化阶段创建的。Spring Boot引入spring-boot-starter-aop后AopAutoConfiguration自动生效默认启用CGLIB代理。它会注册一个叫AnnotationAwareAspectJAutoProxyCreator的Bean后处理器在每个Bean初始化完成后介入。这个后处理器的核心逻辑在AbstractAutoProxyCreator的wrapIfNecessary方法里把容器中所有的Advisor和Aspect类解析出来和当前Bean的方法做切入点匹配。匹配上了就创建代理对象替换原始Bean没匹配上就原样返回。代理创建完成后其他Bean通过Autowired注入时拿到的已经是代理对象了。拦截器链的执行机制代理对象拦截到方法调用后具体发生了什么以JDK动态代理为例JdkDynamicAopProxy实现了InvocationHandler接口所有方法调用都会进入它的invoke方法。invoke方法的核心逻辑是先获取当前方法对应的拦截器链如果链为空直接用反射调用目标方法这是一个性能优化。如果有匹配的拦截器构造一个ReflectiveMethodInvocation对象把代理、目标、方法、参数和拦截器链全部封装进去然后调用proceed()启动整个链的执行。proceed()的实现是一个递归调用的过程public Object proceed() throws Throwable { // 所有拦截器都执行完了调用目标方法 if (this.currentInterceptorIndex this.interceptorsAndDynamicMethodMatchers.size() - 1) { return invokeJoinpoint(); } // 取出下一个拦截器 Object interceptorOrInterceptionAdvice this.interceptorsAndDynamicMethodMatchers.get( this.currentInterceptorIndex); // 执行拦截器拦截器内部会再次调用proceed() return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this); }currentInterceptorIndex从-1开始每次proceed()先递增索引取出当前拦截器并执行。拦截器内部在执行完自己的前置逻辑后调用proceed()把控制权交给链上的下一个拦截器。所有拦截器执行完毕后调用invokeJoinpoint()执行目标方法本身。返回值沿着调用栈逆向传回每个拦截器都有机会对返回值做后置处理。用一个具体场景串联一下。假设AccountService的transfer方法同时被日志切面和事务切面拦截拦截器链里有两个拦截器。调用过程是这样的代理.transfer() → 日志拦截器记录开始时间 → proceed() → 事务拦截器开启事务 → proceed() → 目标方法执行 → 返回结果 → 事务拦截器提交事务 → 返回结果 → 日志拦截器计算耗时并记录 → 返回最终结果拦截器链执行流程图这种递归调用结构保证了每个拦截器都能在目标方法执行前后插入自己的逻辑而且拦截器之间彼此透明不需要知道链上还有哪些其他拦截器。JDK动态代理和CGLIBSpring AOP有两种代理实现方式。代理类型的选择逻辑在DefaultAopProxyFactory的createAopProxy方法里public AopProxy createAopProxy(AdvisedSupport config) { if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { Class? targetClass config.getTargetClass(); if (targetClass.isInterface() || Proxy.isProxyClass(targetClass) || ClassUtils.isLambdaClass(targetClass)) { return new JdkDynamicAopProxy(config); } return new ObjenesisCglibAopProxy(config); } else { return new JdkDynamicAopProxy(config); } }proxyTargetClass为true或者目标类没有实现用户指定的接口时选择CGLIB。如果目标类本身是接口、已经是JDK代理类或者是Lambda类即使proxyTargetClass为true也会退回到JDK动态代理。两种代理方式的区别对比维度JDK动态代理CGLIB代理实现方式基于接口用Proxy.newProxyInstance创建代理类基于继承在运行时生成目标类的子类是否需要接口需要目标类必须实现接口不需要final类和final方法不受影响代理的是接口无法代理子类无法重写Spring Boot默认否是proxyTargetClasstrueSpring Boot从2.0开始把proxyTargetClass的默认值改成了true。这是一个有实际背景的决定早期很多项目的Service类没有接口定义用JDK动态代理会因为缺少接口而导致代理创建失败。统一默认CGLIB后不管目标类有没有接口都能正常代理减少了开发者踩坑的概率。在现代JVM上两种代理方式的性能差异已经很小不是选择的主要考量因素。绝大多数Spring Boot项目直接用默认的CGLIB就好。Spring AOP和AspectJ面试里经常被问到Spring AOP和AspectJ的区别。这两个是不同的AOP实现方案设计目标和适用场景各有不同。Spring AOP是运行时代理方案。它在Bean初始化阶段创建代理对象通过代理拦截方法调用。回到门卫的比喻门卫只管小区大门住户家里发生什么他管不了。Spring AOP只能拦截Spring Bean的方法执行不支持字段访问、构造方法、静态方法。AspectJ是编译期/加载期字节码织入方案。它直接修改目标类的字节码把切面逻辑编织到目标代码中。不需要代理对象因为目标类本身的字节码已经被修改了。它支持方法执行、字段访问、构造方法、对象初始化等多种连接点类型。对比维度Spring AOPAspectJ织入方式运行时生成代理对象编译期或加载期修改字节码支持的连接点方法执行方法执行、字段访问、构造方法、静态方法等是否需要特殊工具不需要需要ajc编译器或加载期织入Agent是否依赖Spring容器是只能作用于Spring Bean否可以作用于任意Java对象内部调用是否生效不生效代理的限制生效字节码已被修改运行时性能开销有代理调用的开销无额外开销织入在编译期完成Spring AOP选择代理方案而不是字节码织入是一个有意的设计取舍。代理方案和Spring容器的集成更自然不需要额外的编译器插件开发体验更简单。对绝大多数项目来说方法级别的拦截完全够用。需要AspectJ的场景确实存在需要拦截非Spring Bean的对象、需要拦截字段访问或构造方法、对性能有极致要求编译期织入没有运行时代理开销。实际项目中也可以两者混用Spring AOP处理日常的方法拦截个别特殊需求用AspectJ补充。AOP实战两个高频场景面试被问到「你在项目中用AOP做过什么」如果只答事务管理面试官不会满意因为那是框架自带的不算你的设计。下面两个场景是实际项目中最常见的自定义切面。场景一接口耗时日志每个对外接口都需要记录调用日志谁调的、参数是什么、执行了多长时间、成功还是失败。如果在每个Controller方法里手写log代码重复且容易遗漏。用自定义注解切面来做方法上标一个注解即可。先定义注解Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface ApiLog { // 接口描述用于日志标识 String value() default ; }切面实现Aspect Component public class ApiLogAspect { private static final Logger log LoggerFactory.getLogger(ApiLogAspect.class); Around(annotation(apiLog)) public Object around(ProceedingJoinPoint point, ApiLog apiLog) throws Throwable { String methodName point.getSignature().toShortString(); String desc apiLog.value().isEmpty() ? methodName : apiLog.value(); long start System.currentTimeMillis(); try { Object result point.proceed(); log.info({} 执行成功, 耗时{}ms, desc, System.currentTimeMillis() - start); return result; } catch (Throwable ex) { log.error({} 执行异常, 耗时{}ms, 异常{}, desc, System.currentTimeMillis() - start, ex.getMessage()); throw ex; } } }使用时在方法上加一行注解ApiLog(转账) Transactional public void transfer(String fromAccount, String toAccount, BigDecimal amount) { accountDao.deduct(fromAccount, amount); accountDao.increase(toAccount, amount); }这个切面的设计有几个值得注意的点。用Around而不是BeforeAfter是因为需要计算方法的执行耗时必须在同一个通知里拿到开始和结束时间。切入点用annotation(apiLog)绑定注解参数可以直接读取注解上的描述信息。异常捕获后必须重新throw不能吞掉异常否则会影响上层的Transactional等切面的正常工作。如果需要记录入参和返回值可以通过point.getArgs()获取参数对result做JSON序列化。生产环境建议对参数做脱敏处理避免日志泄露敏感信息。美团技术团队在操作日志这个场景上做了更深入的实践。他们的方案是自定义LogRecord注解结合SpEL表达式实现动态日志模板通过AOP拦截器在方法执行前后解析模板并记录日志。感兴趣可以看美团技术博客这篇文章https://tech.meituan.com/2021/09/16/operational-logbook.html场景二接口权限校验业务系统里不同的接口需要不同的权限比如转账需要「account:transfer」权限查询余额需要「account:query」权限。把权限校验逻辑分散在每个方法里维护成本高且容易漏。用自定义注解切面集中处理。定义权限注解Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequirePermission { // 需要的权限标识 String value(); }切面实现Aspect Component public class PermissionAspect { Autowired private PermissionService permissionService; Before(annotation(permission)) public void check(JoinPoint point, RequirePermission permission) { String userId UserContext.getCurrentUserId(); if (!permissionService.hasPermission(userId, permission.value())) { throw new AccessDeniedException(权限不足: permission.value()); } } }使用方式RequirePermission(account:transfer) Transactional public void transfer(String fromAccount, String toAccount, BigDecimal amount) { accountDao.deduct(fromAccount, amount); accountDao.increase(toAccount, amount); }这个切面用Before而不是Around因为权限校验只需要在方法执行前做一次判断不需要包裹目标方法的执行过程。校验不通过直接抛异常方法不会被执行。如果切面之间有执行顺序要求比如权限校验要在事务开启之前执行权限校验不通过就不要开启事务可以在PermissionAspect上加Order(1)给事务切面一个更大的Order值确保权限切面先执行。阿里巴巴开源的Sentinel限流框架也是类似的思路。它提供了SentinelResource注解通过SentinelResourceAspect切面拦截方法调用在方法执行前检查是否触发了限流或熔断规则触发则抛出BlockException阻止方法执行。这和上面的权限切面是同一个模式自定义注解标记目标方法切面统一拦截处理。Sentinel的源码在GitHub上https://github.com/alibaba/Sentinel两个切面的共同模式回头看这两个实战案例它们遵循同一个设计模式自定义注解做标记切面做拦截注解参数做配置。这个模式在实际项目中非常高频。接口限流、幂等校验、分布式锁、操作审计都可以用这个模式来实现。面试时能把这个模式讲清楚并且说出你在项目中用它解决过什么问题面试官对你的AOP掌握程度就不会有疑问了。生产环境容易遇到的AOP问题同类内部调用不走代理这是AOP中被踩得最多的坑。AccountService里batchTransfer调用了this.transfer()transfer上标了Transactional。从外部调用batchTransfer时batchTransfer走了代理但内部的this.transfer()是直接调用不经过代理transfer的事务不会生效。Service public class AccountService { public void batchTransfer(ListTransferRequest requests) { for (TransferRequest req : requests) { // this调用不走代理 this.transfer(req.getFromAccount(), req.getToAccount(), req.getAmount()); } } Transactional public void transfer(String fromAccount, String toAccount, BigDecimal amount) { accountDao.deduct(fromAccount, amount); accountDao.increase(toAccount, amount); } }回到门卫的比喻住户之间互相串门不经过门卫。推荐的解决方式是把transfer挪到另一个Service里通过Autowired注入后调用这样调用会经过代理。另一种方式是在启动类上加EnableAspectJAutoProxy(exposeProxy true)然后用AopContext.currentProxy()获取代理对象来调用但这种做法会让业务代码和AOP框架产生耦合不是首选。Transactional失效的常见场景除了内部调用之外Transactional还有几个容易失效的场景方法不是public的。在Spring Boot 2.7Spring Framework 5.3及之前的版本中AnnotationTransactionAttributeSource默认只扫描public方法非public方法上的Transactional会被忽略不报错也不生效。Spring Framework 6.0放宽了这个限制CGLIB代理下protected方法也能生效。异常类型不匹配。Transactional默认只在RuntimeException和Error时回滚。如果方法抛出的是受检异常比如IOException事务不会回滚。需要显式指定Transactional(rollbackFor Exception.class)来覆盖所有异常类型。类没有被Spring管理。如果一个类没有加Service、Component这类注解或者是通过new直接创建的对象Spring不会为它创建代理Transactional自然不会生效。Async失效Async的底层也是AOP代理和Transactional的机制完全一致。内部调用时异步不会生效方法必须是public的Spring 6.0之前目标类必须是Spring Bean。排查方式和Transactional一样。多个切面的执行顺序当多个切面同时作用在一个方法上执行顺序通过Order注解或实现Ordered接口来控制。数值越小优先级越高。对于Around和Before类型的通知优先级高的先执行。对于After和AfterReturning类型的通知优先级高的后执行因为它在拦截器链的外层。不显式指定Order时执行顺序不确定。如果切面之间有依赖关系比如安全检查必须在事务开启之前执行必须用Order显式指定。切面范围过大切入点表达式写得太宽泛会有性能影响。比如execution(* com.example..*.*(..))匹配了项目下所有类的所有方法大量不需要拦截的方法也会经过拦截器链的匹配判断。虽然单次开销不大但在高并发场景下累积起来会有可观的影响。切入点表达式应该尽量精确只匹配真正需要拦截的方法。小结Spring生态里很多注解驱动的功能底层都是AOP在支撑。Transactional管理事务、Async实现异步调用、Cacheable处理缓存、Retryable实现重试它们共享同一套代理机制和拦截器链执行流程。把这套机制理解透了遇到这些注解的各种失效问题不需要去搜索引擎查答案从代理的工作原理就能推断出原因。面试时能从这个角度去回答比背条目要有说服力得多。AOP代理的设计有一个隐性的代价代码的执行流程变成了隐式的。方法上看不到任何痕迹但执行时有额外的逻辑在运行。用在基础设施层面事务、日志、监控、限流时收益大于成本因为这些逻辑本来就不该和业务耦合在一起。如果发现自己在用AOP处理业务规则比如用切面做业务校验或数据转换值得重新评估一下是不是选错了工具。
什么是面向切面编程AOP?
AOP实战这块建议去读一下美团线上使用AOP的日志实战非常经典纯正干货值得借鉴。如果想完整知道AOP的一些知识和应用场景以及坑或者想在面试中拿到高分的那也可以看下我写的这篇内容相对比较完整的。好我们开始吧。Transactional加在方法上事务自动生效。Async加在方法上异步执行。Cacheable加在方法上结果自动缓存。这些注解背后都是同一套机制Spring AOP的代理和拦截器链。面试里AOP的考察一般分三个层面概念层切面、通知、切入点是什么原理层代理对象怎么创建的、拦截器链怎么执行的、JDK代理和CGLIB怎么选的实战层你在项目中用AOP做过什么。前两层大部分人能答上一些第三层往往只说一句「做过日志切面」就没了面试官想听的是具体的设计思路以及实战。下面的内容会把这三个层面都过一遍从AOP要解决的问题讲起到源码级的实现原理再到两个可以直接用在项目里的实战方案。横切关注点AOP要解决什么问题看一个典型的业务方法public void transfer(String fromAccount, String toAccount, BigDecimal amount) { // 权限检查 if (!permissionService.check(fromAccount)) { throw new BusinessException(无权操作); } // 记录日志 log.info(转账开始, from{}, to{}, amount{}, fromAccount, toAccount, amount); // 事务管理 TransactionStatus status txManager.getTransaction(def); try { accountDao.deduct(fromAccount, amount); accountDao.increase(toAccount, amount); txManager.commit(status); } catch (Exception e) { txManager.rollback(status); throw e; } log.info(转账完成, from{}, to{}, fromAccount, toAccount); }整个方法里真正处理业务的只有两行扣减转出方余额和增加转入方余额。权限检查、日志记录、事务管理占了绝大部分代码。这些逻辑在支付方法、退款方法、充值方法里又各出现了一遍代码几乎一样。这类跟具体业务无关但在每个业务方法里都要出现的逻辑有个名字叫横切关注点。它们横跨在多个业务模块之上不属于某一个特定的业务。横切关注点概念图AOP的思路是把横切关注点从业务代码中抽出来放到独立的模块里维护在运行时自动织入到目标方法的前后。抽取之后业务方法只剩核心逻辑Transactional public void transfer(String fromAccount, String toAccount, BigDecimal amount) { accountDao.deduct(fromAccount, amount); accountDao.increase(toAccount, amount); }事务、日志、权限这些逻辑各自定义在独立的切面中框架在运行时自动把它们应用到目标方法上。业务代码干净了横切逻辑也集中到一个地方统一维护。用小区门卫理解代理机制Spring AOP是基于代理实现的理解了代理机制后面所有的概念和问题都能自然串联起来。可以把Spring AOP想成小区门卫的模式。小区里住着各种住户对应你写的各种Service类。外来访客想找某个住户办事不能直接走进小区必须先到门卫那里登记。门卫检查来访者的身份证件确认没问题后放行。访客离开时门卫登记离开时间。如果来访者身份可疑门卫可以直接拒绝进入。映射到Spring AOP住户 你写的Service类目标对象门卫 Spring在运行时生成的代理对象外来访客通过门卫进小区 其他Bean调用你的方法时调到的是代理对象门卫检查证件后放行 Before通知目标方法执行前运行门卫登记离开时间 After通知目标方法执行后运行门卫拒绝来访者进入 Around通知可以控制目标方法是否执行多个门卫有值班顺序 Order注解控制多个切面的优先级这个比喻还能解释Spring AOP中最经典的一个坑小区里住户之间互相串门是不需要经过门卫的。对应到代码里同一个类内部用this调用自己的方法这个调用不经过代理切面逻辑不会生效。后面会详细讲这个问题。门卫只管小区大门管不了住户家里发生什么。Spring AOP也一样只能拦截Spring Bean的方法调用不能拦截字段访问、构造方法、静态方法。这是Spring AOP和AspectJ的核心区别。代理机制示意图Transactional的AOP实现Transactional是AOP在Spring里最典型的应用。追踪一下它的底层实现能帮助理解AOP的整个工作链路。Spring的事务配置类ProxyTransactionManagementConfiguration注册了三个关键的BeanTransactionAttributeSource负责识别哪些方法标了Transactional以及注解上配了什么属性。TransactionInterceptor是真正干活的拦截器它实现了MethodInterceptor接口会被插入到AOP的拦截器链里。BeanFactoryTransactionAttributeSourceAdvisor是Advisor负责把切入点哪些方法需要事务管理和通知TransactionInterceptor绑在一起。当代理对象拦截到一个标了Transactional的方法调用时TransactionInterceptor的invoke方法被执行。它从MethodInvocation中取出目标类信息然后委托给父类TransactionAspectSupport的invokeWithinTransaction方法处理。核心流程省略了非关键分支// 创建事务 TransactionInfo txInfo createTransactionIfNecessary(ptm, txAttr, joinpointIdentification); Object retVal; try { // 这里是一个环绕通知调用拦截器链中的下一个拦截器 // 最终会执行到目标方法 retVal invocation.proceedWithInvocation(); } catch (Throwable ex) { // 异常处理根据回滚规则决定回滚还是提交 completeTransactionAfterThrowing(txInfo, ex); throw ex; } finally { cleanupTransactionInfo(txInfo); } // 正常返回提交事务 commitTransactionAfterReturning(txInfo); return retVal;源码注释里有这么一句This is an around advice。Transactional用的就是环绕通知的模式。TransactionInterceptor在目标方法执行前开启事务在正常返回后提交事务在捕获到异常后根据rollbackFor规则决定回滚还是提交。Transactional调用链流程图理解了这个链路Transactional很多看似奇怪的行为就有了合理的解释内部调用不生效this.methodB()不走代理TransactionInterceptor根本没有机会执行默认只回滚运行时异常completeTransactionAfterThrowing里的判断逻辑默认配置下只有RuntimeException和Error会触发回滚受检异常不会protected方法在Spring 6.0之前不生效AnnotationTransactionAttributeSource默认只扫描public方法Spring 6.0开始放宽了这个限制构造参数传false表示接受非public方法理解了代理机制Transactional、Async、Cacheable这些注解的行为和限制就不再是需要背的条目而是能从同一个机制推导出来的结论。代理对象的创建过程代理对象是在Bean初始化阶段创建的。Spring Boot引入spring-boot-starter-aop后AopAutoConfiguration自动生效默认启用CGLIB代理。它会注册一个叫AnnotationAwareAspectJAutoProxyCreator的Bean后处理器在每个Bean初始化完成后介入。这个后处理器的核心逻辑在AbstractAutoProxyCreator的wrapIfNecessary方法里把容器中所有的Advisor和Aspect类解析出来和当前Bean的方法做切入点匹配。匹配上了就创建代理对象替换原始Bean没匹配上就原样返回。代理创建完成后其他Bean通过Autowired注入时拿到的已经是代理对象了。拦截器链的执行机制代理对象拦截到方法调用后具体发生了什么以JDK动态代理为例JdkDynamicAopProxy实现了InvocationHandler接口所有方法调用都会进入它的invoke方法。invoke方法的核心逻辑是先获取当前方法对应的拦截器链如果链为空直接用反射调用目标方法这是一个性能优化。如果有匹配的拦截器构造一个ReflectiveMethodInvocation对象把代理、目标、方法、参数和拦截器链全部封装进去然后调用proceed()启动整个链的执行。proceed()的实现是一个递归调用的过程public Object proceed() throws Throwable { // 所有拦截器都执行完了调用目标方法 if (this.currentInterceptorIndex this.interceptorsAndDynamicMethodMatchers.size() - 1) { return invokeJoinpoint(); } // 取出下一个拦截器 Object interceptorOrInterceptionAdvice this.interceptorsAndDynamicMethodMatchers.get( this.currentInterceptorIndex); // 执行拦截器拦截器内部会再次调用proceed() return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this); }currentInterceptorIndex从-1开始每次proceed()先递增索引取出当前拦截器并执行。拦截器内部在执行完自己的前置逻辑后调用proceed()把控制权交给链上的下一个拦截器。所有拦截器执行完毕后调用invokeJoinpoint()执行目标方法本身。返回值沿着调用栈逆向传回每个拦截器都有机会对返回值做后置处理。用一个具体场景串联一下。假设AccountService的transfer方法同时被日志切面和事务切面拦截拦截器链里有两个拦截器。调用过程是这样的代理.transfer() → 日志拦截器记录开始时间 → proceed() → 事务拦截器开启事务 → proceed() → 目标方法执行 → 返回结果 → 事务拦截器提交事务 → 返回结果 → 日志拦截器计算耗时并记录 → 返回最终结果拦截器链执行流程图这种递归调用结构保证了每个拦截器都能在目标方法执行前后插入自己的逻辑而且拦截器之间彼此透明不需要知道链上还有哪些其他拦截器。JDK动态代理和CGLIBSpring AOP有两种代理实现方式。代理类型的选择逻辑在DefaultAopProxyFactory的createAopProxy方法里public AopProxy createAopProxy(AdvisedSupport config) { if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { Class? targetClass config.getTargetClass(); if (targetClass.isInterface() || Proxy.isProxyClass(targetClass) || ClassUtils.isLambdaClass(targetClass)) { return new JdkDynamicAopProxy(config); } return new ObjenesisCglibAopProxy(config); } else { return new JdkDynamicAopProxy(config); } }proxyTargetClass为true或者目标类没有实现用户指定的接口时选择CGLIB。如果目标类本身是接口、已经是JDK代理类或者是Lambda类即使proxyTargetClass为true也会退回到JDK动态代理。两种代理方式的区别对比维度JDK动态代理CGLIB代理实现方式基于接口用Proxy.newProxyInstance创建代理类基于继承在运行时生成目标类的子类是否需要接口需要目标类必须实现接口不需要final类和final方法不受影响代理的是接口无法代理子类无法重写Spring Boot默认否是proxyTargetClasstrueSpring Boot从2.0开始把proxyTargetClass的默认值改成了true。这是一个有实际背景的决定早期很多项目的Service类没有接口定义用JDK动态代理会因为缺少接口而导致代理创建失败。统一默认CGLIB后不管目标类有没有接口都能正常代理减少了开发者踩坑的概率。在现代JVM上两种代理方式的性能差异已经很小不是选择的主要考量因素。绝大多数Spring Boot项目直接用默认的CGLIB就好。Spring AOP和AspectJ面试里经常被问到Spring AOP和AspectJ的区别。这两个是不同的AOP实现方案设计目标和适用场景各有不同。Spring AOP是运行时代理方案。它在Bean初始化阶段创建代理对象通过代理拦截方法调用。回到门卫的比喻门卫只管小区大门住户家里发生什么他管不了。Spring AOP只能拦截Spring Bean的方法执行不支持字段访问、构造方法、静态方法。AspectJ是编译期/加载期字节码织入方案。它直接修改目标类的字节码把切面逻辑编织到目标代码中。不需要代理对象因为目标类本身的字节码已经被修改了。它支持方法执行、字段访问、构造方法、对象初始化等多种连接点类型。对比维度Spring AOPAspectJ织入方式运行时生成代理对象编译期或加载期修改字节码支持的连接点方法执行方法执行、字段访问、构造方法、静态方法等是否需要特殊工具不需要需要ajc编译器或加载期织入Agent是否依赖Spring容器是只能作用于Spring Bean否可以作用于任意Java对象内部调用是否生效不生效代理的限制生效字节码已被修改运行时性能开销有代理调用的开销无额外开销织入在编译期完成Spring AOP选择代理方案而不是字节码织入是一个有意的设计取舍。代理方案和Spring容器的集成更自然不需要额外的编译器插件开发体验更简单。对绝大多数项目来说方法级别的拦截完全够用。需要AspectJ的场景确实存在需要拦截非Spring Bean的对象、需要拦截字段访问或构造方法、对性能有极致要求编译期织入没有运行时代理开销。实际项目中也可以两者混用Spring AOP处理日常的方法拦截个别特殊需求用AspectJ补充。AOP实战两个高频场景面试被问到「你在项目中用AOP做过什么」如果只答事务管理面试官不会满意因为那是框架自带的不算你的设计。下面两个场景是实际项目中最常见的自定义切面。场景一接口耗时日志每个对外接口都需要记录调用日志谁调的、参数是什么、执行了多长时间、成功还是失败。如果在每个Controller方法里手写log代码重复且容易遗漏。用自定义注解切面来做方法上标一个注解即可。先定义注解Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface ApiLog { // 接口描述用于日志标识 String value() default ; }切面实现Aspect Component public class ApiLogAspect { private static final Logger log LoggerFactory.getLogger(ApiLogAspect.class); Around(annotation(apiLog)) public Object around(ProceedingJoinPoint point, ApiLog apiLog) throws Throwable { String methodName point.getSignature().toShortString(); String desc apiLog.value().isEmpty() ? methodName : apiLog.value(); long start System.currentTimeMillis(); try { Object result point.proceed(); log.info({} 执行成功, 耗时{}ms, desc, System.currentTimeMillis() - start); return result; } catch (Throwable ex) { log.error({} 执行异常, 耗时{}ms, 异常{}, desc, System.currentTimeMillis() - start, ex.getMessage()); throw ex; } } }使用时在方法上加一行注解ApiLog(转账) Transactional public void transfer(String fromAccount, String toAccount, BigDecimal amount) { accountDao.deduct(fromAccount, amount); accountDao.increase(toAccount, amount); }这个切面的设计有几个值得注意的点。用Around而不是BeforeAfter是因为需要计算方法的执行耗时必须在同一个通知里拿到开始和结束时间。切入点用annotation(apiLog)绑定注解参数可以直接读取注解上的描述信息。异常捕获后必须重新throw不能吞掉异常否则会影响上层的Transactional等切面的正常工作。如果需要记录入参和返回值可以通过point.getArgs()获取参数对result做JSON序列化。生产环境建议对参数做脱敏处理避免日志泄露敏感信息。美团技术团队在操作日志这个场景上做了更深入的实践。他们的方案是自定义LogRecord注解结合SpEL表达式实现动态日志模板通过AOP拦截器在方法执行前后解析模板并记录日志。感兴趣可以看美团技术博客这篇文章https://tech.meituan.com/2021/09/16/operational-logbook.html场景二接口权限校验业务系统里不同的接口需要不同的权限比如转账需要「account:transfer」权限查询余额需要「account:query」权限。把权限校验逻辑分散在每个方法里维护成本高且容易漏。用自定义注解切面集中处理。定义权限注解Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequirePermission { // 需要的权限标识 String value(); }切面实现Aspect Component public class PermissionAspect { Autowired private PermissionService permissionService; Before(annotation(permission)) public void check(JoinPoint point, RequirePermission permission) { String userId UserContext.getCurrentUserId(); if (!permissionService.hasPermission(userId, permission.value())) { throw new AccessDeniedException(权限不足: permission.value()); } } }使用方式RequirePermission(account:transfer) Transactional public void transfer(String fromAccount, String toAccount, BigDecimal amount) { accountDao.deduct(fromAccount, amount); accountDao.increase(toAccount, amount); }这个切面用Before而不是Around因为权限校验只需要在方法执行前做一次判断不需要包裹目标方法的执行过程。校验不通过直接抛异常方法不会被执行。如果切面之间有执行顺序要求比如权限校验要在事务开启之前执行权限校验不通过就不要开启事务可以在PermissionAspect上加Order(1)给事务切面一个更大的Order值确保权限切面先执行。阿里巴巴开源的Sentinel限流框架也是类似的思路。它提供了SentinelResource注解通过SentinelResourceAspect切面拦截方法调用在方法执行前检查是否触发了限流或熔断规则触发则抛出BlockException阻止方法执行。这和上面的权限切面是同一个模式自定义注解标记目标方法切面统一拦截处理。Sentinel的源码在GitHub上https://github.com/alibaba/Sentinel两个切面的共同模式回头看这两个实战案例它们遵循同一个设计模式自定义注解做标记切面做拦截注解参数做配置。这个模式在实际项目中非常高频。接口限流、幂等校验、分布式锁、操作审计都可以用这个模式来实现。面试时能把这个模式讲清楚并且说出你在项目中用它解决过什么问题面试官对你的AOP掌握程度就不会有疑问了。生产环境容易遇到的AOP问题同类内部调用不走代理这是AOP中被踩得最多的坑。AccountService里batchTransfer调用了this.transfer()transfer上标了Transactional。从外部调用batchTransfer时batchTransfer走了代理但内部的this.transfer()是直接调用不经过代理transfer的事务不会生效。Service public class AccountService { public void batchTransfer(ListTransferRequest requests) { for (TransferRequest req : requests) { // this调用不走代理 this.transfer(req.getFromAccount(), req.getToAccount(), req.getAmount()); } } Transactional public void transfer(String fromAccount, String toAccount, BigDecimal amount) { accountDao.deduct(fromAccount, amount); accountDao.increase(toAccount, amount); } }回到门卫的比喻住户之间互相串门不经过门卫。推荐的解决方式是把transfer挪到另一个Service里通过Autowired注入后调用这样调用会经过代理。另一种方式是在启动类上加EnableAspectJAutoProxy(exposeProxy true)然后用AopContext.currentProxy()获取代理对象来调用但这种做法会让业务代码和AOP框架产生耦合不是首选。Transactional失效的常见场景除了内部调用之外Transactional还有几个容易失效的场景方法不是public的。在Spring Boot 2.7Spring Framework 5.3及之前的版本中AnnotationTransactionAttributeSource默认只扫描public方法非public方法上的Transactional会被忽略不报错也不生效。Spring Framework 6.0放宽了这个限制CGLIB代理下protected方法也能生效。异常类型不匹配。Transactional默认只在RuntimeException和Error时回滚。如果方法抛出的是受检异常比如IOException事务不会回滚。需要显式指定Transactional(rollbackFor Exception.class)来覆盖所有异常类型。类没有被Spring管理。如果一个类没有加Service、Component这类注解或者是通过new直接创建的对象Spring不会为它创建代理Transactional自然不会生效。Async失效Async的底层也是AOP代理和Transactional的机制完全一致。内部调用时异步不会生效方法必须是public的Spring 6.0之前目标类必须是Spring Bean。排查方式和Transactional一样。多个切面的执行顺序当多个切面同时作用在一个方法上执行顺序通过Order注解或实现Ordered接口来控制。数值越小优先级越高。对于Around和Before类型的通知优先级高的先执行。对于After和AfterReturning类型的通知优先级高的后执行因为它在拦截器链的外层。不显式指定Order时执行顺序不确定。如果切面之间有依赖关系比如安全检查必须在事务开启之前执行必须用Order显式指定。切面范围过大切入点表达式写得太宽泛会有性能影响。比如execution(* com.example..*.*(..))匹配了项目下所有类的所有方法大量不需要拦截的方法也会经过拦截器链的匹配判断。虽然单次开销不大但在高并发场景下累积起来会有可观的影响。切入点表达式应该尽量精确只匹配真正需要拦截的方法。小结Spring生态里很多注解驱动的功能底层都是AOP在支撑。Transactional管理事务、Async实现异步调用、Cacheable处理缓存、Retryable实现重试它们共享同一套代理机制和拦截器链执行流程。把这套机制理解透了遇到这些注解的各种失效问题不需要去搜索引擎查答案从代理的工作原理就能推断出原因。面试时能从这个角度去回答比背条目要有说服力得多。AOP代理的设计有一个隐性的代价代码的执行流程变成了隐式的。方法上看不到任何痕迹但执行时有额外的逻辑在运行。用在基础设施层面事务、日志、监控、限流时收益大于成本因为这些逻辑本来就不该和业务耦合在一起。如果发现自己在用AOP处理业务规则比如用切面做业务校验或数据转换值得重新评估一下是不是选错了工具。