最近几年不管是面试、工作、还是自己带项目,我几乎每过一段时间就会被人问到同一个问题:“什么是AOP?”这个词在Java后端领域出现频率极高,Spring框架里到处都是它的影子——@Transactional、@Async、日志审计、权限校验,背后都是同一套机制在撑腰。很多人嘴上能答出“面向切面编程”这六个字,但真让他们讲清楚“切面是什么、代理怎么来的、JDK和CGLIB有什么区别、为什么自调用会让切面失效”,就卡壳了。
我写这篇东西的出发点很简单:把AOP从“背概念”变成“真懂”。不管你是刚学Spring的新手,还是准备面试的老开发,或者纯粹是想搞明白项目里那堆切面为什么莫名不生效,这篇文章都值得你花半小时读完。
1. AOP是什么:解决什么问题
1.1 OOP的“盲区”:横切关注点
面向对象编程(OOP)的核心思想是用类来抽象业务,把数据和行为封装在一起。但在任何一个真实系统里,总会有一类功能高度相似、却散落在各个业务类里的代码——比如打印日志、开启事务、校验权限、记录耗时。它们在逻辑上不属于任何单一业务模块,却在几乎所有模块中反复出现。
软件工程里专门给这类逻辑起了个名字,叫横切关注点(Cross-Cutting Concerns)。最典型的就是日志:下单要打日志,支付要打日志,库存扣减要打日志,甚至用户改个密码也要打日志。如果你在每一个方法里手动写死一行日志代码,一旦日志规则变化(比如要求带上traceId、用户ID、耗时毫秒数),你就得改几十上百个类,工作量爆炸而且极易漏改。
AOP站出来解决的,就是这个“横切”问题。它提供了一种能力:把这些和业务无关、却横跨所有模块的通用逻辑,从业务代码中抽离出来,统一写在一个独立模块里,然后声明“当调用某类方法时,先替我执行这段逻辑”。业务代码保持干净,通用逻辑集中维护。
1.2 几个核心术语:别被名字吓住
AOP的名词确实多,初看让人头大,但抓住本质后会发现每个词都很好理解。我整理了一张速记表:
| 术语 | 通俗理解 | 代码对应 |
|---|---|---|
| 切面(Aspect) | 把横切逻辑封装成的模块 | 一个带@Aspect注解的类 |
| 连接点(Join Point) | 程序执行过程中的某个“插队点” | 一般指某个方法执行 |
| 切入点(Pointcut) | 匹配哪些连接点,规则表达式 | execution(* com.x.service.*.*(..)) |
| 通知(Advice) | 在切入点执行的逻辑体 | @Before、@Around等注解方法 |
| 目标对象(Target) | 要被织入切面逻辑的原始对象 | 例如UserService实现类 |
| 代理(Proxy) | 增强后的“替身对象”,调用方真正使用的对象 | 动态生成的Proxy类 |
| 织入(Weaving) | 把切面应用到目标对象并创建代理的过程 | Spring容器启动时自动完成 |
这七个词里,你只要抓住切面、切入点、通知、代理这四个,就足够理解90%的AOP场景。其余两个是辅助概念,面试时能准确说出来就是加分项。
1.3 一个类比:做饭流程里的“埋点”
我用一个生活化的例子帮新手理解。假设你开一家餐厅,每一道菜都有固定的烹饪流程:备菜、下锅、装盘、上桌。你不可能为了统计出餐速度,就在每道菜的做法里都插一段计时代码。更合理的做法是给厨房装一套监控系统,对“所有菜品出锅上桌”这个节点统一计时、统一录影。
在AOP里,“所有菜品”就是匹配到的目标类,“出锅上桌”就是方法执行,“监控系统”就是切面。业务代码依然是做菜本身,监控逻辑独立运行,互不干扰。这就是AOP的核心价值:在不侵入业务代码的前提下,统一给业务方法添加额外行为。
2. AOP的应用场景:到底图什么
2.1 日志与性能监控:最经典的入门场景
日志记录是AOP最普遍的使用场景,也是学习AOP最好的起点。你想给某个Service包里的所有public方法记录调用耗时、入参、出参,常规做法是每个方法里手动写,用了AOP之后只需要一个切面。
@Aspect @Component public class LogAspect { @Around("execution(* com.example.service..*.*(..))") public Object logMethodCost(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); Object result = joinPoint.proceed(); long cost = System.currentTimeMillis() - start; String methodName = joinPoint.getSignature().toShortString(); System.out.println("[" + methodName + "] cost " + cost + " ms"); return result; } }这段代码里的joinPoint.proceed()就是“继续执行原方法”。切进去之后,你在前后都可以补充逻辑:记录入参、统计耗时、捕获异常、统一返回结构。你完全不需要改动任何业务Service代码,日志规则改起来也只要动一个切面类。对于老项目来说,这种无侵入式接入意味着不用死磕历史代码,就能全量铺开监控能力。
2.2 事务管理:所有Java开发都在用的AOP
如果你以前不明白@Transactional为什么能在方法异常时自动回滚,那现在可以真相大白了:它就是AOP的产物。
Spring事务管理器的执行流程,本质上是这样一个切面逻辑:
// 伪代码,描述@Transactional的底层流程 public Object executeWithTransaction(ProceedingJoinPoint joinPoint) { try { beginTransaction(); Object result = joinPoint.proceed(); commitTransaction(); return result; } catch (Exception e) { rollbackTransaction(); throw e; } }当你给某个方法加上@Transactional,Spring容器在启动时会为这个Bean生成一个代理对象。外部调用Bean方法时,实际进入的是代理对象里的事务切面逻辑,而不是直接跑到你业务类内部。这一点很多开发没意识到,直到有一天他们发现“自己调用自己”的同类方法事务不生效,才回头找代理的真相。
2.3 权限控制:一个切面挡住所有未授权请求
权限校验很适合写在AOP里,尤其在非Spring Security的老项目中。你可以自定义一个@RequirePermission注解,然后用切面拦截所有标注了该注解的方法:
@Aspect @Component public class PermissionAspect { @Before("@annotation(requirePermission)") public void checkPermission(JoinPoint joinPoint, RequirePermission requirePermission) { // 从上下文取出当前用户角色 // 与注解要求的角色比对,不满足就抛权限异常 } }这样权限逻辑就能从Controller或Service中彻底剥离。新加一个接口需要权限控制,只需在方法上加注解;权限规则调整,只需改一个切面。设计得好的权限切面,甚至可以做细粒度的按钮级权限控制,且切换权限模型的时候业务代码一动不动。
2.4 缓存、限流、异常兜底:扩展性极强
缓存方面,你可以做一个通知逻辑:先查本地缓存,命中则直接返回,未命中则执行原方法并把结果放入缓存。
限流方面,可以用切面统计窗口期内的请求次数,超过阈值就直接拒绝,不进入业务逻辑。
异常兜底方面,很多统一异常处理框架底层也是AOP的思路——拦截Service层抛出的异常,转换成统一错误码和响应格式,省得每个方法都写try-catch。
我用这些场景想强调的是:AOP不是某个特定框架的功能,而是一种编程范式。它让你把通用逻辑做成“配件”,按需往业务方法上“装配”。装配规则只在切面中声明,业务类本身一尘不染。
3. 实现原理解密:JDK动态代理和CGLIB动态代理
3.1 代理模式是AOP的底牌
说到底,AOP落到代码层面,靠的是设计模式里的代理模式。画在架构图上的话就是:调用方 → 代理对象 → 目标对象。代理对象拦截调用,在执行目标方法前后插入额外逻辑。
Spring的AOP实现,本质上是利用IOC容器在Bean创建阶段“偷偷换掉”了原始对象:容器里保留的、被注入到其他组件里的,不是你的实现类实例,而是一个代理实例。代理实例在调用方法时会先执行切面逻辑,再委托给原始实例。所以AOP能否生效,完全取决于你是不是通过代理对象调用的方法。这一点在后面自调用问题里会变成重要的排查线索。
3.2 JDK动态代理:面向接口的反射方案
JDK动态代理是Java原生支持的,核心就是Proxy和InvocationHandler两个类。它的前提条件是目标对象必须实现了至少一个接口,JDK在运行时动态地创建出一个实现相同接口的代理类。
我用个极简例子演示一下:
// 接口 public interface HelloService { void sayHello(); } // 目标实现类 public class HelloServiceImpl implements HelloService { @Override public void sayHello() { System.out.println("hello"); } } // InvocationHandler:代理逻辑的入口 public class LogInvocationHandler implements InvocationHandler { private final Object target; public LogInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("before " + method.getName()); Object result = method.invoke(target, args); System.out.println("after " + method.getName()); return result; } } // 生成代理 HelloService proxy = (HelloService) Proxy.newProxyInstance( HelloService.class.getClassLoader(), new Class[]{HelloService.class}, new LogInvocationHandler(new HelloServiceImpl()) ); proxy.sayHello();这段代码的重点在Proxy.newProxyInstance的三个参数:类加载器、接口数组、调用处理器。生成的代理类实现了HelloService接口,所有对代理对象的方法调用都会进入LogInvocationHandler.invoke(),在这里你可以选择增强、过滤、或者直接调用目标对象。
面试时如果问细节,需要知道JDK代理使用了反射,调用链是“代理类 → InvocationHandler → 目标方法反射调用”。它的优点是简单、原生、兼容性好;缺点是只能代理接口,并且反射调用对极高频方法会有一定性能开销。
3.3 CGLIB动态代理:面向类的字节码增强
如果一个类没有接口,JDK动态代理就无能为力了。Spring引入了CGLIB(Code Generation Library)作为补充,它的思路不是走接口,而是直接生成目标类的一个子类,用ASM字节码操作技术重写父类的方法,在方法里插入增强逻辑。
看个典型写法:
public class UserService { public void queryUser() { System.out.println("query user"); } } public class UserServiceInterceptor implements MethodInterceptor { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println("before"); Object result = proxy.invokeSuper(obj, args); System.out.println("after"); return result; } } Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(UserService.class); enhancer.setCallback(new UserServiceInterceptor()); UserService proxy = (UserService) enhancer.create(); proxy.queryUser();和JDK动态代理的最大区别在于,CGLIB不要求目标类实现接口,只要目标类可以被继承就行。因此凡是final修饰的类、final修饰的方法,CGLIB都无能为力,因为子类不能继承final类、不能重写final方法。私有方法同样存在限制,因为子类无法继承私有方法,AOP切不到私有方法上。
在性能侧,CGLIB创建代理对象的过程比JDK代理重一点,因为要做字节码生成,但方法调用的时候往往是字节码直接调度的路线,不用走反射。在长期、高频调用的场景下,不少实测项目会感觉到CGLIB的调用链路更利索;反过来,如果你只是临时生成一次代理、调用次数很少,JDK代理的轻量创建又显得更经济。
3.4 选JDK还是CGLIB:框架已经替你做了决定
在Spring的历史版本里,传统Spring默认行为是:目标类实现了接口则用JDK代理,没有接口才用CGLIB。这套逻辑在早年让不少开发者栽过跟头——因为JDK代理对应的对象类型是Proxy,而不是你的实现类,强转就会报ClassCastException。
Spring Boot从较新版本开始改变了默认策略:默认开启spring.aop.proxy-target-class=true,也就是优先使用CGLIB。这么做的原因很实际:CGLIB不要求接口,兼容性更强,而且大多数业务Service并没有特意抽接口,时代变了,全面CGLIB反而省心。
如果你需要强制指定某个项目使用JDK代理,需要把spring.aop.proxy-target-class设置为false。不过真的很少遇到这种需求,我建议大多数人让框架默认即可,别跟它拧着来。
下面这张对比表,可以帮你快速在脑海里建立全貌:
| 对比项 | JDK动态代理 | CGLIB动态代理 |
|---|---|---|
| 实现方式 | 接口 + 反射 | 子类 + 字节码增强 |
| 目标类要求 | 必须实现接口 | 类不能是final |
| 生成代理速度 | 较快 | 较慢,要生成字节码 |
| 调用效率 | 反射调用 | 直接方法调度,往往更优 |
| final/private方法 | 不影响(接口本就不能final) | final方法无法代理 |
| 依赖 | JDK自带 | 需要引入CGLIB/Spring内部已集成 |
4. 在Spring里实战:从依赖到切面的一整套流程
4.1 先搞清楚:Spring AOP不等于AspectJ
不少初学者会混淆Spring AOP和AspectJ。严格说,AspectJ是一套完整的、独立的面向切面框架,它支持编译时织入、编译后织入、加载时织入,功能远比Spring AOP强大。Spring AOP走的路线是“借用AspectJ的注解和切点表达式语法”,但运行时实现依然是自己那套动态代理。
对普通业务开发来说,这个区别意味着什么?意味着你不需要去碰AspectJ的编译器,不需要做AspectJ Maven插件,只需要在代码里用@Aspect、@Pointcut、@Around这些注解,Spring容器在运行时就会通过动态代理把逻辑织入进去。轻量,好用,对Spring应用来说最大的实际价值已经被拿到了。
如果有人问“Spring AOP能不能拦截构造方法?”答案是不能,因为动态代理只接管现有方法调用。如果问“能不能拦截字段访问?”同样不能。这是Spring AOP的边界,但日常业务开发根本用不上那些能力,所以完全不用担心。
4.2 Spring Boot里快速写一个切面
第一步,加入依赖。Spring Boot项目只需要引入:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>第二步,写切面类,并加上@Aspect和@Component注解:
@Aspect @Component public class PerformanceAspect { @Pointcut("execution(* com.example.order.service.*.*(..))") public void orderServiceMethod() { } @Around("orderServiceMethod()") public Object aroundOrderService(ProceedingJoinPoint joinPoint) throws Throwable { String method = joinPoint.getSignature().toShortString(); long begin = System.currentTimeMillis(); try { return joinPoint.proceed(); } finally { long cost = System.currentTimeMillis() - begin; System.out.println(method + " cost " + cost + "ms"); } } }这里有几个容易翻车的细节要注意。第一,@Pointcut方法本身不关心返回值,它只是一个切点声明的载体。第二,@Around必须调用proceed(),不调用那就等于把原方法“掐掉”了,业务逻辑不会执行。第三,@Around方法抛出的异常类型声明为Throwable,否则拦截范围会被限制。
如果你是在传统Spring MVC项目而不是Spring Boot里,记得在配置类上添加@EnableAspectJAutoProxy开启AOP支持,Spring Boot的starter会自动完成这一步。
4.3 切点表达式和通知类型:这两张表是你的工具箱
切点表达式负责回答“对哪些方法生效”,我列几个最常见写法:
| 表达式 | 含义 |
|---|---|
execution(public * *(..)) | 匹配所有public方法 |
execution(* com.shop.*.service.*.*(..)) | 匹配com.shop下任意包名.service包下的所有方法 |
within(com.shop.order.service.*) | 匹配该包下的所有方法 |
@annotation(com.shop.annotation.Cacheable) | 匹配所有标注了指定注解的方法 |
bean(userService) | 匹配名为userService的Bean的方法 |
通知类型有五种,它们的触发时机各不一样:
| 通知 | 注解 | 触发时机 |
|---|---|---|
| 前置通知 | @Before | 目标方法执行前 |
| 后置通知 | @After | 目标方法执行后(无论正常或异常) |
| 返回后通知 | @AfterReturning | 目标方法正常返回后 |
| 异常后通知 | @AfterThrowing | 目标方法抛出异常后 |
| 环绕通知 | @Around | 目标方法执行前后都能干预,最灵活 |
实战里我推荐优先考虑@Around,因为它能做完整的前后处理,还可以控制是否放行、是否吞异常、是否改写返回值。@AfterThrowing适合做纯粹的异常监控;@AfterReturning适合做结果落库。这些通知之间还可以配合使用,比如@Around记录耗时、@AfterThrowing单独上报异常。
4.4 多个切面的执行顺序:别让逻辑互相打架
当一个方法同时命中多个切面时,执行顺序是有讲究的。默认情况下,多个切面的执行顺序是不确定的,但你可以通过实现Ordered接口或者在切面类上加@Order注解来人为控制。
@Aspect @Component @Order(1) public class LogAspect { ... } @Aspect @Component @Order(2) public class TxAspect { ... }数字越小优先级越高。多个切面嵌套时,执行顺序像洋葱一层一层包着目标方法:先进入优先级最高的切面,执行其前置逻辑,然后进入下一个切面,直到最终执行目标方法,返回时按相反方向依次执行后置逻辑。
如果你开发过支付系统会明白这个顺序有多重要:日志切面通常要包在最外层,权限切面提前拦截,事务切面紧贴在业务方法外面。一旦顺序错了,可能出现事务切面已经开启事务、却被外层日志切面吞掉异常导致回滚失效的诡异问题。
5. 面试高频追问:IOC、AOP、动态代理的关系
5.1 面试官这么问你是在考察什么
“讲讲IOC和AOP的原理”是Java面试里出现频率极高的问题。面试官问AOP,通常不是要听你背概念,而是想确认三件事:你是不是真的理解代理机制?你能不能说出JDK和CGLIB的区别?你有没有踩过AOP不生效的坑?
所以回答的时候我建议用递进结构。第一层讲概念:AOP是面向切面编程,把日志、事务这些横切关注点从业务代码中剥离,利于维护和复用。第二层讲实现:核心是动态代理,有JDK动态代理和CGLIB动态代理两条路线。第三层讲细节:JDK要求接口且走反射,CGLIB基于子类生成和字节码增强;Spring Boot默认CGLIB;自调用不走代理。
这样一套下来,面试官心里对你这块知识的评价基本就稳了。
5.2 30秒讲清楚AOP的最小回答模板
如果你只需要一个最短的回答,可以这么说:
AOP,全称面向切面编程,它解决的是OOP解决不好的横切关注点问题。实现上,Spring利用代理模式在运行时为Bean生成动态代理对象,调用方拿到的其实是代理。代理负责在执行真实方法前后插入切面逻辑,比如开启事务、打印日志。具体技术分两种:目标类实现接口时可以用JDK动态代理,基于反射实现;没有接口或者框架默认开启了proxy-target-class时,用CGLIB生成子类来做增强。Spring Boot现在默认走CGLIB。AOP能生效的前提是调用方必须通过代理对象调用目标方法,否则切面逻辑不会执行,这也是事务失效最常见的元凶之一。
这段话信息密但有逻辑,面试现场能说出来,基本就可以了。
5.3 IOC和AOP为什么总是成对出现
IOC(控制反转)和AOP常被并称Spring两大核心,它们其实是互相成就的关系。IOC解决的是对象创建和依赖注入的问题,把对象的生命周期交给容器统一管理;AOP解决的是横切逻辑的织入问题,但织入的前提是“容器有办法在Bean创建后对其进行包装”。
正因为IOC容器负责Bean的实例化,Spring才能在返回Bean之前悄悄用ProxyFactory生成一个代理对象放到容器中。调用方注入到的,是已经经过代理包装的对象。所以可以这么说:IOC给AOP提供了织入的舞台,AOP让IOC管理的对象有了更丰富的行为能力。
5.4 自调用陷阱:同一个类里方法互相调用,切面失效了怎么办
这是AOP实操里的老难题,也是面试官必问的扩展题。
@Service public class OrderService { @Transactional public void createOrder() { deductStock(); } @Transactional public void deductStock() { } }假如外部调用createOrder(),你以为两个方法的@Transactional都会生效?实际上,createOrder()方法是通过代理对象调用的,事务生效;但deductStock()是createOrder()内部直接通过this调用的,此时这个this指向的是目标对象,而不是代理对象。切面在代理对象中,绕过它就等于绕过了AOP,所以deductStock()的事务逻辑掉了。
解决的方案一般有三种。
第一种,把一个组件拆成两个不同的Service,让Service A调用Service B,这样外部的调用仍然是走代理。
第二种,在类内部注入自己:
@Autowired @Lazy private OrderService self; public void createOrder() { self.deductStock(); }这里的self是从容器里拿到的代理对象,所以事务切面正常生效。加@Lazy是为了避免构造阶段因循环依赖出问题。
第三种,用AopContext.currentProxy()拿到当前代理:
public void createOrder() { ((OrderService) AopContext.currentProxy()).deductStock(); }但用这个方法需要在配置里设置@EnableAspectJAutoProxy(exposeProxy = true),属于“治本但稍麻烦”的方案。我个人的建议是优先考虑拆类,业务边界清晰,代码也更自然。
6. 踩坑经验:切面不生效、循环依赖、性能开销
6.1 切面不生效?按我的排查清单来
我在自己项目里遇到过太多次“切面写了但没反应”的问题,后来总结出一套排查顺序,新手照着查基本能定位:
**切面类有没有被Spring管理。**检查是否有
@Component注解,或者被Spring扫描到。一个没放进IOC容器的@Aspect类只是一堆注解而已,完全不起作用。**Spring Boot的starter有没有引入。**只写了切面却没有引入
spring-boot-starter-aop,注解不会生效。**切点表达式是否匹配目标。**很多人写
execution(* com.example..*.*(..)),范围没想清楚,结果目标Service根本没被匹配到。建议先写一个最宽泛的execution(* com.example..*.*(..)),确认能切到之后再逐步收紧。**目标方法是不是final/private/static。**CGLIB要求能被继承和重写,final类、final方法、private方法都属于不可强化对象,切了也白切。
**是不是内部自调用。**重点检查是否通过
this调用同类方法,这是事务不生效的头号原因。**调用方拿到的Bean是不是代理。**如果你手动
new了一个Service,或者从某个静态工厂里拿到的不是Spring容器中的代理,自然也没有切面。比如把Service偷偷用static方法缓存了一份,后面调用的全是旧对象,切面当然失效。
6.2 切面和循环依赖搅在一起怎么办
有AOP参与的循环依赖,比较容易出现的场景是:类A依赖类B,类B依赖类A,同时A或B上有切面。Spring解决普通循环依赖靠的是三级缓存和提前暴露原始对象,但如果早期暴露的原始对象直接被消费了,而不是在后续生成代理再替换,那就会导致某些Bean拿到的是未经代理的裸对象,切面失效。
遇到循环依赖,我一般先不急着加@Lazy或者改设计,而是看看有没有办法直接消除循环依赖。拆类、调接口、调整调用方向,往往比在缓存细节里折腾更持久可靠。如果项目代码实在太乱无法重构,@Lazy注入是一个快速止血的办法;再不行,显式配置@DependsOn调整Bean创建顺序也能救急。
6.3 不要过度设计切面,也别在切面里干重活
AOP很强大,但有一种错误比不生效更隐蔽,就是滥用。我曾经在一个老项目里看到有人把很小的查询方法也加上日志切面和缓存切面,一次查询要经过两层代理、多道切点匹配、好几个环绕通知,本来十几毫秒的接口硬生生变成了一百多毫秒。
切面里的代码要尽量轻。日志切面不要同步刷盘,异步发送或交给日志框架处理都行;缓存切面不要做复杂序列化;限流切面的计数器要尽量走内存。所有这些横切逻辑都跑在你的核心方法调用链上,每多一秒额外处理,所有业务就多一秒延迟。
切点表达式同样要精打细算。execution(* com.example..*.*(..))这种宽泛写法虽然方便,但会让容器里几乎每个Bean的方法调用都被代理逻辑判断一遍。我见过有些项目里的Controller、配置类、甚至在Spring内部组件都被无谓地代理了,启动变慢,运行期也不踏实。至少要把包名范围收敛到实际需要监控的Service或者指定注解上。
6.4 用AOP能做出什么“高级感”设计
讲几个我实际操作中比较满意的玩法,给扩展思路用。
一是注解式幂等控制。定义一个@Idempotent注解,切面在执行业务前先判断请求唯一键是否已处理过,处理过就直接返回历史结果,否则执行业务并记录。加一个注解就能给接口加上幂等能力,业务代码不用写重复逻辑。
二是注解式重试。定义一个@RetryOnFailure,切面里捕获异常,按策略最多重试N次,每次间隔一定时间。对于和外部系统对接的偶发网络抖动场景,比业务代码里写for循环干净得多。
三是注解式多租户数据隔离。自定义@TenantIntercept,切面里自动向查询条件追加当前租户ID,业务查询完全不用感知租户维度的拼接。这种情况下AOP的优势体现得非常极致——你甚至不用改动任何业务SQL,就实现了全局隔离。
这些设计本质上都遵循同一种模式:用注解表达意图,用切面实现机制,业务代码保持声明式描述。这也是AOP在实际工程中最大的魅力。
最后再分享一位老同事当年的口头禅:“代理不生效,先想想走没走代理。”这句话帮我排查掉的项目问题,比我记过的任何排查文档都多。AOP说穿了就是一个精心设计的代理使用方式,你只要把代理和切入点这两件事刻在脑子里,遇到各种诡异问题都能很快定位到根因。新知识上手总是有些门槛,但一旦理解透了,它就会成为你工程能力里非常硬核的一层地基。