1. 为什么我一定要手写一遍AOP而不是背原理
前阵子去面试,面试官上来就问了一个我自以为很熟的题:“Spring 6.0的Spring AOP底层到底怎么实现的?”我想都没想就回答“JDK动态代理和CGLIB动态代理二选一”,然后面试官笑了笑,追了一句:“那你给我讲讲JDK动态代理为什么不能代理实现类,CGLIB又为什么不能代理final方法?手写一个增强方法的最小闭环你能写出来吗?”我当时就愣住了。回来之后我把背诵宝典扔一边,直接在JDK 17 + Spring 6.0环境下把AOP的核心链路手写了一遍,从最简单的动态代理增强,到注解驱动的小型AOP容器,再到拦截器链和切面顺序。这篇记录就是这次手写的完整复盘,适合准备面试、看不进去Spring源码、以及想自己实现轻量级AOP工具的同学。
1.1 面试复盘:背下来的是知识点,不是能力
很多人和我一样,背了一堆结论:AOP是面向切面编程、Spring AOP基于动态代理、切面由切点和通知组成、AOP常用于日志、事务、权限校验、接口耗时统计等场景。这些词谁都会说,但面试官只要往下追问两层就露馅了。
我复盘时把问题拆成了三层:
- 第一层:AOP是什么?有哪些核心概念?能解决什么重复代码问题?这一层靠背。
- 第二层:JDK动态代理和CGLIB动态代理各自怎么实现?代理对象如何拦截方法调用?这两者的适用边界是什么?这一层需要动手写过才知道。
- 第三层:Spring 6.0中这些机制是怎么被组织起来的?切面怎么被解析成Advisor?代理在Bean生命周期哪个阶段被创建?点名调用时拦截器链如何执行?这一层需要把第二层的东西和Spring容器机制串起来。
第三层如果没写过,基本答不出来。以前我连“JDK动态代理生成的代理对象和实现类没有继承关系”这种关键推论,都是出错后才明白的。
1.2 手写的目标:不重写Spring,只把黑盒拆成白盒
手写AOP听起来吓人,但我给自己定的范围非常克制:
- 用JDK动态代理写出一个可运行的最小增强闭环;
- 用CGLIB动态代理覆盖“无接口”场景;
- 定义一套精简注解,模拟
@Aspect、@Before、@AfterReturning; - 完成切面注册、切点匹配、代理创建、通知执行这四个核心环节;
- 不用实现AspectJ的
execution表达式解析器,那是个独立且庞大的工程; - 不用实现完整的异常通知、环绕通知和复杂嵌套拦截链,先跑通主链路。
这个范围和Spring AOP的真实结构是对应的:Spring AOP底层也是“解析切面 -> 构建Advisor -> 创建代理 -> 在拦截器链中执行通知”,只是多了更多管理层次。手写之前我看ProxyFactory源码像看天书,手写之后再回看,基本能猜到每一段在干什么。
1.3 AOP的使用场景,手写之前先有个感性认知
AOP解决的痛点是“横切逻辑”散落各处。日志、权限校验、事务控制、接口耗时统计、异常兜底,这些逻辑如果每个业务方法都写一遍,代码会越来越脏。AOP的做法是把这些逻辑抽取成“通知”,在合适的“切点”上织入,让业务代码保持纯粹。
举个例子,一个下单接口既要记录入参日志,又要统计耗时,还要做方法级权限校验。不用AOP时,这些代码会出现在每一个service方法里;用了AOP后,业务Service里只有下单逻辑,日志、耗时、权限都是独立切面。这也就是为什么“AOP使用场景”会成为面试高频词。我在手写过程中更深地体会到,代理只是载体,真正核心的是“如何把横切逻辑与业务逻辑解耦”。
2. 动手之前,先把两个动态代理基本功补齐
2.1 JDK动态代理:以接口为边界的拦截机制
JDK动态代理的核心是java.lang.reflect.Proxy.newProxyInstance()。它要求目标类必须实现接口,然后动态生成一个“实现了这些接口”的代理类,所有接口方法的调用都会被转发到同一个InvocationHandler.invoke()方法里。
先看一个最小例子:
public class JdkProxyDemo { public interface UserService { void createUser(String name); } public static class UserServiceImpl implements UserService { @Override public void createUser(String name) { System.out.println("创建用户:" + name); } } public static void main(String[] args) { UserService target = new UserServiceImpl(); UserService proxy = (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), (proxyInstance, method, methodArgs) -> { System.out.println("前置通知:开始执行 " + method.getName()); Object result = method.invoke(target, methodArgs); System.out.println("后置通知:执行结束 " + method.getName()); return result; }); proxy.createUser("张三"); } }输出是:
前置通知:开始执行 createUser 创建用户:张三 后置通知:执行结束 createUser这段代码已经具备AOP的最小形态:通知写在InvocationHandler里,目标方法通过method.invoke(target, methodArgs)调用。但要注意一个关键点:JDK动态代理生成的代理类是Proxy的子类,同时实现了UserService接口,它和UserServiceImpl之间没有任何父子关系。所以代理对象只能强转成接口类型,不能强转成实现类。Java是单继承,代理类已经继承了Proxy,再想和原来的实现类建立关系,只能通过接口这个“契约”。
这也是JDK动态代理“为什么一定要求有接口”的根本原因:不是设计者偷懒,而是继承体系不允许代理类在继承Proxy的同时再去继承目标类。没有接口,就没有统一的调用入口,生成代理类就没有实现依据。
2.2 CGLIB动态代理:以继承为路径的增强路径
CGLIB走的是另一条路:它直接生成目标类的“子类”,子类重写目标类的非final方法,方法内部调用被转发到MethodInterceptor.intercept()。因为子类和目标类是父子关系,所以代理对象可以强转成目标类。
同样给一个最小例子:
public class CglibProxyDemo { public static class OrderService { public void createOrder(String orderNo) { System.out.println("创建订单:" + orderNo); } } public static void main(String[] args) { OrderService target = new OrderService(); Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(target.getClass()); enhancer.setCallback((MethodInterceptor) (obj, method, args, methodProxy) -> { System.out.println("CGLIB前置通知:" + method.getName()); Object result = methodProxy.invokeSuper(obj, args); System.out.println("CGLIB后置通知:" + method.getName()); return result; }); OrderService proxy = (OrderService) enhancer.create(); proxy.createOrder("A0001"); } }输出:
CGLIB前置通知:createOrder 创建订单:A0001 CGLIB后置通知:createOrder这里有个非常重要的细节:CGLIB回调中调用原始方法要用methodProxy.invokeSuper(obj, args),而不是method.invoke(obj, args)。因为obj是生成的代理子类实例,如果直接method.invoke(obj, args),等于在子类实例上再次触发方法调用,会再一次进入拦截回调,形成无限递归,最终栈溢出。invokeSuper走的是底层的FastClass机制,绕过了子类重写,直接调用父类逻辑。这个坑在手写过程中几乎必踩,后面我还会详细说。
由于CGLIB是生成子类,所以它的边界也很清楚:final类无法生成子类,final方法无法被重写,private方法子类不可见。这些方法无法被代理拦截。
2.3 Spring 6.0的代理选择与CGLIB重打包
既然是针对Spring 6.0,我补充一个环境层面的理解。Spring 6.0要求JDK 17以上,同时spring-core模块里已经重新打包了CGLIB,类路径变成了org.springframework.cglib.proxy.Enhancer这样的形式。也就是说,你不用再像旧项目一样单独引入cglib依赖,Spring内部已经维护了一份重打包版本,避免和外部CGLIB版本冲突。
代理选择上,Spring Framework的默认策略是:目标类实现了接口时,优先使用JDK动态代理;目标类没有实现接口时,自动降级到CGLIB。如果显式开启proxy-target-class=true,则不管有没有接口,一律使用CGLIB。Spring Boot从2.0开始默认就是proxy-target-class=true,所以现在大多数项目里跑起来的代理其实是CGLIB代理,只是很多人没察觉。
我把两种代理放在一起做了个对比表,方便记忆:
| 对比项 | JDK动态代理 | CGLIB动态代理 |
|---|---|---|
| 生成方式 | 生成实现目标接口的代理类 | 生成目标类的子类 |
| 必要条件 | 目标类必须有接口 | 目标类不能是final,方法不能是final |
| 强转实现类 | 不可以 | 可以 |
| 调用原始方法 | method.invoke(target, args) | methodProxy.invokeSuper(obj, args) |
| 典型使用场景 | 接口驱动的Spring Bean | 无接口类、默认proxy-target-class场景 |
| 常见局限 | 无法代理没有接口的类 | 无法代理final/private成员 |
这份对比看着简单,但每一条都能从机制上推导出来。手写一遍之后,这些结论不再是背诵项,而是自然推出的约束。
3. 手写第一步:JDK动态代理跑通方法增强的最小闭环
3.1 从demo出发,把通知抽象成独立接口
上面JDK动态代理的例子虽然能跑,但通知逻辑直接写在InvocationHandler里,太硬编码。手写AOP的第一步,是把通知抽出来变成可复用组件。我先定义了一个最简单的环绕通知接口:
public interface AroundAdvice { Object around(Object target, Method method, Object[] args) throws Throwable; }然后实现一个日志通知:
public class LogAroundAdvice implements AroundAdvice { @Override public Object around(Object target, Method method, Object[] args) throws Throwable { System.out.println("[手写AOP] 前置通知:" + method.getName()); Object result = method.invoke(target, args); System.out.println("[手写AOP] 后置通知:" + method.getName()); return result; } }再写一个通用代理工厂:
public class JdkAopProxy { @SuppressWarnings("unchecked") public static <T> T createProxy(T target, AroundAdvice advice) { return (T) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), (proxy, method, args) -> advice.around(target, method, args)); } }用法如下:
UserService service = JdkAopProxy.createProxy(new UserServiceImpl(), new LogAroundAdvice()); service.createUser("李四");输出:
[手写AOP] 前置通知:createUser 创建用户:李四 [手写AOP] 后置通知:createUser到这一步,我们已经把“给出目标对象和通知,生成一个增强后的代理对象”这条链路封装出来了。它离Spring AOP更进一步了:通知不再是散落在InvocationHandler里的代码,而是一个有名称、可传递的组件。
3.2 这段代码到底模拟了AOP的哪几个环节
很多人手写不清楚,是因为不知道眼前代码对应Spring里的什么概念。我画过一张对应关系表,非常有用:
| 手写代码 | Spring AOP里的对应物 |
|---|---|
AroundAdvice接口 | 通知(Advice) |
LogAroundAdvice实现类 | 具体的切面逻辑 |
JdkAopProxy | ProxyFactory/DefaultAopProxyFactory |
目标对象target | TargetSource(目标对象来源) |
Proxy.newProxyInstance | 创建代理对象的具体动作 |
Spring AOP比这段代码多的,主要是两点:一是切点匹配,它要知道这个通知应该作用在哪些方法上,而不是所有方法都增强;二是Advisor组装,Spring会把“切点 + 通知”打包成一个Advisor,再交给代理创建器。我写到这里时最大的感受是:AOP框架的核心骨架并不神秘,就是“找到目标、匹配方法、包装方法调用”。Spring只是在这条骨架外面套了大量的扩展点。
3.3 调试中遇到的第一个问题:代理对象不能强转成实现类
我还原了一个很多新手会踩的坑。假如UserServiceImpl里有一个独有的方法deleteUser(),你从代理对象拿回了一个UserService引用,然后试图强转成UserServiceImpl调用这个独有方法:
UserServiceImpl impl = (UserServiceImpl) service; // 这里会抛ClassCastException原因在前面已经说过:JDK动态代理生成的代理类是Proxy的子类,同时实现了UserService接口,但它和UserServiceImpl没有任何继承关系。Java类型转换要求两个类之间存在父子关系或接口实现关系,这里显然不满足。
这个坑在Spring里也很常见。早期Spring项目如果没开proxy-target-class,Controller注入一个Service接口,它拿到的其实是JDK代理对象,这时候如果代码里强行把接口对象转成实现类,就会遇到同一个ClassCastException。Spring Boot 2.0默认启用CGLIB后,这种情况少了,但原理依然是原理。
4. 手写第二步:用CGLIB动态代理覆盖无接口场景
4.1 在Spring 6.0里正确引入Enhancer
JDK动态代理的局限很明显:目标类没有接口时,它无能为力。实际项目里很多Service类并不实现接口,尤其是你用Spring Boot写业务,大部分类都是“一个类直接干活”的风格。这时候就需要CGLIB。
在Spring 6.0环境下,我使用的是Spring重打包后的CGLIB:
import org.springframework.cglib.proxy.Enhancer; import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.cglib.proxy.MethodProxy;如果脱离Spring写纯Java项目,用原版net.sf.cglib.proxy.Enhancer也行,API基本一致。区别只是包名不同。
4.2 无接口类上的完整代理工厂
我的目标类是一个没有接口的OrderService:
public class OrderService { public void createOrder(String orderNo) { System.out.println("创建订单:" + orderNo); } }代理工厂如下:
public class CglibAopProxy { public static Object createProxy(Object target, AroundAdvice advice) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(target.getClass()); enhancer.setCallback((MethodInterceptor) (obj, method, args, methodProxy) -> { System.out.println("[手写CGLIB AOP] 前置通知:" + method.getName()); Object result = methodProxy.invokeSuper(obj, args); System.out.println("[手写CGLIB AOP] 后置通知:" + method.getName()); return result; }); return enhancer.create(); } }调用方式:
OrderService proxy = (OrderService) CglibAopProxy.createProxy(new OrderService(), new LogAroundAdvice()); proxy.createOrder("B0002");输出:
[手写CGLIB AOP] 前置通知:createOrder 创建订单:B0002 [手写CGLIB AOP] 后置通知:createOrder对比JDK版本的代码,调用方几乎感觉不到差别,但内部机制完全不同。这里proxy确实是OrderService的一个子类,所以可以安全强转成OrderService,也可以调用它继承下来的公有方法。
4.3 CGLIB的局限与性能印象
手写了CGLIB这一版之后,我对它的边界条件印象深了很多:
final类没法生成子类,所以final类不能被CGLIB代理;final方法不会被子类重写,所以final方法不会被拦截;private方法对子类不可见,也不会被拦截;- 目标类如果有无参构造器,Enhancer在生成子类时需要走构造逻辑,构造器里的副作用可能会重复执行,这是一个容易被忽略的坑。
至于性能,过去的说法是“CGLIB代理生成慢、调用快,JDK代理生成快、反射调用慢”。但JDK 17对动态代理做了不少优化,两种代理的性能差距已经不像早期那么明显。选谁更多是看使用的场景和框架配置,不必因为性能传说而焦虑。
5. 手写第三步:组装一个简化版AOP容器
5.1 设计目标:把“通知”变成可配置的声明式切面
到目前位置,通知是手动传给代理工厂的,每次创建代理都要自己指定增强逻辑,这还谈不上AOP框架。AOP的精髓是“声明式”:我在切面类上写一个@Before注解,容器就能自动识别,凡是匹配某个切点的方法,都自动被增强。
所以我决定手写一个简化版AOP容器,目标如下:
- 用一个
@MyAspect注解标记切面类; - 用
@MyBefore("方法名")和@MyAfterReturning("方法名")标记通知方法; - 容器注册切面之后,
createProxy(target)自动判断用JDK还是CGLIB; - 调用目标方法时,容器自动匹配切点并执行对应通知。
切点匹配我就先用“方法名相等”这种最简策略,因为AspectJ表达式解析不是这次手写的重点。重点是先把Spring AOP的容器组织方式复刻出来。
5.2 定义注解和通知结构
先定义四个注解:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface MyAspect { }@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MyBefore { String value(); }@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MyAfterReturning { String value(); }@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface MyOrder { int value() default Integer.MAX_VALUE; }然后定义一个通知描述类,它承担Spring中Advisor的一部分职责:把切面实例、通知方法、切点表达式、通知类型打包到一起。
public class AdviceDefinition { public enum AdviceType { BEFORE, AFTER_RETURNING } private final Object aspectInstance; private final Method adviceMethod; private final String pointcut; private final AdviceType type; private final int order; public AdviceDefinition(Object aspectInstance, Method adviceMethod, String pointcut, AdviceType type, int order) { this.aspectInstance = aspectInstance; this.adviceMethod = adviceMethod; this.pointcut = pointcut; this.type = type; this.order = order; } public Object getAspectInstance() { return aspectInstance; } public Method getAdviceMethod() { return adviceMethod; } public String getPointcut() { return pointcut; } public AdviceType getType() { return type; } public int getOrder() { return order; } }真实Spring里对象是Advisor = Pointcut + Advice,我这里用AdviceDefinition直接装“切点+通知方法”,思想是一样的。通知方法目前设计成无参,真实Spring中方法可以接收JoinPoint参数,手写阶段先不引入这个复杂度。
5.3 切面扫描:把注解变成通知列表
容器的核心是一个SimpleAopContainer,它维护一份通知列表,并负责切面注册和代理创建。
先看注册切面这一段:
public class SimpleAopContainer { private List<AdviceDefinition> advices = new ArrayList<>(); public void registerAspect(Object aspect) { Class<?> clazz = aspect.getClass(); if (!clazz.isAnnotationPresent(MyAspect.class)) { throw new IllegalArgumentException("该对象没有标注@MyAspect"); } int order = resolveOrder(clazz); for (Method method : clazz.getDeclaredMethods()) { MyBefore before = method.getAnnotation(MyBefore.class); if (before != null) { advices.add(new AdviceDefinition(aspect, method, before.value(), AdviceDefinition.AdviceType.BEFORE, order)); } MyAfterReturning after = method.getAnnotation(MyAfterReturning.class); if (after != null) { advices.add(new AdviceDefinition(aspect, method, after.value(), AdviceDefinition.AdviceType.AFTER_RETURNING, order)); } } advices.sort(Comparator.comparingInt(AdviceDefinition::getOrder)); } private int resolveOrder(Class<?> clazz) { MyOrder order = clazz.getAnnotation(MyOrder.class); return order == null ? Integer.MAX_VALUE : order.value(); } }这段代码做的事情,对应Spring里AnnotationAwareAspectJAutoProxyCreator的工作:扫描切面类,解析注解方法,生成一批Advisor。Real Spring里还有一个切点表达式解析器,我这里直接读注解上的字符串。
5.4 自动代理工厂:JDK和CGLIB二选一
注册完切面之后,createProxy负责创建代理对象。判断逻辑很简单:有接口就走JDK动态代理,没有接口就走CGLIB。这也对应SpringDefaultAopProxyFactory.createAopProxy()的默认行为。
public Object createProxy(Object target) { Class<?> targetClass = target.getClass(); if (targetClass.getInterfaces().length > 0) { return createJdkProxy(target); } return createCglibProxy(target); } private Object createJdkProxy(Object target) { return Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), (proxy, method, args) -> invokeWithAdvice(target, method, args)); } private Object createCglibProxy(Object target) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(target.getClass()); enhancer.setCallback((MethodInterceptor) (obj, method, args, methodProxy) -> invokeWithAdvice(target, method, args)); return enhancer.create(); }真正的通知执行逻辑在invokeWithAdvice里:
private Object invokeWithAdvice(Object target, Method method, Object[] args) throws Throwable { List<AdviceDefinition> beforeList = findMatchedAdvices(AdviceType.BEFORE, method); List<AdviceDefinition> afterList = findMatchedAdvices(AdviceType.AFTER_RETURNING, method); for (AdviceDefinition advice : beforeList) { advice.getAdviceMethod().invoke(advice.getAspectInstance()); } Object result = method.invoke(target, args); for (AdviceDefinition advice : afterList) { advice.getAdviceMethod().invoke(advice.getAspectInstance()); } return result; } private List<AdviceDefinition> findMatchedAdvices(AdviceType type, Method method) { return advices.stream() .filter(d -> d.getType() == type && d.getPointcut().equals(method.getName())) .collect(Collectors.toList()); }到这里,一个“注解驱动的简化AOP容器”就成形了。写一个切面测试:
@MyAspect @MyOrder(1) public class LogAspect { @MyBefore("createOrder") public void beforeCreateOrder() { System.out.println("[日志切面] createOrder 开始"); } @MyAfterReturning("createOrder") public void afterCreateOrder() { System.out.println("[日志切面] createOrder 结束"); } }启动:
SimpleAopContainer container = new SimpleAopContainer(); container.registerAspect(new LogAspect()); OrderService proxy = (OrderService) container.createProxy(new OrderService()); proxy.createOrder("T2026-001");输出:
[日志切面] createOrder 开始 创建订单:T2026-001 [日志切面] createOrder 结束这个效果已经非常接近Spring AOP:切面类用注解声明,容器负责扫描、匹配、代理和通知执行。
5.5 自调用失效:手写框架第一时间暴露的坑
手写框架跑通之后,我马上撞上了一个经典问题:自调用失效。
场景是这样的,TradeService内部一个方法直接调用了另一个被通知标记的方法:
public class TradeService { @MyBefore("createTrade") public void createTrade() { System.out.println("创建交易"); this.cancelTrade(); } @MyBefore("cancelTrade") public void cancelTrade() { System.out.println("内部取消交易"); } }我用容器创建代理后调用createTrade(),输出是:
[日志切面] createTrade 开始 创建交易 内部取消交易注意,cancelTrade()上的@MyBefore("cancelTrade")没有生效。原因很简单:createTrade里的this.cancelTrade()调用的目标是原始对象,不是代理对象。代理只包裹了外部入口,内部this调用永远绕过了代理。
Spring AOP也有完全一样的问题。这也是很多人在同一个类里调用带事务、带缓存注解的方法时,发现注解不生效的根本原因。解决办法通常有三种:把内部调用改成注入自身后通过代理调用;使用AopContext.currentProxy()并且开启exposeProxy = true;或者干脆把自调用设计成跨Bean调用,让调用入口从代理进入。手写框架时把这个坑踩一遍,后面再遇到Spring里的“注解不生效”,你一眼就能判断是不是这个原因。
6. 手写过程中真实踩到的坑和进阶理解
6.1 CGLIB回调里错误调用method.invoke导致的递归崩溃
我前面说过,CGLIB回调里调用原始方法应该用methodProxy.invokeSuper(obj, args)。但我第一次写的时候就写成了method.invoke(obj, args),程序直接StackOverflowError。
原因是:obj是代理子类的实例,method.invoke(obj, args)会触发子类里被重写的方法,而这个重写方法又会进入intercept回调,于是无限循环。使用methodProxy.invokeSuper(obj, args)则走的是FastClass机制,直接调用父类方法,绕过子类重写,所以不会再次进入拦截器。
JDK动态代理里没有这个坑,因为JDK的method.invoke(target, args)传入的是最原始的目标对象,不是代理对象。两个代理机制代码风格相似,但仅仅这一个内部细节不同,就能让不熟悉的人排查很久。
6.2 多个切面的顺序:没有Order时顺序完全失控
我又加了一个权限切面,和日志切面一起注册,都拦截createOrder。结果发现执行顺序完全取决于注册顺序,毫无确定性。这个顺序问题在手写时会被无限放大。
Spring里通过Ordered接口或@Order注解控制切面顺序,数值越小优先级越高。前置通知按切面顺序依次执行,后置通知在真实Spring调用链里会出现逆序效果。我的简化容器里目前只是简单排序,但它至少让我明白了一个事实:AOP的顺序不是凭空出现的,需要显式设计。
@MyOrder(1)这种小细节,真实项目里经常是bug源头。两个切面一个做权限,一个做日志,如果权限切面排后面,可能会导致未授权请求也先打了日志,甚至执行了部分业务逻辑。手写之后我看任何切面代码,第一反应都是关心它的Order值。
6.3 切点表达式:手写框架做到什么程度算合格
我的简化容器用method.getName().equals(pointcut)做切点匹配,这是最简陋的版本。真实Spring AOP使用的是AspectJ切点表达式,类似:
execution(public * com.example.trade.service.TradeService.createTrade(..))这个表达式的意思是:匹配com.example.trade.service.TradeService类中名为createTrade的public方法,参数随意。execution只是AspectJ表达式类型之一,还有其他如within、annotation等。
手写AOP时,要不要自己实现一个执行表达式解析器?我的建议是不要。AspectJ表达式解析是独立工程,涉及AST解析、类型信息匹配,已经超出了“理解AOP核心机制”的范围。手写阶段能读懂表达式、知道表达式最终会被解析成切点对象,就已经合格了。真的需要做完整切点匹配,直接把AspectJ的切点解析器作为依赖引进来,而不是重复造轮子。
6.4 Spring AOP和AspectJ,两件容易混的事
有段时间我一直以为Spring AOP底层就是AspectJ。手写之后彻底搞清了边界:Spring AOP是运行时织入,基于动态代理,只在Spring容器管理的Bean上生效;AspectJ是编译期或加载期织入,通过AspectJ编译器把切面代码直接织进字节码,适用范围远大于Spring容器。
Spring AOP只是借用了AspectJ的注解语法和切点表达式解析能力,真正执行织入动作的,仍然是JDK动态代理或CGLIB动态代理。所以严格来说,Spring AOP是“使用AspectJ风格的切面模型,但用代理机制实现”。面试时能把这个边界讲清楚,比单纯背“Spring AOP基于动态代理”要加分很多。
6.5 回看源码:手写之后再读Spring的落点
手写完之后,我又回去翻了Spring源码,几个关键落点变得非常清晰:
AbstractAutoProxyCreator.postProcessAfterInitialization:Bean初始化完成后,Spring会在这里判断是否需要创建代理;AnnotationAwareAspectJAutoProxyCreator:负责处理@AspectJ注解的切面,生成Advisor;DefaultAopProxyFactory.createAopProxy:根据目标和配置决定用JDK还是CGLIB;ReflectiveMethodInvocation:拦截器链的调用入口,通知按照Advisor顺序依次执行。
Spring AOP的织入流程,简单说就是:容器启动时扫描所有切面,解析成Advisor列表;每个Bean初始化完成后,判断它是否匹配某个Advisor;如果匹配,就创建代理对象并替换原Bean;后续从容器里拿到的都是代理,方法调用会被拦截器链处理。
这个流程和我手写的SimpleAopContainer思路几乎一一对应,区别只是Spring的管理层次更精细、扩展点更多、切点表达式更强大。手写一遍再去读源码,哪怕只是扫一眼类名和关键方法,你也能猜到代码大概在干什么,而不是像无头苍蝇一样乱翻。
整个过程走下来,我最有价值的体会是:JDK动态代理和CGLIB动态代理本身并不难,难的是把“切点”、“通知”、“切面顺序”、“代理选择”、“自调用失效”这些元素组织成一条可靠的执行链路。如果你也卡在AOP原理上,别急着背更多结论,按我这个路径从JDK代理写到简化容器,踩一遍递归、自调用和Order的坑,再回来看Spring的源码,你会觉得每一行都不陌生。