深入 Spring 15:Advisor 怎样进入 Bean 的创建过程
同一个订单服务为何取得的是代理
new OrderTarget() 返回业务对象,容器 getBean("orders") 却可能返回代理。Advisor 没有修改 Java 的 new 指令;自动代理创建器作为 BeanPostProcessor 参与实例暴露,决定交给调用者的引用。
本实验先手动 ProxyFactory 包装一次,再把相同 Advisor 交给 DefaultAdvisorAutoProxyCreator,让容器创建订单与观察者两个 Bean。观察者回指订单,订单又持有观察者,强制出现早期引用需求。可证伪的问题是:观察者注入的早期对象是否与 refresh 后查到的最终对象相同?若只是数通知执行了几次,无法回答对象图是否一致。
环境固定 Spring Framework 6.2.11、JDK 21,源码 SHA 为 4c134254642d88e058aa004bdaf44168e1be7bb2。可运行代码为 examples/spring-framework-lab/src/main/java/blog/spring/Chapter15.java。
Advisor 把匹配范围和执行动作放在一起
一个 DefaultPointcutAdvisor 包含 Pointcut 与 Advice。实验切点只接受实现 Orders 的目标类上的 place 方法,通知则先增加计数,再 proceed。这里没有 AspectJ 注解解析;Advisor 已经是可直接参与筛选的对象。
1 | |
手动工厂调用 addAdvisor(),明确选择通知。自动代理创建器则通过 BeanFactory 搜索 Advisor Bean。前者适合解释单个代理,后者把选择过程嵌入容器的创建协议。不能因为手动工厂能工作,就认为容器已经安装了相应后处理器。
实验显式创建 TracingCreator,设置 BeanFactory,并调用 addBeanPostProcessor(),然后才 refresh。这个受控顺序避免“为注册后处理器而先创建业务 Bean”的干扰。生产配置通常交给容器注册;程序式提前安装在此只是把阶段暴露给断言,不是推荐手工维护所有基础设施。
候选发现与当前 Bean 可用范围不是同一个集合
AbstractAdvisorAutoProxyCreator.findEligibleAdvisors() 先发现候选,再筛选可以应用到当前目标类的 Advisor,接着调用扩展点并排序。没有可用 Advisor 时,getAdvicesAndAdvisorsForBean() 返回 DO_NOT_PROXY,后续保留原对象。筛选与排序源码
实验中候选只有名为 auditAdvisor 的一个 Advisor,但两个业务 Bean 的结果不同:orders 被包装,observer 保持原对象。根因是匹配范围,不是注册先后随机决定。最终 Advised.getAdvisors().length == 1 证明 orders 上只装入一项业务 Advisor;还需用一次 place 调用得到一次计数,才能说明调用经过了它。
同一容器里存在 Advisor 不等于每个 Bean 都应该有代理。诊断某个 Bean 未被增强时,应记录目标类、候选数、可应用数以及最终代理中的 Advisor,而不是只检查配置文件里是否出现了某个注解。
基础设施为什么要排除
AbstractAutoProxyCreator.wrapIfNecessary() 检查 target-sourced Bean、已经记录为无需代理的 Bean、基础设施类型以及 shouldSkip。基础设施判断涵盖 Advice、Pointcut、Advisor 与 AopInfrastructureBean。匹配到这类对象时,在 advisedBeans 中记录 false 并返回原对象。自动代理状态与排除源码
Advisor 自己参与代理决策,如果对它再套用同一套候选查找与通知执行,会把基础设施的创建依赖引回正在解决的问题。排除规则提供一条明确边界。本实验直接把 advice 对象传给后处理回调,并断言返回同一引用;这比只观察某个基础设施 Bean 恰好没有匹配方法更接近排除分支。
需要区分 API 类型排除与 BeanDefinition 中的 role 元数据。本篇验证的是源码 isInfrastructureClass() 的类型判定,没有把任意设置为 ROLE_INFRASTRUCTURE 的业务类概括成自动免代理。
早期代理怎样避免注入一套、查找另一套
setter 循环中的时序是 orders 先分配目标实例,注入 observer 时又需要 orders。普通单例流程此时可以请求早期引用;自动代理创建器实现 getEarlyBeanReference(),先按 cacheKey 记录原 bean,再调用 wrapIfNecessary 返回可暴露的代理。
本实验的 TracingCreator 只覆写该回调以记录父类返回对象,不改变生成逻辑。日志同时包含原对象与暴露对象的身份。refresh 结束后,断言 observer.orders() == context.getBean("orders"),并检查这个引用出现在 early 列表中。这个引用级断言排除了“早期拿 raw,后来另造 proxy”的情况。
postProcessAfterInitialization() 会移除对应的 earlyBeanReferences 记录并与当前 bean 比较。相同原对象已经参与早期代理路径时,这里不再重复 wrap。最终容器的单例创建流程结合已经存在的早期引用决定 exposedObject,因此最后取到的是早期代理。单看后处理器这一方法返回 raw,就推断最终对外暴露 raw,会漏掉外层创建算法。单例最终暴露处理
这条协议依赖同一个创建器、同一 Bean 创建流程与相同原对象身份。它不承诺任何自定义后处理器、任何循环依赖或任意时点包装都能保持一致。构造器循环尚未有实例可暴露,与这里的 setter 场景不同;原生 Framework 的实验也不能用于断言 Boot 默认允许循环依赖。
一层代理通过,并不表示重复包装不可能
实验检查最终代理的 TargetSource 指向非代理目标,且单次 place 仅增加一次计数。随后故意以这个代理为新 ProxyFactory 的 target,并再次加入同一 Advisor。再调用 place,计数增加两次:外层通知执行一次,内层通知执行一次。
这是一个正常可构造的反例。早期引用去重只处理特定创建协议,无法禁止应用显式再包装。多个代理层也可能各自承担不同职责;问题应表述为“是否意外执行了重复通知”,而非“看见代理嵌套必定是框架故障”。
上游 CglibProxyTests.proxyAProxy 保留了对代理再代理的测试;AdvisorAutoProxyCreatorTests.commonInterceptorAndAdvisor 检查公共拦截器与匹配 Advisor 的组合,customTargetSourceNoMatch 则覆盖不匹配的路径。它们作为 SOURCE_VERIFIED 的阅读证据,本实验执行的是自己的完整容器循环与计数断言。上游自动代理测试
复现与故障定位练习
1 | |
原始运行记录为 evidence/15/local-20261002/run.txt。最终输出 RESULT Chapter15 PASS 前,程序会验证手动通知、自动包装、早期与最终引用一致、单 Advisor、非重复目标、基础设施排除、未匹配 Bean、单次通知和显式双层通知。Context 在 try-with-resources 中关闭,不创建数据库连接或线程池。
反例题:在项目中发现两次相同通知日志,是否足以证明 earlyBeanReferences 去重失效?不够。此处再包装实验就能在去重正常的情况下出现两次,应先读 TargetSource 的层数与每层 Advisor。
可执行练习:把切点中的 method.getName().equals("place") 改为不存在的方法名,重新运行。自动包装断言应失败,observer 的依赖仍能连接原订单对象。再恢复切点但删掉 addBeanPostProcessor,观察候选 Advisor 存在而 orders 仍无代理。两个变体分别隔离“匹配错误”与“后处理器未安装”。
排查顺序对应可观测状态:是否安装创建器、有哪些候选、当前目标能匹配哪些候选、提前暴露了什么引用、最后对外保存了什么引用。这五项足以解释本篇实验,无需凭注解数量推测自动代理是否发生。
