☰
Spring AOP动态切点:按方法参数精准控制拦截逻辑
2026/10/21 19:11:32 网站建设 项目流程

Spring AOP这个词,做Java后端的人基本都听过。但说实话,大部分人对它的理解也就停在@Aspect、@Before、@Pointcut这几个注解上,写了好几年接口,也很少有人去琢磨Pointcut本身还有静态匹配和动态匹配之分。今天聊的DynamicMethodMatcherPointcut,是AOP体系里一个偏冷门、但关键时刻能救场的类。它解决的是一个非常具体的问题:方法要不要拦截,不能只看方法签名,还得看方法被调用时传进来的参数值。这听起来不复杂,实际落地时坑不少,但只要理解了它的运行机制,就能把很多原本要写在业务代码里的if-else逻辑,干干净净地收敛到切面里。

这篇文章适合谁?对Spring AOP有一定认知、但没深入研究过Pointcut机制的开发者;以及正在做审计日志、权限控制、敏感操作拦截这类需求的团队。我会从机制原理讲到Spring Boot中的可运行示例,再附上踩坑记录和排查思路,力求看完就能直接照着落地。

1. 动态匹配和静态匹配的本质区别

1.1 静态匹配的局限在哪里

平时用的最多的@Pointcut表达式,比如execution(* com.demo.service.*.*(..))或者@annotation(com.demo.annotation.AuditLog),本质上都是静态匹配。所谓静态,就是在Spring创建代理对象的阶段,根据方法签名、方法上的注解、所属类的类型等这些“编译期就固定不变”的信息,一次性决定某个方法要不要被拦截。这个决策结果会被缓存下来,之后每次调用都不需要再计算。

这种模式在大多数场景下没问题。比如“凡是带@AuditLog注解的方法都要做操作日志”,这种规则和参数无关,业务方法写好了,注解标好了,行为就完全确定了。

但总有规则长在参数上。举个例子,假设一个转账方法transfer(String targetAccount, BigDecimal amount, String operator),业务要求是:只有转账金额超过一万,或者目标账户属于重点监控名单时,才需要额外记录审计日志。如果硬用静态切点,只能把transfer方法整体拦截,然后在Advice里自己写if判断当前参数是否属于敏感操作。这样Java代码里就会散落一堆和业务流程无关的判断逻辑,而且规则一变就得改代码。

静态匹配的另一个局限是:方法本身可能被多处调用,有的调用场景需要拦截,有的调用场景不想拦截,但方法签名完全一样。这时候静态匹配根本区分不了,只能全拦或者全不拦。

1.2 动态匹配的运行机制

动态匹配就是为了解决“参数参与决策”的问题。它的核心思路是:在方法调用发生时,AOP框架把当时的实际参数值透传到匹配器里,由匹配器根据参数内容返回建议,返回true就执行通知,返回false就放行。

Spring AOP对MethodMatcher接口定义了两个match方法:

public interface MethodMatcher { boolean matches(Method method, Class<?> targetClass); boolean matches(Method method, Class<?> targetClass, Object... args); boolean isRuntime(); }

其中matches(Method, Class)是静态匹配,matches(Method, Class, Object...)是动态匹配。MethodMatcher的isRuntime方法返回true时,Spring会把它标记为运行时匹配器:代理创建时先用静态方法匹配一次,把明显不匹配的方法过滤掉;等到每次实际调用时,再调用带参数的matches方法做最终决策。

Design上这里有一个很关键的点:第一个matches方法并不是只对isRuntime=false的匹配器才有意义。即使isRuntime=true,第一次静态匹配也必须通过,方法才有资格进入后续的动态匹配流程。这就给我们留下了一个优化空间——先用尽可能宽的静态条件过滤,再用动态条件精判,可以减少性能损耗。

1.3 适合动态匹配的业务场景

结合我实际接触过的需求,以下几类场景特别适合用动态匹配:

一类是敏感操作审计。比如后管系统里,同一个查询接口,普通员工查自己的数据不需要记录,但查别人的敏感数据必须留痕。参数不满足条件时,切面直接跳过,连日志都不落。

一类是灰度开关控制。方法是否执行某些增强逻辑,取决于当前传入的用户ID或租户ID是否在灰度白名单里。这种规则天然依赖运行时的上下文参数。

还有一类是幂等控制、限流降级场景,需要根据特定的参数维度来决定某个调用是否走增强逻辑,而不是对这个方法的所有调用无差别处理。

这类需求如果交给注解式AOP做,一般都要在Advice里拿到参数再套一层判断,代码割裂感很强。动态匹配则把决策逻辑从Advice移到了Pointcut,Advice只关心“命中之后做什么”,职责更清晰。

2. DynamicMethodMatcherPointcut核心原理拆解

2.1 Pointcut、ClassFilter、MethodMatcher三者关系

Spring AOP里,Pointcut是切点抽象,它的定义非常简洁:

public interface Pointcut { ClassFilter getClassFilter(); MethodMatcher getMethodMatcher(); }

一个切点由两部分组成:类过滤器ClassFilter和方法匹配器MethodMatcher。ClassFilter用来限定“哪些类可以被匹配”,比如只匹配service包下的类,或者只匹配接口名称以Service结尾的类。MethodMatcher用来限定“类里的哪些方法可以被匹配”,以及是否需要对参数进行动态判断。

这种拆分不是多此设计。类过滤做一次粗筛,方法匹配做细筛,动态匹配做最终决策,三级流水线各管一段,互相不干扰,也方便复用。比如同一个MethodMatcher可以跟不同的ClassFilter组合成不同的切点,适配不同的业务场景。

DynamicMethodMatcherPointcut这个类,就是Spring提供的一个便捷骨架:它实现了Pointcut接口,同时以内部匿名类的方式提供了一个默认的ClassFilter(默认过滤所有类),以及一个isRuntime固定为true的MethodMatcher。换句话说,只要继承它,再重写几个matches方法,就自带动态匹配能力,不需要自己再去拼装Pointcut和MethodMatcher。

2.2 isRuntime()与三次匹配调用链

我在这里花点时间讲清楚整个调用链,因为它直接关系到很多人调试时为什么“看起来没生效”。

Spring AOP在创建代理时,会遍历容器里所有Advisor,取出Pointcut,执行第一次匹配:

第一次:pointcut.getClassFilter().matches(targetClass) 第二次:pointcut.getMethodMatcher().matches(method, targetClass)

这两次都通过,这个方法的代理才会被创建。这里需要注意,如果类的过滤都过不了,后面方法匹配再精准也没用。

等到代理对象的方法真正被调用时,Spring会做第三次匹配:

第三次:pointcut.getMethodMatcher().matches(method, targetClass, args)

只有当MethodMatcher的isRuntime()返回true时,第三次匹配才会执行。而DynamicMethodMatcherPointcut恰恰把isRuntime写死为true,所以继承它的切点,每次方法调用都会执行带参数版本的matches方法。

这三次匹配的职责完全不同:前两次是静态筛选,决定代理要不要建、方法要不要挂拦截链;第三次是运行时精判,决定当前这一次调用到底要不要执行通知。理解这条链路之后,很多误会都能解开,比如“为什么我改了切点的静态过滤条件,重启服务才生效”“为什么动态匹配这么多次调用还是会执行”,答案都在这三次匹配的时机差异里。

2.3 DynamicMethodMatcherPointcut源码级别细节

DynamicMethodMatcherPointcut的源码并不长,核心就是两件事:提供默认ClassFilter和默认DynamicMethodMatcher。

public abstract class DynamicMethodMatcherPointcut extends DynamicMethodMatcher implements Pointcut { private ClassFilter classFilter = ClassFilter.TRUE; public void setClassFilter(ClassFilter classFilter) { this.classFilter = classFilter; } @Override public ClassFilter getClassFilter() { return this.classFilter; } }

而父类DynamicMethodMatcher本身就是一个MethodMatcher,它把isRuntime定为true,并给出了matches方法的默认实现:

public abstract class DynamicMethodMatcher implements MethodMatcher { @Override public final boolean isRuntime() { return true; } @Override public boolean matches(Method method, Class<?> targetClass) { return true; } @Override public boolean matches(Method method, Class<?> targetClass, Object... args) { return true; } }

看到这里,我相信很多人对DynamicMethodMatcherPointcut的“懒设计”就有感觉了。它默认所有类都匹配、所有方法都匹配、所有参数都匹配,把决策权全部留给子类去重写。继承了它,如果不重写任何方法,就相当于一个拦截所有方法的全通切点,这在实际工程里几乎不会是想要的效果。

所以正确姿势是:至少重写matches(Method, Class)做静态方法范围限制,并按需重写matches(Method, Class, Object...)做动态参数判断。如果你想限制类范围,调用setClassFilter或在构造方法里传入自定义ClassFilter。

3. Spring Boot完整示例:按参数动态记录审计日志

3.1 场景与前置准备

为了把原理落到能跑的项目上,我设计了一个简化但完整的场景。模拟某公司的交易服务模块,有一个账户操作接口,其中暴露了transfer和withdraw两个方法。需求是:

方法调用仅在满足以下任一条件时,需要记录审计日志:

  • 转账金额或取现金额大于等于10000
  • 目标账户(转账方法)或者操作账户(取现方法)命中重点监控名单

如果参数不满足条件,就不需要记录审计日志。

前置准备很简单,Spring Boot项目只需引入aop依赖:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>

如果你用Spring Boot 3.x,依赖不变。Spring Boot会自动配置AOP支持,把容器里所有Advisor注册进自动代理流程,无需额外写@EnableAspectJAutoProxy。

业务侧定义一个账户服务接口和实现类,模拟两个方法:

public interface AccountService { void transfer(String targetAccount, BigDecimal amount, String operator); void withdraw(String account, BigDecimal amount, String operator); }
@Service public class AccountServiceImpl implements AccountService { @Override public void transfer(String targetAccount, BigDecimal amount, String operator) { System.out.println("执行转账: 目标账户=" + targetAccount + " 金额=" + amount + " 操作人=" + operator); } @Override public void withdraw(String account, BigDecimal amount, String operator) { System.out.println("执行取现: 账户=" + account + " 金额=" + amount + " 操作人=" + operator); } }

3.2 定义动态切点

接下来是重头戏,定义一个继承DynamicMethodMatcherPointcut的切点类:

public class DynamicAuditPointcut extends DynamicMethodMatcherPointcut { /** 重点监控账户列表,实际项目中可以从配置中心拉取 */ private final Set<String> sensitiveAccounts = new HashSet<>(); public DynamicAuditPointcut() { sensitiveAccounts.add("ACC-MONITOR-001"); sensitiveAccounts.add("ACC-MONITOR-002"); // 构造方法里也可以setClassFilter setClassFilter(clazz -> clazz.getName().contains("AccountService")); } /** 静态匹配:先按方法名缩小范围 */ @Override public boolean matches(Method method, Class<?> targetClass) { return method.getName().equals("transfer") || method.getName().equals("withdraw"); } /** 动态匹配:根据本次调用的参数决定是否拦截 */ @Override public boolean matches(Method method, Class<?> targetClass, Object... args) { if (args == null || args.length < 2) { return false; } BigDecimal amount = (BigDecimal) args[1]; String account = (String) args[0]; if (amount == null) { return false; } boolean hitAmount = amount.compareTo(new BigDecimal("10000")) >= 0; boolean hitAccount = sensitiveAccounts.contains(account); return hitAmount || hitAccount; } }

这里有两个设计要点。第一,静态matches只判断方法名,不涉及参数,因为参数在代理创建阶段根本不存在。第二,动态matches里对参数做了null安全校验和类型转换,Args数组的顺序完全取决于目标方法入参顺序,所以一定要写清楚到底哪个下标对应哪个业务含义。

你可能有个疑问:类过滤用了setClassFilter,静态方法匹配又判断了方法名,是否重复?不重复。ClassFilter过滤的是类本身,哪怕类里只有一个匹配的方法,其他方法也会被过滤,不参与后续任何匹配;MethodMatcher的静态matches进一步判断具体方法名,相当于二级筛选,两者缺一不可。

3.3 定义通知(Advice)

切点只回答“拦不拦”,真正干活的是Advice。这里用的Advice是组织拦截链的MethodInterceptor:

public class AuditRecordAdvice implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { String methodName = invocation.getMethod().getName(); Object[] args = invocation.getArguments(); System.out.println("[审计] 方法=" + methodName + " 参数=" + Arrays.toString(args) + " 时机=" + LocalDateTime.now()); Object result = invocation.proceed(); System.out.println("[审计] 方法=" + methodName + " 执行完成"); return result; } }

注意一点:如果invocation.proceed()不被调用,目标方法就不会执行,调用链会在当前拦截器处断开。在审计场景里我们一般只是旁路记录,必须保证方法正常执行,千万别漏掉这个调用。

3.4 组装Advisor并注册

有了切点和Advice,还需要把它们组装成Advisor,Spring AOP只认Advisor,不直接认Pointcut。

@Configuration public class AuditAopConfig { @Bean public Pointcut dynamicAuditPointcut() { return new DynamicAuditPointcut(); } @Bean public Advisor dynamicAuditAdvisor(Pointcut dynamicAuditPointcut) { return new DefaultPointcutAdvisor(dynamicAuditPointcut, new AuditRecordAdvice()); } }

DefaultPointcutAdvisor是Spring提供的通用Advisor实现,构造参数就是Pointcut和Advice。把这个Advisor注册成Bean后,Spring Boot的自动代理机制会把它应用到所有符合条件的Bean上。整个装配过程不需要在业务Service上加任何注解,业务代码零侵入,这是动态匹配方案非常大的一个优势。

3.5 验证效果

写一个简单的Runner或者Controller方法验证一下:

@Component public class AuditRunner implements CommandLineRunner { private final AccountService accountService; public AuditRunner(AccountService accountService) { this.accountService = accountService; } @Override public void run(String... args) { // 金额不足阈值,预期不拦截 accountService.transfer("NORMAL-001", new BigDecimal("5000"), "peter"); // 金额达到阈值,预期拦截 accountService.transfer("NORMAL-002", new BigDecimal("20000"), "peter"); // 命中监控账户,金额很小,预期拦截 accountService.transfer("ACC-MONITOR-001", new BigDecimal("100"), "peter"); // 既不满足金额也不命中账户,预期不拦截 accountService.withdraw("NORMAL-003", new BigDecimal("3000"), "julia"); } }

运行后控制台输出应该是:

执行转账: 目标账户=NORMAL-001 金额=5000 操作人=peter [审计] 方法=transfer 参数=[NORMAL-002, 20000, peter] 时机=2025-01-01T10:00:00 执行转账: 目标账户=NORMAL-002 金额=20000 操作人=peter [审计] 方法=transfer 参数=[ACC-MONITOR-001, 100, peter] 时机=2025-01-01T10:00:00 执行转账: 目标账户=ACC-MONITOR-001 金额=100 操作人=peter 执行取现: 账户=NORMAL-003 金额=3000 操作人=julia

第一次和最后一次调用没有审计日志,说明动态匹配确实按参数把调用分流了。第二次和第三次调用命中规则,执行了Advice逻辑。整个过程中AccountServiceImpl的代码一个字没改。

3.6 静态过滤与动态匹配结合的最佳实践

上面示例代码虽然能跑,但从性能角度看,动态matches里面每次都执行Set的contains操作和BigDecimal的compareTo,在方法调用量极大时还是有开销的。

最佳实践是:把能前置判断的规则都放到静态匹配里。比如,如果某条规则只针对特定类、特定方法名,就优先在静态matches里拦住;如果某些方法根本不需要参与审计,就用静态条件直接返回false。只有真正依赖参数值的规则,才放到动态matches里。

还可以做一层缓存。由于动态匹配的结果完全依赖参数内容,如果同一组参数会被高频重复调用,可以在切点里维护一个基于参数字符串或参数hashCode的缓存,命中直接放行。但做缓存要注意内存回收,别让缓存无限制增长。我之前在一个批处理系统里遇到过这种情况,加了基于参数摘要的并发缓存之后,性能损耗基本可以忽略。

4. 常见问题与性能陷阱排查

4.1 为什么我写的matches方法没有生效

我在本地和几个项目里都遇到过这类问题,通常有以下几种原因。

最常见的是只重写了带参数的matches,没重写静态matches。前面说过,DynamicMethodMatcherPointcut的静态matches默认返回true,这意味着所有类、所有方法都会通过第一轮筛选,你的切点实际作用范围比你以为的大得多。但这并不会导致“不生效”,反而会导致“过度拦截”。如果你碰到的是完全不拦截,那就要检查是不是类过滤出了问题——比如默认ClassFilter是TRUE,但你在setClassFilter里写的条件把所有目标类都排除掉了。

另一个很容易踩的坑是:目标Bean是通过CGLIB创建的代理,但切点匹配的是实现类方法,而不是接口方法。如果你实现的接口方法上没写任何东西,实现类方法名也正常,一般没问题;但如果目标方法被final修饰,CGLIB无法为其生成代理,动态匹配再精准也只能干看着。

还有一个隐蔽问题:方法内部自调用。比如AccountService的transfer方法内部调用了withdraw方法,外层通过代理调transfer时,内部this.withdraw是直接调用原始对象,不会经过代理,动态匹配对内部那次调用完全失效。这一点对任何AOP方案都是一样的,不是DynamicMethodMatcherPointcut特有的,但很多人会在这里怀疑切点写错了。

4.2 动态匹配的性能开销

Spring AOP的动态匹配,最直接的性能损耗在于每次方法调用都会执行一次带参数的matches方法,并且参数会被包装成Object数组。如果这个切面挂在核心链路上,每秒调用几万次,那matches里的任何逻辑都会被放大。

我在一个网关类服务里做过统计,动态matches里如果做了JSON序列化、远程配置查询这类操作,整体耗时可能增加30%以上。所以规则很简单:

一是规则尽量简单。动态matches里只做内存判断、零依赖计算,不允许出现网络IO、数据库查询、复杂正则等操作。

二是能静态就静态。把不需要动态判断的大多数方法先用静态条件排除掉,真正走到动态matches的方法数量会小很多。

三是规则结果可缓存。参数值离散程度不高的情况下,可以按参数的关键维度做缓存,注意控制缓存上限和过期时间。

关于DynamicMethodMatcherPointcut的误用还有一种情况,就是本来不需要动态匹配,却非要用它。如果规则只跟方法签名、注解、类类型有关,老老实实用静态Pointcut就好,没必要给每次调用增加额外匹配开销。

4.3 代理方式带来的差异

Spring Boot默认对于有接口的类使用JDK动态代理,对于没有接口的类使用CGLIB。JDK代理要求目标对象实现了接口,代理对象也只能按接口暴露能力。你的AccountService有接口,注入的accountService实际是JDK代理,这点没问题。

但如果你想对没有接口的类做动态匹配,需要确保Spring Boot使用CGLIB代理。Spring Boot 2.x之后,默认设置已经是spring.aop.proxy-target-class=true,所以默认就是CGLIB,不需要额外配置。

CGLIB代理有几个边界需要留意:final方法、static方法、private方法都不能被代理。如果目标方法恰好是private,比如某个Service内部调用的辅助方法,外部切点是匹配不到它的,动态匹配也不会参与。

4.4 多个Advisor的执行顺序

一个系统里通常不会只有一个切面。比如既有事务切面,又有你自定义的动态审计切面,这时候执行顺序就很重要。Spring AOP中多个Advisor的优先级通过Order决定,数字越小越先执行。

如果希望审计逻辑先执行、事务再介入,可以在Advisor的Bean方法上加@Order:

@Bean @Order(1) public Advisor dynamicAuditAdvisor(Pointcut dynamicAuditPointcut) { return new DefaultPointcutAdvisor(dynamicAuditPointcut, new AuditRecordAdvice()); }

如果不控制顺序,可能会出现这类现象:事务已经提交了,你的审计切面才执行;或者审计切面先执行了但事务还没开启,拿到的连接状态不是你预期的。动态切点因为方法调用链长,排序问题更要提前设计。

4.5 常见问题速查表

现象可能原因解决办法
完全不拦截静态matches返回false,或ClassFilter把目标类过滤掉了断点检查getClassFilter和静态matches返回值
方法被拦截但动态匹配结果未生效isRuntime被误改成false确认没有重写isRuntime,保持默认true
所有方法都被拦截静态matches没重写,默认返回true重写matches(Method, Class),限定目标方法
每次调用性能明显下降动态matches里有IO操作或复杂计算精简规则,加缓存,把计算量前置到静态阶段
内部方法调用未触发切面自调用绕过了代理想办法通过代理调用,或调整方法拆分
外部拿到的是原始Bean而非代理Bean没有进入自动代理流程检查aop依赖,确认被拦截类在扫描路径中
Advice抛异常导致业务失败通知内代码未做异常隔离审计等旁路逻辑单独try-catch,避免影响主流程

5. 规则配置化与工程落地建议

5.1 把拦截规则迁移到配置中心

上面的代码里,敏感账户集合是写死在构造方法里的。真正到生产环境,这套规则肯定要动态变更,不可能每次改规则都发版本。

比较好的做法是让动态切点持有配置服务引用,从配置中心、数据库或分布式缓存读取规则。比如改造一下切点:

public class DynamicAuditPointcut extends DynamicMethodMatcherPointcut { private final SensitiveRuleService ruleService; public DynamicAuditPointcut(SensitiveRuleService ruleService) { this.ruleService = ruleService; } @Override public boolean matches(Method method, Class<?> targetClass) { return method.getName().equals("transfer") || method.getName().equals("withdraw"); } @Override public boolean matches(Method method, Class<?> targetClass, Object... args) { BigDecimal amount = (BigDecimal) args[1]; String account = (String) args[0]; return ruleService.isSensitiveOperation(method.getName(), account, amount); } }

配置服务可以在内存中维护一份规则快照,后台监听配置变更,更新快照。这样切点的判断逻辑完全变成数据驱动,规则变更秒级生效,不用重启应用。

但要注意,动态matches里不能直接访问远程配置中心,否则每次调用都是远程调用,性能直接崩。正确的做法是配置服务启动时加载到本地缓存并定期刷新,或者订阅推送更新,保证matches方法内只有内存读写。

5.2 与注解式AOP如何选择

很多人会拿这个方案和@Aspect注解式AOP对比。我的经验是,两者不是替代关系,而是分工关系。

注解式AOP的强项是声明式匹配:代码里写@AuditLog,静态切点拿到注解信息即可,可读性好、上手快,适合规则固定、参数相关性弱的场景。弱点是,如果要根据参数值做复杂判断,要么在Pointcut表达式里写比较绕的args绑定,要么在Advice里堆业务判断,哪一种都会让代码变得别扭。

动态匹配的强项正好相反:规则完全数据化、参数参与决策、业务代码零侵入。弱点是理解成本高、性能开销需要控制、不容易被团队里新手快速接手。所以我的建议是:核心决策类、规则频繁变化的模块优先考虑动态匹配;业务结构清晰的模块,用注解式AOP足够。

还有一种混合方案:用注解式切点作为粗筛,在Advice内部引用规则引擎做动态决策。这其实是很多团队实际采用的模式,它兼具了声明式的清晰和参数决策的灵活,只是决策逻辑放到了Advice里,职责边界需要自己把握。

5.3 工程落地建议

最后聊几点工程上的建议,都是我踩过坑之后沉淀下来的。

第一,动态匹配器的命名和职责要明确。不要在一个切点里塞多个业务的判断逻辑,否则后期排查“为什么这个接口被拦了”会非常痛苦。建议一个业务域一个切点,或者至少把规则按业务拆到独立方法。

第二,日志打点要全。动态匹配因为每次调用都可能走不同分支,建议在matches方法入口和出口打debug日志,或者把关键判断结果抛给Advice记录。不然线上遇到“某个调用没审计日志”,很难定位是切点没匹配上,还是匹配上了但Advice内部跳过。

第三,测试要覆盖静态和动态两个维度。静态匹配的测试用Mock方法签名即可,动态匹配必须设计参数维度的用例,把阈值的边界情况、null值、类型不匹配等异常输入都覆盖到。

第四,如果项目里已经存在大量基于注解的切面,引入动态匹配时要注意代理链的复杂度。每个Advisor都是一层拦截,拦截器太多会让调用链变深,Debug排查困难。建议先清理掉重复的切面,再新增动态切点,别让代理链无限膨胀。

最后再分享一点个人体会

DynamicMethodMatcherPointcut这个类,我在实际项目里真正独立用到它的次数其实不多,但每一次遇到“规则必须根据参数动态决定”的需求,它都是最贴合的解法。相比在业务方法里到处加if判断,把决策交给切点、把操作交给Advice,这种分离方式让代码长期维护起来舒服很多。如果你已经理解了静态匹配和动态匹配的区别,也看懂了上面的示例,完全可以找一个轻量的业务场景试试,比如给某个内部接口加上按用户灰度开启的日志切面。跑通一次,你对Spring AOP的理解,就不只是停留在注解上了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询