尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Spring AOP环绕通知实战:从日志切面到分布式锁的完整指南

Spring AOP环绕通知实战:从日志切面到分布式锁的完整指南 做一个接口日志需求时我一开始是根本没想到用AOP的。领导只丢了一句话把每个接口的入参、出参、耗时都记下来。我第一反应是这还不简单Controller里每个方法加几行日志不就完了。结果加了三十多个方法之后发现问题越来越多异常分支的日志漏记、耗时统计被日志代码自身污染、参数里带着敏感信息误打出来、同事一加方法就忘了写日志。后来被逼无奈翻出了Spring AOP的环绕通知才发现一个Around注解就可以把这堆破事一次性解决。这篇东西我不打算写成官方文档式的教程。我更想站在已经在项目里跑过这套东西的角度把环绕通知的核心机制、完整实现、容易踩的坑以及几个真正能提升开发效率的进阶玩法一次性讲清楚。适合刚接触Spring AOP、听说过Around但不知道怎么用的同学也适合已经在用但被自调用失效切面不执行这类问题卡住的人。1. 为什么绕了一圈最后还是回到环绕通知1.1 一个日志埋点需求逼出来的技术选型先说说我当时的处境。项目是个预约服务类的业务系统Controller层密密麻麻几十个接口。我要给每个接口记录操作日志、执行耗时和异常情况。一开始我的方案很粗暴GetMapping(/order/{id}) public ResultOrderVO getOrder(PathVariable Long id) { long start System.currentTimeMillis(); log.info(查询订单开始参数{}, id); try { OrderVO order orderService.getOrder(id); log.info(查询订单结束耗时{}ms, System.currentTimeMillis() - start); return Result.success(order); } catch (Exception e) { log.error(查询订单异常, e); throw e; } }这个方法看起来完整但致命问题在于它是人肉埋点。项目里有几十个接口每个都复制粘贴这套逻辑代码被日志代码侵蚀得不成样子。更难受的是新来的同事未必记得这套规范漏记、误记、格式不统一的事情每天都有。你需要的是把记录日志这个横切逻辑从业务方法里抽离出来让业务方法只管自己的业务日志、耗时、异常捕获交给一个统一的地方处理——这正是AOP的典型场景。1.2 五种通知方式里只有Around是全能选手Spring AOP提供了五种通知类型Before、AfterReturning、AfterThrowing、After和Around。把它们摆在一起对比各自的局限就非常明显通知类型能拿参数能拿返回值能捕获异常能控制方法是否执行Before能不能不能不能AfterReturning能能不能不能AfterThrowing能不能能不能After能不能不能不能Around能能能能我在做日志切面时需要的四件事记录入参、拿到返回结果、捕获异常、统计耗时。Before只能解决入参记录返回结果得等AfterReturning异常得单独写AfterThrowing耗时统计又必须把开始时间和结束时间串起来。如果全用这些散装通知一个需求的逻辑会被切成四五个方法互相之间还要靠ThreadLocal传递变量维护成本反而上去了。Around不一样。它把方法执行整体包了起来proceed()调用之前的事、调用之后的事、异常处理全都可以写在一个方法里。这也让我后来意识到环绕通知本质上是手动实现整个拦截器链其他四种通知能力它都有只是需要自己控制调用时机。所以在做横切逻辑的时候如果拿不准该用哪种通知直接上Around基本不会错。2. 环绕通知的内核proceed()就是那个放行开关2.1 动态代理与切面织入的机制很多人用AOP的时候只知道在方法上加个注解就能生效但不知道为什么生效。要想真正理解Around得先理解Spring AOP的底层是动态代理。Spring Boot容器在启动时会对被切点匹配到的Bean创建代理对象。如果你的Bean实现了接口默认走JDK动态代理生成一个实现同样接口的代理类如果没实现接口则走CGLIB生成目标类的子类代理对象。你在任何地方注入的其实都是这个代理对象而不是原始Bean。调用方法时请求会先进入代理对象内部的拦截器链切面逻辑就在这个链条上执行。Around是这条链上包得最靠外的一层它有权决定要不要继续往下走、用什么参数往下走、把什么结果返回给调用方。这个过程可以类比成安检口目标方法是候车厅里的乘客切面是安检员proceed()就是放行闸机。安检员可以检查乘客的行李参数、记录时间耗时、决定放不放行还能在乘客出来后再补一刀结果加工。2.2 proceed()的三种形态无参、带参、返回值Around方法接收到的是一个ProceedingJoinPoint这是JoinPoint的子接口多了一个proceed()方法。这个方法是整个环绕通知的核心它有几种常见用法。最基本的形态是无参proceed()直接继续执行目标方法并把目标方法的返回值原样返回Around(execution(* com.example.service.*.*(..))) public Object around(ProceedingJoinPoint pjp) throws Throwable { // 方法执行前的逻辑 Object result pjp.proceed(); // 方法执行后的逻辑 return result; }注意这里proceed()返回的是Object你在切面方法里最终return什么调用方拿到的就是什么。这就给结果加工留下了空间。第二种形态是带参proceed(Object[] args)。执行前你可以在pjp.getArgs()拿到的参数数组上做手脚然后通过proceed(args)把修改后的参数传给目标方法。这在做参数校验、参数解密、参数脱敏时非常有用。Around(execution(* com.example.service.*.*(..))) public Object around(ProceedingJoinPoint pjp) throws Throwable { Object[] args pjp.getArgs(); for (int i 0; i args.length; i) { if (args[i] instanceof String) { args[i] ((String) args[i]).trim(); } } return pjp.proceed(args); }第三种形态最容易忽略proceed()可能抛出异常。所以环绕通知方法签名里必须有throws Throwable否则编译都过不了。这个Throwable是目标方法抛出的异常你可以在环绕通知里用try/catch捕获它决定是吞掉、包装还是继续上抛。2.3 为什么ProceedingJoinPoint只能在Around中使用刚刚说过ProceedingJoinPoint是JoinPoint的子接口它独有的就是proceed()。这个设计是有讲究的。proceed()意味着放行目标方法而Before、After这些通知是被动触发的它们没有权限也没有能力决定目标方法是否执行所以Spring只给Around注入了ProceedingJoinPoint。其他通知方法拿到的是普通的JoinPoint只有getArgs()、getSignature()、getTarget()这些只读能力。这个约束我一开始觉得多余后来才想明白如果把proceed()给到After或AfterReturning逻辑上就乱了。方法都已经执行完了再调用proceed()是要干什么所以Spring设计这个类型区分本质上是强迫你在正确的阶段处理横切逻辑。也正因为这样Around成为唯一能手动控制整个调用流程的通知方式灵活性的代价是你必须自己处理好try/finally否则很容易出现异常吞掉、连接不释放这类问题。3. 从零落地一个环绕通知完整实现与逐行拆解3.1 依赖与切面骨架Spring Boot项目里引入AOP非常简单只要加一个起步依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-aop/artifactId /dependency这个依赖会自动帮我们注册AnnotationAwareAspectJAutoProxyCreatorSpring容器就能识别Aspect注解的类并创建代理。接下来定义一个切面类Aspect Component Slf4j public class ApiLogAspect { Pointcut(execution(public * com.example.demo.controller..*.*(..))) public void controllerPointcut() { } Around(controllerPointcut()) public Object logAround(ProceedingJoinPoint pjp) throws Throwable { // 核心逻辑 } }Aspect告诉Spring这是一个切面类Component把它注册成BeanPointcut定义拦截规则Around绑定切点并编写环绕逻辑。这里有个容易被忽略的细节切点方法本身不需要任何实现它只是一个锚点Around(controllerPointcut())就是通过方法名引用这个锚点。execution(public * com.example.demo.controller..*.*(..))这个表达式展开理解是匹配com.example.demo.controller包及其子包下所有类的所有public方法方法参数不限返回值类型不限。其中..表示包及其子包*.*是任意类名.任意方法名(..)是任意参数列表。3.2 接口日志切面的完整代码我最终沉淀下来的日志切面长这样Aspect Component Slf4j public class ApiLogAspect { Pointcut(execution(public * com.example.demo.controller..*.*(..))) public void controllerPointcut() { } Around(controllerPointcut()) public Object logAround(ProceedingJoinPoint pjp) throws Throwable { long start System.currentTimeMillis(); Signature signature pjp.getSignature(); String methodName signature.getDeclaringTypeName() . signature.getName(); Object[] args pjp.getArgs(); log.info(接口调用开始, method{}, args{}, methodName, Arrays.toString(args)); Object result; try { result pjp.proceed(); log.info(接口调用完成, method{}, cost{}ms, result{}, methodName, System.currentTimeMillis() - start, result); return result; } catch (Exception e) { log.error(接口调用异常, method{}, cost{}ms, error{}, methodName, System.currentTimeMillis() - start, e.getMessage(), e); throw e; } } }这段代码已经把日志需求完整实现了入参记录、返回记录、耗时统计、异常捕获。核心逻辑都包在try/catch里pjp.proceed()是唯一必须的调用。有两点我要重点强调第一catch块里必须throw e。你可以在日志里记录异常但绝对不能把异常吞掉。一旦吞掉业务方的try/catch就感知不到错误事务也可能会异常提交这是非常危险的。如果确实想吞异常那就要在业务设计上明确这个异常是可控的、可以被忽略的。第二方法级日志不要打印超长内容。我最早的时候把整张订单表、整份JSON响应都打进日志一次调用产生几MB日志文件排查问题反而更困难。后来统一加了长度限制超过2000个字符就截断日志量立刻降下来了。3.3 拦截带参方法与参数修改实际业务里光记录参数是不够的你可能想在方法执行前把参数统一清洗一遍。比如所有入参里的字符串都去掉首尾空格或者把敏感字段脱敏后再传给目标方法。这时候就要用到pjp.getArgs()加pjp.proceed(args)。Around(controllerPointcut()) public Object sanitizeArgs(ProceedingJoinPoint pjp) throws Throwable { Object[] args pjp.getArgs(); for (int i 0; i args.length; i) { if (args[i] instanceof String) { args[i] ((String) args[i]).trim(); } else if (args[i] instanceof UserDTO) { UserDTO user (UserDTO) args[i]; user.setIdCard(maskIdCard(user.getIdCard())); } } return pjp.proceed(args); }注意一个关键点修改参数数组后必须通过pjp.proceed(args)把新数组传进去而不是修改完以后继续调用无参的proceed()。proceed()无参版本等价于把原始参数原样传入你改的数组不会生效。这个方法也有一个容易翻车的地方参数数组里的对象是同一个引用如果你改的是对象的内部属性即使不通过proceed(args)目标方法看到的也是被改过的对象。但如果你做的是替换操作比如把args[i]指向一个新对象那就必须用proceed(args)传新数组。这个区别不搞清楚很容易出现参数明明改了目标方法收到的还是旧值的诡异问题。3.4 统一封装返回结果的写法很多前后端分离项目要求所有接口统一返回ResultT结构。如果在每个Controller方法里手动包一层代码会非常啰嗦。用环绕通知可以做统一包装但前提是目标方法本身返回的是裸数据。Around(execution(* com.example.demo.controller..*.*(..))) public Object wrapResult(ProceedingJoinPoint pjp) throws Throwable { Object result pjp.proceed(); if (result instanceof Result) { // 目标方法已经返回统一结构了不要再包一层 return result; } ResultObject success new Result(); success.setCode(0); success.setMsg(success); success.setData(result); return success; }这里最容易被忽略的是判断结果是否已经被包装。如果切面拦截了所有Controller方法而某个方法本来就返回Result你再包一层就变成ResultResultT前端解析直接炸掉。我见过不止一次这种事故所以这个判断必须写。另外要注意如果Controller方法返回类型是ResponseEntityT前面的返回值类型判断需要额外处理ResponseEntity的情况。这类统一包装逻辑我建议只用在项目从一开始就确定所有接口都用统一返回结构的场景老项目半路接入很容易踩到返回类型不一致的坑。4. 多通知与多切面协作顺序和异常的真相4.1 同一个切面内五种通知的执行顺序环绕通知虽然强大但项目中很少只有一个切面。有时候日志切面、权限切面、事务切面、缓存切面会同时作用于同一个方法。这个时候理解通知执行顺序就非常重要。先看最基础的场景同一个切面类中同时定义了多个通知都作用于同一个切点。Aspect Component Slf4j public class OrderAspect { Around(execution(* com.example.service.OrderService.*(..))) public Object around(ProceedingJoinPoint pjp) throws Throwable { log.info(around before); Object result pjp.proceed(); log.info(around after); return result; } Before(execution(* com.example.service.OrderService.*(..))) public void before() { log.info(before); } AfterReturning(execution(* com.example.service.OrderService.*(..))) public void afterReturning() { log.info(afterReturning); } After(execution(* com.example.service.OrderService.*(..))) public void after() { log.info(after); } }正常返回时的执行顺序是around beforebefore目标方法执行afterReturningafteraround after如果目标方法抛出异常顺序是around beforebefore目标方法抛出异常afterThrowingafter异常继续向外抛这里的本质是Around的proceed()调用进入Spring的拦截器链链内依次执行Before、目标方法、AfterReturning或AfterThrowing、After。当proceed()返回后Around的after逻辑才执行。所以**Around是所有通知类型中外层包裹的那一个**其他通知的优先级由Spring内部排序决定你不需要手工控制。4.2 异常在环绕通知里的传播路径异常处理这一块环绕通知和其他通知之间的交互是特别容易出问题的。看这个场景Around(pointcut()) public Object around(ProceedingJoinPoint pjp) throws Throwable { try { return pjp.proceed(); } catch (Exception e) { log.error(捕获到异常: {}, e.getMessage()); // 注意这里没有重新抛出 return null; } }如果环绕通知里catch住了异常并且没有再次抛出会发生两件事目标方法抛出的异常被吞掉了调用方拿到的是null同时容器内的AfterThrowing通知永远不会触发因为Spring认为异常已经被处理了。反过来如果你在catch里重新throw e异常会继续向外传播AfterThrowing才会在proceed()抛出异常后触发。所以在设计环绕通知时要明确自己是想处理异常并恢复正常流程还是记录异常并继续上抛。前者要非常谨慎因为一旦吞掉异常事务回滚往往也不会发生——这也是很多人用AOP做异常处理时会遇到事务不生效的原因之一。我个人的建议是环绕通知里除非有明确的异常恢复方案否则一律catch之后记录日志再throw。把异常处理交给AfterThrowing或全局异常处理器避免自己动手处理时把事务和调用链搞乱。4.3 多切面嵌套与Order控制当多个切面同时作用于同一个方法时切面之间存在嵌套关系。默认情况下多个切面的执行顺序是不确定的所以需要显式控制。Order注解就是干这个的数字越小越先执行。Aspect Component Order(1) public class LogAspect { // 日志切面 } Aspect Component Order(2) public class PermissionAspect { // 权限切面 }当Order(1)的切面和Order(2)的切面同时作用于同一个方法时执行顺序是LogAspect的Aroundbefore部分PermissionAspect的Aroundbefore部分目标方法PermissionAspect的Aroundafter部分LogAspect的Aroundafter部分也就是说数字越小的切面越靠外。这对权限控制场景非常重要你肯定希望权限校验在日志之前执行如果权限校验失败日志里根本不应出现接口调用开始。所以权限切面的Order应该比日志切面更小。反过来如果某个切面需要记录请求是否被权限拦截那就把日志切面的Order调大一点。实际项目里我一般会约定权限和幂等这类前置保护切面用Order(1)~Order(10)日志和监控这类观察型切面放后面。把顺序规则定义清楚多切面协作时就不会乱成一锅粥。5. 环绕通知失效的常见场景与排查链路5.1 自调用最经典的AOP失效陷阱这个坑我觉得90%的Spring开发都踩过。看一个例子Service public class OrderService { public void createOrder() { // 业务逻辑 this.cancelOrder(); // 自己调自己 } public void cancelOrder() { // 这个方法被切面拦截了但实际不会生效 } }如果cancelOrder()上挂了环绕通知希望做日志或权限校验从createOrder()里调用this.cancelOrder()时切面根本不会执行。原因很简单Spring容器里注入的是OrderService的代理对象但this指向的是当前对象本身也就是原始Bean。AOP代理只对外部调用生效内部方法之间通过this互调不会经过代理对象。解决方案有三种注入自身代理对象Service public class OrderService { Autowired private OrderService self; public void createOrder() { self.cancelOrder(); // 通过代理对象调用切面生效 } }使用AopContext.currentProxy()但需要配置EnableAspectJAutoProxy(exposeProxy true)。把被调方法拆到另一个独立的Service类里通过注入这个新类的Bean来调用。这也是我比较推荐的方式结构上更清晰也能避免循环依赖问题。为了排查这类问题我一般会在环绕通知里先打印一条日志如果调了方法但日志没出来优先怀疑是不是自调用。5.2 切点表达式写错拦不住也不知道为什么第二个高频坑是切点表达式写错。Spring AOP切点表达式的细节非常多最常见的错误是包名路径写错或者..符号用错。比如// 错误只匹配 controller 包下的类不匹配子包 execution(* com.example.controller.*.*(..)) // 正确匹配 controller 包及其子包 execution(* com.example.controller..*.*(..))这个单点和双点的区别在文本上看着不大效果却差很远。我再举个例子// 错误第三段 * 匹配的是包名而不是类名 execution(* com.example.*.service.*(..)) // 正确匹配 com.example 包下所有子包中的 service 类 execution(* com.example..service.*(..))排查这类问题我有一个笨办法先不要用Around用Before加打印日志测试切点。如果Before触发了说明切点没问题如果没触发说明切点表达式或代理对象有问题这时候再去检查类上有没有Service、方法是不是public、Bean有没有被代理。把问题范围一步步缩小效率高很多。5.3 返回值类型不匹配动态代理的隐形约束环绕通知还有一个容易被忽略的约束方法的返回值类型必须和目标方法兼容。如果你在切面方法里返回了一个不兼容的类型强转时会抛ClassCastException或者代理层直接报错。举个例子目标方法返回UserVOpublic UserVO getUser(Long id) { ... }环绕通知如果把返回值改成了StringAround(pointcut()) public Object around(ProceedingJoinPoint pjp) throws Throwable { Object result pjp.proceed(); return 伪造的结果; // 类型不兼容 }单看Around方法的Object返回值编译没问题但运行到代理调用层时Spring会检查返回值是否匹配目标方法的签名直接抛异常。尤其是JDK动态代理场景下代理类的方法签名已经由接口决定了强转不兼容的类型必然报错。所以做统一返回包装时前面代码里那个instanceof判断特别重要。返回包装结构时也要确保ResultT是Controller方法声明的返回类型否则一样会翻车。5.4 Spring Boot 2.6循环依赖带来的新问题Spring Boot 2.6开始默认禁止循环依赖启动时如果检测到Bean循环引用会直接报错。这事跟AOP有什么关系关系大了。AOP会生成代理对象有时候会延迟Bean的初始化本来2.5之前还能跑的项目到了2.6就启动失败。这是因为代理对象在创建的时候可能又要去注入另一个Bean两个Bean互相等待。如果你在项目里遇到了这样的报错The dependencies of some of the beans in the application context form a cycle:先别急着改业务代码可以这样排查看是不是真的业务上绕不开的循环依赖。如果是代理互相引用想办法把其中一个Bean拆开或者用Lazy延迟注入打破循环。如果确实是为AOP引入的循环依赖比如A切面里注入了BB又依赖A可以考虑把切面类里的依赖改为ObjectProviderT或者Lazy。老项目短期迁移时可以设置spring.main.allow-circular-referencestrue临时放开但不建议长期依赖这个开关它掩盖了设计问题。我做过一个老项目从2.1升到2.6的改造AOP加的代理导致三处循环依赖最后都是通过Lazy解决的。这个方案的好处是不改业务结构代价是首次访问时可能多一次代理创建性能上可接受。6. 环绕通知的进阶玩法不止日志和监控6.1 用环绕通知做接口幂等预约类业务系统里用户手滑点了两次提交是非常常见的场景。单选按钮明明已经提交成功用户又快速点了第二次如果后端不做幂等就会生成两笔订单。用环绕通知配合Redis可以做一个非常简洁的幂等切面。先定义一个注解Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface Idempotent { String key() default ; }然后在需要幂等的方法上加注解。切面负责生成唯一key、判断Redis里是否已存在、存在则直接返回重复请求。Aspect Component public class IdempotentAspect { Autowired private StringRedisTemplate redisTemplate; Around(annotation(idempotent)) public Object idempotentAround(ProceedingJoinPoint pjp, Idempotent idempotent) throws Throwable { // key可以由业务ID 方法名拼接 String key buildKey(pjp, idempotent.key()); Boolean firstCall redisTemplate .opsForValue() .setIfAbsent(key, 1, Duration.ofSeconds(10)); if (firstCall null || !firstCall) { throw new RuntimeException(重复请求请勿重复提交); } try { return pjp.proceed(); } finally { // 注意成功的请求是否需要删除key看业务需要 // redisTemplate.delete(key); } } }这个方案里annotation(idempotent)这个切点表达式可以直接把注解对象注入到切面方法参数中拿到注解上的自定义属性。这个写法的好处是业务方只需要加一行注解幂等逻辑完全收敛在切面里。需要留意的是Redis key的过期时间太短挡不住快速重复点击太长又会误伤正常的重复请求这个值要根据业务节奏调。6.2 用环绕通知处理MyBatis字段加密查询你的热搜词里有个很典型的问题Spring Boot MyBatis实现数据库字段级加密了怎么做查询。很多团队会采用在DAO层手动加密解密的笨办法但更优雅的方式是用环绕通知拦截Mapper接口的方法在查询返回后对加密字段统一解密。假设你在实体类里有这样一些需要加密的字段public class UserDO { private Long id; CryptField private String idCard; // 身份证号数据库里存的是密文 private String name; }在Mapper层的切面里处理Aspect Component public class CryptFieldAspect { Around(execution(* com.example.demo.mapper..*.*(..))) public Object decryptResult(ProceedingJoinPoint pjp) throws Throwable { Object result pjp.proceed(); decryptFields(result); return result; } private void decryptFields(Object obj) { if (obj instanceof List) { for (Object item : (List?) obj) { decryptFields(item); } } else if (obj ! null) { // 反射遍历字段找到带CryptField的字段并解密 } } }这样设计的好处是业务层和Mapper层完全无感知DBA看库里的数据是密文业务代码操作的是明文。缺点是反射遍历集合会有一定性能开销不过对绝大多数业务系统来说完全可控。在这个场景里环绕通知的pjp.proceed()返回值就是Mapper方法的查询结果在返回之前统一做解密。如果以后要支持按加密字段查询只需要在入参方向也加一个切面方法把明文转密文后再传入proceed(args)加解密逻辑就全部收敛在切面里了。6.3 用环绕通知封装分布式锁最后一个进阶玩法是用环绕通知封装分布式锁。分布式锁的经典写法是获取锁 → try业务 → finally释放锁。如果每个方法都手写这套模板代码会非常冗余而且很容易出现finally里忘了释放锁的bug。用自定义注解加环绕通知可以把这段逻辑收敛起来Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface DistributedLock { String key(); long waitTime() default 3; long leaseTime() default 10; }切面实现Aspect Component public class DistributedLockAspect { Autowired private RedissonClient redissonClient; Around(annotation(lock)) public Object lockAround(ProceedingJoinPoint pjp, DistributedLock lock) throws Throwable { String lockKey lock.key(); RLock rLock redissonClient.getLock(lockKey); boolean acquired rLock.tryLock(lock.waitTime(), lock.leaseTime(), TimeUnit.SECONDS); if (!acquired) { throw new RuntimeException(系统繁忙请稍后重试); } try { return pjp.proceed(); } finally { rLock.unlock(); } } }我在实际项目里用这个方案处理过库存扣减、优惠券发放、订单状态流转这些高并发场景。环绕通知的优势体现在异常安全上finally块保证了不管业务方法是否抛异常锁都会释放不会出现锁死的问题。这段逻辑如果用AfterThrowing和After两个通知拆开写反而容易忽略异常路径这也是我为什么强调能用Around解决的就别用散装通知。说回整个环绕通知的使用体感。Spring AOP的环绕通知看起来只是几个注解的事但真正把它用好需要你对代理机制、通知执行顺序、异常传播路径和Spring容器初始化都有清晰的认识。我在项目里用顺手之后日志、幂等、加解密、分布式锁、权限校验都开始向切面逻辑收敛Controller层最终回归到只做参数绑定和业务编排代码清爽程度提升了不少。最后再分享一个我自己的小习惯给切面加上Order注释并在类上写明依赖顺序。这种团队约定的注释看起来不起眼但在多人协作时能避免许多我加的切面为什么没生效的争论。环绕通知是Spring AOP里最灵活、也最强大的一把刀用得好它是架构利器用不好就是隐藏的雷。希望上面这些思路能帮你少踩几个我不小心踩过的坑。
返回列表