Spring AOP 内核:自动代理创建器的秘密

bee2026-10-0865 分钟0 次阅读
为什么打上 @Aspect 就能生效?答案藏在 AnnotationAwareAspectJAutoProxyCreator 里。拆解它如何找到切面、筛选 Advisor、选择合适的代理工厂,并给出「切面不生效」的完整自查清单。
1 / 117
小节
〇、30 秒看懂
2 / 117

上一篇讲的是「切面怎么写」,这一篇只回答一个问题:你明明只加了个 @Aspect,容器凭什么就把你的 Bean 换成了代理?是谁换的、在第几步换的? 答案是一个名字很长的组件 AnnotationAwareAspectJAutoProxyCreator,整篇文章就是在拆它的四条动作:它是个后置处理器 → 它会找候选顾问 → 它逐个求值切点 → 命中就造代理并把原对象掉包。

3 / 117

先把这五个词按「完全没写过 Java」的口径钉死。BeanPostProcessor——容器在造每个 Bean 前后插进来的钩子,只有两个方法,但它的返回值能替换掉原本那个 Bean。Advisor(顾问)——一个「切点 + 通知」的配对对象,是 Spring 内部真正使用的单位;你的 @Aspect 会在启动时被拆成好几个 Advisor。切点(pointcut)——判断「这个方法的签名归不归我管」的那段表达式。目标对象(target)与代理(proxy)——前者是你 new 出来的原始实例,后者是套在它外面的那层壳,壳里存着 target。织入(weaving)——把通知接到调用路径上的动作,Spring AOP 靠「运行期造代理」来完成织入。

4 / 117
类比

工厂流水线末端的质检台讲的是 BeanPostProcessor。流水线上每一台产品都要照常走完全部工序,但在装箱前会经过一个质检台,质检员有权把你的那台换成同型号加装了防盗芯片的一台。postProcessAfterInitialization 就是「装箱前最后一眼」这个权力:它返回什么,货架上(单例池)就摆什么。所以你在别处注入到的、getBean 取到的,全都是被换过的那台——原始那台并没有报废,它被塞进新机器肚子里当核心零件(target)。

5 / 117
类比

给手机套壳讲的是「代理替换原对象」。手机(业务对象)已经组装完工、开机自检也跑完了,才在装盒那一刻被套上壳(代理)。壳不改变手机里的任何一颗芯片,只是所有按键都得从壳上按一遍(多一层转发)。三件事必须一起记:壳只能套在从流水线上下来的手机上(自己 new 的对象没人给它套壳)、壳太厚时套不上(final 类无法继承)、手机自己呼叫自己时绕过了壳(同类自调用)。

6 / 117
架构图
图 · 本篇地图:一次代理创建的调用时间轴
图 · 本篇地图:一次代理创建的调用时间轴
7 / 117

学完这一节,你要能回答三个问题:

8 / 117
  1. 代理是在 Bean 生命周期的哪一步生成的?为什么这一步决定了「@PostConstruct 里调自己的方法没有通知」?
  2. 一个 @Aspect 类是怎么被「拆」成 Advisor 的?候选 Advisor 为空意味着什么?
  3. 看到 Bean is not yet fully initialized、Cannot proxy final class 这类报错,该沿着上面这条时间轴的哪一站去找?
9 / 117
小节
一、一行注解背后的 import 链
10 / 117

上一节我们把代理工厂拆到了 DefaultAopProxyFactory,但还有最后一个问题没回答:代理到底是谁、在什么时候创建的? 你已经写好了 @Aspect,也核对过切点,可容器凭什么知道要把某个 Bean 换成代理?

11 / 117

入口就是我们习惯性加上去的那一行注解。

12 / 117
代码对照
代码java
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Import(AspectJAutoProxyRegistrar.class)   // 关键:导入一个「注册器」public @interface EnableAspectJAutoProxy {    boolean proxyTargetClass() default false;    boolean exposeProxy() default false;}
解读
  • @Import 是 Spring 装配的「手动挡」:它让一个注解可以直接往容器里塞进一个类
  • AspectJAutoProxyRegistrar 实现的是 ImportBeanDefinitionRegistrar,能在解析阶段拿到 BeanDefinitionRegistry
  • 它其实只做一件事:把 AnnotationAwareAspectJAutoProxyCreator 注册成一个 BeanDefinition
13 / 117
代码对照
代码java
// AspectJAutoProxyRegistrar 的核心(简化)public void registerBeanDefinitions(        AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {    // 1. 容器里还没有的话,就注册一个自动代理创建器    AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);    // 2. 把注解上的属性回填到创建器的 definition 上    AnnotationAttributes attrs = AnnotationConfigUtils.attributesFor(            importingClassMetadata, EnableAspectJAutoProxy.class);    if (attrs.getBoolean("proxyTargetClass")) {        AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);    }    if (attrs.getBoolean("exposeProxy")) {        AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);    }}
解读

提示:Spring Boot 下这一步是自动完成的。AopAutoConfiguration 一旦发现类路径上有 AspectJ 支持,就会替你把上面那行 @Import 补上(对应 spring.aop.auto=true),所以 Boot 项目里你从来不用手写 @EnableAspectJAutoProxy。

14 / 117

到这里必须点破它的真实身份:AnnotationAwareAspectJAutoProxyCreator 不是一个主动扫描的组件,而是一个 BeanPostProcessor——它不创造 Bean,只在别的 Bean 的创建流程里「顺手插一杠子」。更准确地说,它还实现了 BeanFactoryAware,因为要拿到 BeanFactory,才能引用容器里其他 Bean。

15 / 117
小节
二、继承体系:它究竟是谁
16 / 117

把那个长名字一层层剥开,你会发现每一层能力都来自一个清晰的接口:

17 / 117
对照表
层级(接口 → 实现)这一层负责什么
BeanPostProcessor在 Bean 初始化前后拿到回调,所有「偷偷加工 Bean」的起点
InstantiationAwareBeanPostProcessor多出实例化前后、属性填充前的回调
SmartInstantiationAwareBeanPostProcessor多出「提前暴露引用」的能力,用于解决循环依赖
AbstractAutoProxyCreator真正实现「判断并创建代理」的基类
AspectJAwareAdvisorAutoProxyCreator按 @Order 对同一目标上的 Advisor 排序
AnnotationAwareAspectJAutoProxyCreator增加「解析 @AspectJ 注解」的能力,就是我们在用的这个
18 / 117

从下往上看,正好是一条能力递进的路线:从「能收到回调」到「能感知 Bean 的存在」,再到「能把 Bean 换成代理对象」。这也是 Spring 选择用 BeanPostProcessor 做切入点的原因——它只需要在标准生命周期流程里挂一个回调,无需改写容器的创建逻辑。

19 / 117
代码对照
代码java
public interface BeanPostProcessor {    // 初始化之前(@PostConstruct 之前)    default Object postProcessBeforeInitialization(Object bean, String beanName) {        return bean;    }    // 初始化之后(@PostConstruct 之后)——自动代理就发生在这里    default Object postProcessAfterInitialization(Object bean, String beanName) {        return bean;    }}
解读

要点:postProcessAfterInitialization 的返回值会替换容器里原本的 Bean。一旦这里返回代理,别人注入到的就是代理,原始对象被藏在 SingletonTargetSource 里——这正是「who wraps whom」的答案。

20 / 117
小节
三、代理创建时机:初始化之后的「顺手一包」
21 / 117

把所有环节串起来,主入口是 postProcessAfterInitialization,它随即调用 wrapIfNecessary:

22 / 117
java
// AbstractAutoProxyCreator(简化)public Object postProcessAfterInitialization(Object bean, String beanName) {    if (bean != null) {        Object cacheKey = getCacheKey(bean.getClass(), beanName);        // 循环依赖场景下已经提前暴露过代理,这里不要重复包装        if (this.earlyProxyReferences.remove(cacheKey) != bean) {            return wrapIfNecessary(bean, beanName, cacheKey);        }    }    return bean;}
23 / 117
java
// wrapIfNecessary(简化)——判断「要不要代理」的核心protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {    if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {        return bean;   // 已经是内部目标对象,避免套娃    }    if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {        return bean;   // 之前判定过「不需要代理」,直接用缓存结果    }    if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {        this.advisedBeans.put(cacheKey, Boolean.FALSE);        return bean;   // 切面自己、以及基础设施类不代理    }    // 找出所有能作用在当前 Bean 上的 Advisor    Object[] specificInterceptors =            getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);    if (specificInterceptors != DO_NOT_PROXY) {        this.advisedBeans.put(cacheKey, Boolean.TRUE);        Object proxy = createProxy(                bean.getClass(), beanName, specificInterceptors,                new SingletonTargetSource(bean));        this.proxyTypes.put(cacheKey, proxy.getClass());        return proxy;   // 返回代理,替换原始实例    }    this.advisedBeans.put(cacheKey, Boolean.FALSE);    return bean;}
24 / 117
架构图
图 1 · 自动代理创建器的调用链
图 1 · 自动代理创建器的调用链
25 / 117
  • 第一道闸是缓存:advisedBeans 记住「这个类要不要代理」,避免每个 Bean 都重复求值切点
  • 第二道闸是基础设施类:代理创建器自己、Advisor、Pointcut 这些不参与匹配,否则会自我缠绕
  • 第三道闸是 getAdvicesAndAdvisorsForBean:它是真正「找顾问」的地方,内部会调用 findEligibleAdvisors,把候选 Advisor 过滤成命中当前类的那一份
  • 命中后进入 createProxy,由 ProxyFactory 接手,产出代理并返回给容器
26 / 117

findEligibleAdvisors 的过滤逻辑并不神秘,无非「先拿全部候选,再逐个用切点去试」:

27 / 117
代码对照
代码java
// findEligibleAdvisors(简化)protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {    // 候选:所有 AspectJ Advisor + 所有 Advisor 类型的 Bean(比如 @Transactional)    List<Advisor> candidateAdvisors = findCandidateAdvisors();    // 逐个用 ClassFilter / MethodMatcher 判定,只留下能命中当前类的    List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);    extendAdvisors(eligibleAdvisors);   // 保证 exposeInvocation / 排序等扩展点    if (!eligibleAdvisors.isEmpty()) {        eligibleAdvisors = sortAdvisors(eligibleAdvisors);   // 按 @Order 排序    }    return eligibleAdvisors;}
解读
  • findCandidateAdvisors() 会去问 BeanFactoryAdvisorRetrievalHelper:容器里所有 Advisor 类型的 Bean 都算候选
  • findAdvisorsThatCanApply 才是真正的「切点匹配」,用 AopUtils.canApply 逐个比对
  • sortAdvisors 决定最终通知链的顺序——注意这里已经排好了序,运行期只是照单执行
28 / 117

读到这里可以把两条线分开了:左边这一列发生在启动、每个 Bean 只有一次;右边这一列发生在每一次调用。第九节与第十一节的实验都在右边那一列上,而第六节那张「切面不生效自查清单」基本都在左边那一列里:

29 / 117
架构图
图 · 两条时间线:创建期与调用期
图 · 两条时间线:创建期与调用期
30 / 117
小节
四、Advisor 的诞生:@Aspect 是怎么被「拆」成顾问的
31 / 117

候选 Advisor 里的 AspectJ 部分,来自 AnnotationAwareAspectJAutoProxyCreator 委托的 ReflectiveAspectJAdvisorFactory。它逐一读取切面类里的方法,把带通知注解的挑出来:

32 / 117
代码对照
代码java
// ReflectiveAspectJAdvisorFactory(简化)public List<Advisor> getAdvisors(MetadataSource metadataSource) {    Class<?> aspectClass = metadataSource.getAspectClass();    List<Advisor> advisors = new ArrayList<>();    // 1. 先解析 @Pointcut 方法,建立「切点名 → Pointcut」的映射    for (Method method : aspectClass.getMethods()) {        if (isPointcut(method)) {            // 注册到 aspectJAdvisors 的切点缓存        }    }    // 2. 再处理通知方法:@Before / @Around / @After 等    for (Method method : getAdvisorMethods(aspectClass)) {        Advisor advisor = getAdvisor(method, metadataSource, aspectInstanceFactory);        if (advisor != null) {            advisors.add(advisor);        }    }    return advisors;}
解读
  • 每个通知方法都会被打包成一个 InstantiationModelAwarePointcutAdvisorImpl
  • 这个 Advisor 内部持有两部分:Pointcut(在哪儿拦)+ Advice(拦下来做什么)
  • 通知方法的形参绑定(JoinPoint、returning、throwing)也在这里被解析成 Pointcut 的一部分

说明:这也是为什么 returning = "result" 写错不会编译失败——它到运行期构建 Advisor 时才被解析,绑定不上就在此时报错。切面能启动、日志却报绑定失败,多半是这一幕。

33 / 117
小节
五、代理工厂的选择:一次「二选一」
34 / 117

命中之后,createProxy 交给 ProxyFactory。ProxyFactory 的继承链很短,核心能力都在父类 AdvisedSupport 上:

35 / 117
text
AdvisedSupport        # 保存 target、advisors、interfaces、proxyTargetClass  └─ ProxyFactory     # 对外入口,暴露 getProxy()        └─ AopProxyFactory(接口)              └─ DefaultAopProxyFactory   # 在 JDK / CGLIB 之间二选一
36 / 117
对照表
要素JDK 动态代理CGLIB
代理类形态实现目标接口的 $Proxy0继承目标类的子类
前置条件目标必须有接口目标类与方法不能是 final
触发条件hasNoUserSuppliedProxyInterfaces 为假强制 proxyTargetClass 或没有可用接口
产物JdkDynamicAopProxyCglibAopProxy
37 / 117
代码对照
代码java
// DefaultAopProxyFactory.createAopProxy(简化)public AopProxy createAopProxy(AdvisedSupport config) {    if (config.isOptimize() || config.isProxyTargetClass()            || hasNoUserSuppliedProxyInterfaces(config)) {        return new CglibAopProxy(config);   // 强制 CGLIB,或目标没有可用接口    }    return new JdkDynamicAopProxy(config);  // 有接口且未强制 -> JDK}
解读

提醒:Spring Boot 2.x 起 proxy-target-class 默认为 true,所以 Boot 项目里你看到的基本都是 CGLIB 代理。这不是换了引擎,只是选型默认值变了。

38 / 117
小节
六、切面不生效自查清单
39 / 117

「明明写了切面却毫无反应」的头号难题,按这张表逐一排查,八成能定位:

40 / 117
对照表
症状根因验证方法
被别的 Bean 调用生效,自己内部调用失效内部自调用绕过代理抽出到另一个 Bean;或开 exposeProxy 后 AopContext.currentProxy()
private / final / static 方法通知完全不触发这些方法天生不可代理看方法修饰符;或确认是否 CGLIB 的 final 类
启动无报错,一个方法都没被拦切点表达式写错(包层级 / 签名)开 logging.level.org.springframework.aop=DEBUG 看匹配日志
目标对象是容器外 new 出来的非容器 Bean 不会被后置处理器加工让容器管理(@Component / @Bean)
@Aspect 类从没被注册成 Bean少了 @Component,创建器找不到它检查是否被组件扫描覆盖
需要自调用但拿不到自身代理未开启 exposeProxy@EnableAspectJAutoProxy(exposeProxy = true)
41 / 117
java
// 自调用场景的正解之一:暴露代理后用 AopContext 取自身代理@EnableAspectJAutoProxy(exposeProxy = true)   // 必须先开启public void outer() {    // 别写 this.inner()——那是原始对象,不经过代理    ((OrderService) AopContext.currentProxy()).inner();}
42 / 117

排查的第一手段永远是日志。打开 AOP 的 DEBUG,创建器会打印每个 Bean 命中了哪些 Advisor、用了哪种代理类型;切点没匹配上时,还会写出「does not match」的具体原因。这比盯着 execution 猜要快得多。

43 / 117
小节
七、与 AspectJ 编译期织入的区别
44 / 117

很多人把 Spring AOP 和 AspectJ 混为一谈,其实它们是两套机制:

45 / 117
对照表
维度Spring AOPAspectJ
织入时机运行期,由 BeanPostProcessor 动态生成代理编译期 / 类加载期,直接改写字节码
能力范围只能拦 Spring 容器里的 Bean 方法能拦字段访问、构造器、静态方法、容器外对象
性能代理转发有少量运行开销织入后相当于普通代码,无代理开销
依赖无需额外编译步骤需要 AspectJ 编译器或 javaagent
典型用途业务切面:日志、事务、权限深度监控、性能剖析、非 Spring 对象增强
46 / 117

一句话区分:Spring AOP 是「代理级别的横切」,AspectJ 是「字节码级别的横切」。日常业务用 Spring AOP 足够;当你要拦私有方法、字段访问或容器外的对象时,才需要 AspectJ 织入。

47 / 117
小节
八、坑:@Transactional 与自定义切面顺序混乱
48 / 117
坑

@Transactional 本质也是一个 Advisor,和你的自定义切面在同一条通知链上竞争。如果顺序配错,会得到一种极其诡异的 bug——事务「不生效」。典型现场:日志切面 @Order(1) 排在最外层,事务切面在内层;日志切面的 @Around 在 try/catch 里吞掉了异常,于是异常根本没传到外层事务切面,事务自然不回滚。记住一条规律:@Order 数字越小越外层,越外层越先进入、最后离开;谁有资格「吞异常」,谁就不能排在事务外面。需要修复时,把事务切面的顺序调到最外层(更小的 @Order),并让所有环绕通知原样向上抛异常。

49 / 117
小节
九、动手体验:代理类型与通知链顺序
50 / 117

下面这个演示把代理的创建与通知链的组装可视化出来。切换代理类型,观察「谁包着谁」,再体会一下通知链的顺序:

51 / 117
内核实验
TeaVM代理创建与通知链内核演示未启动
观察代理类型与通知链顺序,体会「who wraps whom」
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
52 / 117
原理动画
动图 · 从 @Aspect 到可执行代理
动图 · 从 @Aspect 到可执行代理
53 / 117

代理造好之后,通知链是怎么「跑」的?advices 这一支把五种通知挂在一次调用上,你会看到它们其实是链表里的五个元素,而不是一句「先后顺序」:

54 / 117
内核实验
TeaVM通知链就是那条 interceptor 链表未启动
normal 看链表怎么一路走到底;切 order 看两个切面在链表里怎么穿插
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
55 / 117

动画先给全景——游标怎么从 0 一路加到链表末尾,再逐层退回:

56 / 117
原理动画
动图 · proceed() 递归:通知链表怎么走完
动图 · proceed() 递归:通知链表怎么走完
57 / 117

真正把断点打进去看,才明白「先进后出」不是口诀而是递归的必然。下面是 Spring 内部那几行的单步台,右边同步刷新 currentInterceptorIndex——它每加一次,就有一层通知进入;它等于链表长度时,才换目标方法出场:

58 / 117
单步调试台
单步台单步看 proceed() 递归:游标是怎么走到目标方法的1 / 7
连点下一步,盯住右边 currentInterceptorIndex:第 3 步还是 0,第 6 步才反射调到目标
被调试的代码
1CglibAopProxy$DynamicAdvisedInterceptor.intercept(proxy, method, args, mp) { // ①
2 List<Object> chain = this.advised.getInterceptors(method, target); // ② 按切点取通知
3 return new ReflectiveMethodInvocation(proxy, target, method, args, tp, chain).proceed();
4}
5// 下面就是 ReflectiveMethodInvocation.proceed() 本体
6if (this.currentInterceptorIndex == this.interceptorsAndFilters.size() - 1) { // ③ 走到尽头了吗
7 Object interceptor = this.interceptorsAndFilters.get(++this.currentInterceptorIndex);
8 return interceptor.invoke(this); // ④ 通知自己决定何时 proceed
9}
10return invokeJoinpoint(); // ⑤ 链表走完,反射调用目标方法
11// 目标返回后逐层出栈:每个通知的「后半段」按相反顺序执行
此刻的变量
代理类OrderServiceImpl$$SpringCGLIB$$0
methodcreate(OrderDTO)
advisedAdvisedSupport(3 个 Advisor)
调用栈
1$SpringCGLIB$$0.create
2CglibAopProxy.intercept
1每一次调用都从这一行进 intercept 开始。注意它什么增强逻辑都没做,只是去问「这次调用该走哪条链」——链是运行期按方法取的,不是启动时焊死的。
59 / 117
小节
十、决策:切面不生效该先查什么
60 / 117
决策
决策你新写了一个 `@Aspect` 类,给它所在包的 Service 方法加日志,启动一切正常,但运行后发现**一条日志都没打**。第一步应该做什么?
61 / 117
小节
十一、上手体验:亲眼看着代理被造出来
62 / 117

这一节把前面五条链路全部做成可以点的实验。核心是 autoptr(自动代理创建器)的四个分支,建议按顺序点,因为它们正好对应时间轴上的四站。

63 / 117

第一站:代理到底在第几步诞生。这一支会告诉你它挂在 Bean 生命周期的「初始化后置」,也就是第九篇讲的第⑥步;还会解释循环依赖时为什么改走 getEarlyBeanReference 这个入口:

64 / 117
内核实验
TeaVM在后置处理里拦截:代理诞生的那一刻未启动
重点看第③步「挂在生命周期第⑥步」和第⑦步推出的两条常识
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
65 / 117

第二站:判定「值不值得包」。findEligibleAdvisors → shouldWrap → advisedBeans 缓存这三件事都在这一支,它顺带解释了那个怪现象——「换了个 getBean 顺序,代理就没了」:

66 / 117
内核实验
TeaVM候选判定:一个 Bean 一生只判一次未启动
注意第④步:哪怕只有一个方法命中,整个 Bean 也会被代理
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
67 / 117

第三站:真正动手造代理的工厂。ProxyFactory 里的三件套、getProxy() 那一刻才生成类、以及 proxyTargetClass=true 会把选择强行掰向 CGLIB 并引发一连串连锁反应:

68 / 117
内核实验
TeaVMProxyFactory:模具房怎么冲压出代理未启动
第⑤步那条 ClassCastException 原文可以直接粘进第十三节的速查表
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
69 / 117

第四站最容易被忽略:代理背后站的到底是谁。默认是 SingletonTargetSource(永远是同一个 Bean),但换成池或原型就能做到「每次调用换一个目标」;同时 Object 的 toString/equals/hashCode 永远由代理自己回答,不进通知链——这解释了「两个业务相等的代理放进 HashSet 却算两个」:

70 / 117
内核实验
TeaVMTargetSource:总机号不变,接线员轮换未启动
看第⑤⑥步:为什么代理当 Map key 会出问题
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
71 / 117

光看创建器还不够,还要把它放回整条流水线和具体判定里看。第一个实验把 Bean 从实例化一路跑到「初始化后置」,你能看清代理是在哪一步插进来的;第二个实验验证「判定发生在方法级」这件事——同一段包路径写进 execution 与 within 含义不同;第三个实验则演示代理类型怎么选、以及自调用为什么会绕过这一切:

72 / 117
内核实验
TeaVM把代理放回 Bean 的一生里看未启动
切到「单例」跑一圈,注意第⑥步「初始化后置」就是本篇第四节的位置
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
73 / 117
内核实验
TeaVM命中判定发生在方法级未启动
对照第四节:这就是 findAdvisorsThatCanApply 内部逐方法做的事
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
74 / 117
内核实验
TeaVMJDK 还是 CGLIB,以及自调用陷阱未启动
先看两种代理的形态差异,再切「同类自调用」验证第五道闸之外的那条限制
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
75 / 117
原理动画
动图 · 代理掉包现场:从成品到上架
动图 · 代理掉包现场:从成品到上架
76 / 117

第五站回到调用期:多个切面在同一条链表里怎么排。inner 会逐帧列出「Around1 前 → Around2 前 → Before → 目标 → After → Around2 后 → Around1 后」,正好是上面那个单步台的多人版:

77 / 117
内核实验
TeaVM多个 Advisor 在同一条链表里的位置未启动
inner 看链表逐层进出,reverse 看忘写 @Order 时顺序为什么不可预测,txaspect 看事务为什么不能排在自定义切面里面
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
78 / 117

内核里的类名长得劝退,其实它们和职责是一一对应的死映射。这一局把这七个「栈帧常客」配一遍——配错了会告诉你它在时间轴的哪一站:

79 / 117
配对闯关
闯关内核类名 ↔ 它到底干的那件事已配对 0/7 · 配错 0
左边是你在堆栈和 DEBUG 日志里一定会撞见的名字,右边是它的真实职责
先点左边一个
80 / 117

实验做到这里,可以换成命令行自己敲。这台控制台连着浏览器里的同一个容器,回显全部由内核算出来——按本篇时间轴的顺序敲,四站一站一站过:

81 / 117
内核控制台
82 / 117
小节
十二、沙盘:换三个开关,看代理形态怎么变
83 / 117

左边拨三个开关——代理策略、目标类的形态、有没有接口——右边立刻给出「用哪种代理、注入该写什么类型、会撞哪条限制」。这三个开关覆盖了本篇第五节整张对比表:

84 / 117
沙盘
沙盘代理选型沙盘:三种目标 × 两种策略
运行结果
DefaultAopProxyFactory: hasUserSuppliedInterface -> JdkDynamicAopProxy
getClass() -> com.sun.proxy.$Proxy42
注入 UserService 接口:OK
这是 Spring 原生默认(proxyTargetClass=false)+有接口的最优组合,什么都不用改。
85 / 117
说明

沙盘最后一格值得多说一句——你以为写了 proxyTargetClass=false 就一定是 JDK 代理,其实 DefaultAopProxyFactory 的第一个条件就是 hasNoUserSuppliedProxyInterfaces(config):没有可用接口时无条件走 CGLIB。另外 optimize 也在同一个 if 里,所以「我明明设了 false 怎么还是 CGLIB」的答案通常在这两行代码里,不在配置文件里。

86 / 117
小节
十三、常见报错速查
87 / 117

以下片段都能整段粘进搜索框;「沿时间轴定位」那一列指回第十一节的时间轴图,告诉你该在哪一站找:

88 / 117
对照表
报错原文(片段)真实原因30 秒自救深挖看第几篇
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'com.example.service.UserService': Cannot proxy final class [com.example.service.UserService]目标是 final 类,CGLIB 靠继承生成子类,而 final 类不能被继承去掉 final;确实要保留就把该能力改成装饰器手写,或换 AspectJ 编译期织入#12 动态代理 · 本篇第十二节沙盘 final-type
NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.aop.Advisor' available(或 ...BeanFactoryTransactionAttributeSourceAdvisor)有人显式注入了一个 Advisor Bean,但那个 Bean 从没被注册(少了 @Bean / @Component,或不在扫描范围)确认 Advisor 是通过 @Bean 方法暴露的;事务相关则检查是否启用了 @EnableTransactionManagement本篇第三节 findCandidateAdvisors · #8 容器刷新
BeanCurrentlyInCreationException: Bean is not yet fully initialized(常伴 received null on self reference / is not fully initialized)循环依赖让代理提前曝光:BPP 还没跑完,别的 Bean 就来取早期引用,此时 getEarlyBeanReference 已经产出了代理优先重构单向依赖;必须留就用 @Lazy 打断一条边;确认这两个 Bean 都是 singleton#10 循环依赖 · 本篇第十一节 autoptr 第一站
IllegalStateException: Required to bind 2 arguments, but only bound 1 (JoinPointMatch was NOT bound)切点里有形参绑定(如 @annotation(x))但通知方法少写了对应形参,或形参名不一致让注解里的变量名与形参名完全一致,并把 JoinPoint/ProceedingJoinPoint 放在第一位#13 @AspectJ 细节第五节
ClassCastException: class com.sun.proxy.$Proxy42 cannot be cast to class com.example.service.UserServiceImpl目标有接口且走了 JDK 代理,代理只是接口的实现,不是实现类的子类注入改成接口类型;或 spring.aop.proxy-target-class=true(Boot 默认已 true)本篇第五节 · 第十二节沙盘
启动日志出现 ...is not eligible for getting processed by all BeanPostProcessors / 通知在自己的切面里不生效该 Bean 太早创建(例如被某个 BPP 依赖),部分后置处理器来不及挂上;切面类本身也从不被代理别把切面/基础设施类写成被 BPP 直接依赖的对象;加 @Lazy 或用 ObjectProvider 推迟#8 容器刷新 · #9 Bean 生命周期
AOP configuration problem: no matching method found for advice(或启动正常但一行通知都没打)候选 Advisor 为空:@Aspect 类没注册成 Bean,或组件扫描没覆盖到它getBeanNamesForType(Advisor.class) 数一数;补 @Component 并确认扫描路径本篇第六节自查清单
Cannot convert class ... to required type ...; nested exception is java.lang.IllegalStateException: Failed to create dynamic proxy代理创建阶段就失败,通常是 target 与 interfaces 配置冲突(如目标类实现了泛型接口)或 CGLIB 版本冲突看紧随其后的 Caused by;jar 冲突时用 mvn dependency:tree -Dincludes=org.aspectj:aspectjweaver,cglib#2 Maven 依赖仲裁 · 本篇第五节
自调用时通知消失(无异常)this.method() 落在原始对象上,压根没经过代理,因此也就没经过 wrapIfNecessary 的成果抽到另一个 Bean;或 @EnableAspectJAutoProxy(exposeProxy = true) 后用 AopContext.currentProxy()本篇第六节 · #15 AOP 实战
89 / 117
坑

Bean is not yet fully initialized 这一句最容易让人误以为是「初始化写错了」。它真正的意思是:这个 Bean 还在创建过程中就被别人提前拿走了。在纯 Spring AOP 里,一旦某个 Bean 参与循环依赖又被提前曝光,代理就会在 getEarlyBeanReference 里提前生成一次;如果这时你又给它加了「依赖自身代理」的逻辑,就会出现「同一个 Bean 两个不同实例」的诡异行为。判断依据:栈里同时出现 DefaultSingletonBeanRegistry.getSingleton 与 AbstractAutoProxyCreator.getEarlyBeanReference,就是这条路。

90 / 117

上面那段坑的「加重版」会直接把应用拦在启动阶段,而且报错里同时出现 AOP 和循环依赖两套词汇,读起来特别吓人。练一次——注意它的凶手行不在最上面:

91 / 117
报错急救
报错急救BeanCurrentlyInCreationException: injected in its raw version but eventually wrapped
循环依赖撞上 AOP 代理:两个版本只有一个能活

OrderService 注入 UserService,UserService 又注入 OrderService,两边都没有 @Transactional;你给 OrderService 新加了一个日志切面,重启后应用起不来。

APPLICATION FAILED TO START
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'orderService': Bean with name 'orderService' has been injected into other beans [userService] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using getBeanNamesForType with the allowEagerInit flag turned off, for example.
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:643)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:456)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.getEarlyBeanReference(AbstractAutoProxyCreator.java:321)
at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.wrapIfNecessary(AbstractAutoProxyCreator.java:352)
点你认为的「凶手行」(可反复试)
不会也没关系:先猜异常名,再猜哪一行在做决定。
92 / 117
小节
十四、随堂自测
93 / 117
随堂自测
随堂自测你把一个 Service 类写成 `public final class OrderService`,切点表达式确认无误,启动却直接失败。最可能的报错与根因是?
先自己选一个,选中立刻告诉你对不对
94 / 117
随堂自测
随堂自测某次启动日志里出现 `Bean is not yet fully initialized`,栈帧中同时能看到 `AbstractAutoProxyCreator.getEarlyBeanReference`。这说明发生了什么?
先自己选一个,选中立刻告诉你对不对
95 / 117
小节
十五、动手练习
96 / 117
小节
第一档 · 照做
97 / 117

目标:绕开 @Aspect,亲手用一个 Advisor Bean 让代理创建器干活,并把它的判定过程打印出来。这一步做完,你会真正理解「Advisor = 切点 + 通知」和「候选为空就不代理」这两句话。

98 / 117
java
package com.example.internals;import org.aopalliance.intercept.MethodInterceptor;import org.springframework.aop.Advisor;import org.springframework.aop.support.DefaultPointcutAdvisor;import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.stereotype.Service;import java.lang.annotation.*;@Configurationpublic class ManualAopConfig {    @Target(ElementType.METHOD)    @Retention(RetentionPolicy.RUNTIME)    public @interface Timed {}    @Service    public static class ReportService {        @Timed        public String build(String day) {   // 命中:带 @Timed            return "report-" + day;        }        public String ping() {              // 不命中:没有注解            return "pong";        }    }    /** 关键:Advisor 必须以 @Bean 的形式进容器,才会被 findCandidateAdvisors 收进候选 */    @Bean    public Advisor timedAdvisor() {        MethodInterceptor interceptor = invocation -> {            long start = System.currentTimeMillis();            try {                return invocation.proceed();            } finally {                System.out.println("[advisor] " + invocation.getMethod().getName()                        + " took " + (System.currentTimeMillis() - start) + "ms");            }        };        // 切点:方法上带 @Timed;通知:上面那个拦截器        return new DefaultPointcutAdvisor(                AnnotationMatchingPointcut.forMethodAnnotation(Timed.class), interceptor);    }}
99 / 117
java
package com.example.internals;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.context.ConfigurableApplicationContext;@SpringBootApplicationpublic class InternalsApplication {    public static void main(String[] args) {        try (ConfigurableApplicationContext ctx =                     SpringApplication.run(InternalsApplication.class, args)) {            ReportService svc = ctx.getBean(ReportService.class);            System.out.println("代理类名:" + svc.getClass().getName());            System.out.println(svc.build("2026-01-01"));            System.out.println(svc.ping());            System.out.println("容器里的 Advisor 数量:"                    + ctx.getBeanNamesForType(org.springframework.aop.Advisor.class).length);        }    }}
100 / 117

启动前先加一行 DEBUG,才能看到创建器的判定过程:

101 / 117
properties
logging.level.org.springframework.aop=DEBUG
102 / 117

预期输出(省略时间戳与前缀):

103 / 117
代码对照
代码text
Creating instance of bean 'manualAopConfig.ReportService'Creating shared instance of singleton bean 'timedAdvisor'Adding SLF4J-based advisor: ... InstantiationModelAwarePointcutAdvisorImpl ...   # 若你另有 @Aspect 切面Created JDK/AOP Alliance ProxyFactoryBean-based proxy with characteristics [...] # 命中后创建代理代理类名:com.example.internals.ManualAopConfig$ReportService$$SpringCGLIB$$0[advisor] build took 0msreport-2026-01-01pong容器里的 Advisor 数量:1
解读
  • build 有 [advisor] 前缀日志,ping 没有:判定确实发生在方法级,但只要有一个方法命中,整个 Bean 就被代理(第十一节第二站)
  • 类名里出现 $$SpringCGLIB$$:你 getBean 拿到的是代理,原始 ReportService 实例只在它肚子里当 target
  • 把 @Bean 从 timedAdvisor() 上删掉,Advisor 数量 变成 0,两条业务输出前的 DEBUG 也不再提代理——这就是「候选 Advisor 为空 ⇒ 完全不建代理」
104 / 117
小节
第二档 · 变体
105 / 117
  1. 把切点换成 new ExecutionPointcut("com.example.internals...(..)")。你会观察到:ping() 也开始打印 [advisor],说明同一份 Advisor 只是换了「在哪拦」,通知代码一个字不用动。
  2. 把 ReportService 改成 final class。你会观察到:启动直接抛 BeanCreationException: ... Cannot proxy final class,对照第十三节第一行。
  3. 把注入声明改成 ManualAopConfig.ReportService svc = ctx.getBean(...) 并保持 CGLIB。你会观察到:仍然正常;再把 spring.aop.proxy-target-class=false 且给 ReportService 抽出一个接口,然后尝试按实现类接收——ClassCastException 出现,沙盘 auto|iface|by-impl 那一格的现场版。
  4. 在 ReportService 里加一个方法 self(),内部调 this.build("x")。你会观察到:[advisor] 不打第二条,因为 this 是原始对象。这是本篇唯一一条不在「创建期」而在「调用期」的限制。
106 / 117
小节
第三档 · 造一个
107 / 117

做一个代理体检工具:启动完成后自动报告「哪些 Bean 被代理了、用了哪种代理、被哪些 Advisor 命中」。

108 / 117
  • 实现一个 ApplicationListener<ApplicationReadyEvent>(或用 SmartInitializingSingleton),遍历 beanFactory.getBeanDefinitionNames()
  • 对每个 Bean 调 beanFactory.getBean(name),用 AopUtils.isAopProxy(bean) / isCglibProxy / isJdkDynamicProxy 分类计数
  • 命中代理的,再用 ((Advised) bean).getAdvisors() 打印 Advisor 简名列表;捕获异常时记录 Bean 名继续跑,不要因单个 Bean 失败中断报告
  • 输出一张汇总表:总数 / CGLIB 数 / JDK 数 / 命中最多的前 5 个 Advisor;并在 spring.aop.report=false 时整体跳过
109 / 117

验收清单:① 表格数字与 logging.level.org.springframework.aop=DEBUG 的人工计数一致;② 故意把一个 @Aspect 的 @Component 注释掉,报告里对应 Advisor 消失且业务输出不变(证明候选为空 ⇒ 不代理,也不报错);③ 你能用这张报告向同事解释「为什么我的 Service 类名后面多了一串 $$SpringCGLIB$$」;④ 报告本身不触发任何额外 Bean 的提前创建(观察 DEBUG 里没有出现新的 Creating shared instance)。

110 / 117
小节
十六、要点自查
111 / 117
自检

从 @EnableAspectJAutoProxy 到「代理进单例池」,按顺序说出五个关键方法名,并指出哪一步做了切点求值。

112 / 117
自检

postProcessAfterInitialization 的返回值有什么特殊语义?为什么说它是「who wraps whom」的答案?

113 / 117
自检

wrapIfNecessary 的三道闸分别挡住什么?如果没有 advisedBeans 缓存会有什么后果?

114 / 117
自检

DefaultAopProxyFactory 在什么条件下会无视 proxyTargetClass=false 直接用 CGLIB?

115 / 117
自检

循环依赖下代理为什么要提前生成?哪条入口方法负责这件事,它如何保证代理只有一个?

116 / 117
口诀

创建器是质检台,装箱前一眼看;返回代理即掉包,原对象藏在壳里面。

117 / 117
总结

把这一节的链路记成一条线——@EnableAspectJAutoProxy 通过 @Import 注册了 AnnotationAwareAspectJAutoProxyCreator;它作为 BeanPostProcessor,在 Bean 初始化之后的 postProcessAfterInitialization 里调用 wrapIfNecessary;wrapIfNecessary 用 findEligibleAdvisors 找到能命中当前类的 Advisor,命中后交给 ProxyFactory,由 DefaultAopProxyFactory 在 JdkDynamicAopProxy 与 CglibAopProxy 之间二选一,最终用代理替换原始实例。切面不生效时,先开 AOP 的 DEBUG 日志拿事实,再对照「自调用、private/final、切点写错、非容器对象、未注册成 Bean、未开 exposeProxy」六条清单排查;而 @Transactional 的失效,多半是切面顺序让异常没能传到事务边界。