news 2026/10/1 4:12:00

手写AOP核心链路:从JDK动态代理到CGLIB破解Spring AOP底层

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手写AOP核心链路:从JDK动态代理到CGLIB破解Spring AOP底层

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实现类具体的切面逻辑
JdkAopProxyProxyFactory/DefaultAopProxyFactory
目标对象targetTargetSource(目标对象来源)
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的源码,你会觉得每一行都不陌生。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 4:11:58

多智能体AI重构药物研发数据:3.7万Agent实战拆解

做药物研发数据的人&#xff0c;应该都体会过那种无力感&#xff1a;明明数据库里躺着上万项临床试验&#xff0c;真到立项决策时&#xff0c;却翻不出几条能直接支撑判断的信息。不是数据少&#xff0c;是数据太散、太乱、格式太任性。最近Science刊出的多智能体AI重构早期药物…

作者头像 李华
网站建设 2026/10/1 4:11:57

风光储互补微电网Simulink仿真建模全流程解析

组网容易&#xff0c;仿真正经跑通难。风光储互补微电网的Simulink仿真&#xff0c;这几年不管是毕设、华为杯还是工程预研&#xff0c;都成了高频需求。但很多刚上手的人一打开MATLAB就懵了&#xff1a;光伏、风机、储能、PCC&#xff0c;一大堆模块往哪儿摆&#xff1f;控制策…

作者头像 李华
网站建设 2026/10/1 4:11:40

黑烟车识别毕设全流程:YOLOv8训练与视频时序检测实战

简介&#xff1a;一套完整的计算机视觉毕业设计项目包&#xff0c;聚焦基于深度学习的黑烟车自动识别。面向计算机视觉方向高校毕业生、目标检测学习者和智慧环保项目开发者&#xff0c;针对黑烟车人工监管成本高、效率低的痛点&#xff0c;提供从数据标注、图像增广、模型搭建…

作者头像 李华
网站建设 2026/10/1 4:11:16

深度强化学习交易实战:拆解DeepTrader源码中的DQN与奖励机制

简介&#xff1a;面向量化交易与投资组合管理方向的研究者&#xff0c;DeepTrader源代码复现包解决的是如何利用深度强化学习实现风险收益平衡的组合管理问题&#xff0c;底层方法可对照参考论文《DeepTrader: A Deep Reinforcement Learning Approach for Risk-Return Balance…

作者头像 李华
网站建设 2026/10/1 4:10:57

SWAT模型全局敏感性分析:Sobol与PAWN对比及Matlab实现

第一次用SWAT去率定一个200多平方公里的流域&#xff0c;我心里确实是发毛的。参数太多&#xff0c;而且很多参数在物理意义上是重叠的&#xff0c;你改这个和改那个&#xff0c;模拟结果可能差不多&#xff0c;根本分不清是谁在起作用。手动试错试了三天&#xff0c;算出的NSE…

作者头像 李华
网站建设 2026/10/1 4:10:53

彼得·林奇实地调研法:用生活常识挖掘潜力股

买你了解的股票&#xff0c;这句话说出来容易&#xff0c;真落到日常操作上&#xff0c;能做到的人少得可怜。彼得林奇的“实地调研”方法&#xff0c;本质上就是把这句话变成一套可以执行、可以复用的动作。它不是什么神秘的内幕消息渠道&#xff0c;也不是让你去当侦探&#…

作者头像 李华