1. 先搞清楚锚点位置:doRegisterBean() 在容器启动链路里处于哪个阶段
很多朋友第一次接触doRegisterBean()这个函数,是在读AnnotatedBeanDefinitionReader源码时偶然碰到的。方法名很短,短到容易低估它;但它处理的却是整个 Spring IoC 容器最前期、也最关键的动作——把用户提供的、标注了注解的配置类,转换成容器能管理的BeanDefinition对象。
我的建议是:不要一上来就逐行读方法体,先搞清楚它在容器启动生命周期里的锚点位置。假设你写了这样一个启动入口:
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class)) { AppService service = context.getBean(AppService.class); }注意,Spring 容器此时不是一上来就调用getBean()去创建实例。它做的第一件大事是执行这一行调用链:
AnnotationConfigApplicationContext构造器内部先创建AnnotatedBeanDefinitionReader(负责注册注解类)和ClassPathBeanDefinitionScanner(负责扫描包路径);- 然后执行
register(annotatedClasses); register()内部把传入的配置类逐个转交给reader.register(Class<?>... annotatedClasses);- 最后才进入
refresh(),触发后续的容器刷新、BeanFactoryPostProcessor 执行、Bean 实例化等阶段。
而doRegisterBean()就藏在reader.register()往下的第三步里。整个链路的原始代码大致是这样的:
// AnnotationConfigApplicationContext#register public void register(Class<?>... annotatedClasses) { Assert.notEmpty(annotatedClasses, "At least one annotated class must be specified"); this.reader.register(annotatedClasses); } // AnnotatedBeanDefinitionReader#register public void register(Class<?>... annotatedClasses) { for (Class<?> annotatedClass : annotatedClasses) { registerBean(annotatedClass); } } // AnnotatedBeanDefinitionReader#registerBean public void registerBean(Class<?> annotatedClass) { doRegisterBean(annotatedClass, null, null, null, null); }所以doRegisterBean()其实处在“用户代码执行”与“容器 refresh、真正实例化 Bean”之间的一座桥上。它负责登记元信息、生成名称、补全通用注解属性,然后把BeanDefinition交给BeanDefinitionRegistry。这里有一个非常容易混淆的认知误区:很多人以为doRegisterBean()会触发@Configuration类里@Bean方法的解析,实际上不会。它就是处理“当前传入的这个类本身”,让这个类作为一个 bean definition 登记在案。@Bean方法的展开,要等到refresh()阶段由ConfigurationClassPostProcessor完成,这一点我在后面的章节展开细说。
既然定位清楚了,接下来就该看方法签名。这个方法的签名不像普通 API 那样一眼能看出全部意图,其中藏着几个很重要的设计细节。
2. 方法签名拆解:五个入参、包级可见、返回 void 背后的考量
在 Spring 5.3 版本里,doRegisterBean()的完整签名是这样的:
<T> void doRegisterBean(Class<T> beanClass, @Nullable String name, @Nullable Class<? extends Annotation>[] qualifiers, @Nullable Supplier<T> supplier, @Nullable BeanDefinitionCustomizer[] customizers)先注意一个细节:它没有private修饰符,是包级可见的。这说明它不是设计给外部业务代码直接调用的,外部调用靠的是AnnotatedBeanDefinitionReader.registerBean(...)以及GenericApplicationContext.registerBean(...)这类包装方法。把核心逻辑收敛成包级函数,把参数较多的公共入口留给上层包装,这本身就是一种务实的设计——公共 API 保持简短,复杂逻辑放在内部自由展开。
接下来逐个看参数:
| 参数 | 类型 | 作用 | 对应什么场景 |
|---|---|---|---|
beanClass | Class<T> | 要注册的配置类或普通组件类 | 核心输入,不能为空 |
name | String | 手动指定的 bean 名称 | 为null时交给BeanNameGenerator自动生成 |
qualifiers | 注解类型数组 | 额外限定符、@Primary、@Lazy | 编程式注册时补充限定信息 |
supplier | Supplier<T> | 函数式实例供给器 | Spring 5.0 引入替代构造器实例化的方式 |
customizers | BeanDefinitionCustomizer[] | 对BeanDefinition做二次定制 | 给外部扩展留出口子,闭包式的轻量回调 |
这里最值得展开的是supplier参数。Spring 5.0 开始支持函数式注册,Supplier<T>允许你绕过类构造器,直接提供一个实例生成函数。比如:
context.registerBean( MyService.class, () -> new MyService(new MyDependency("manual")) );我见过不少团队用这种方式做条件化的 Bean 注册。相比@Bean方法,函数式注册的最大优势是:注册动作本身发生在容器 refresh 之前,而且你可以直接在 Java 代码里控制实例的组装过程,不需要为了一个配置类维护一套注解扫描规则。但代价也很明显——它不会自动执行属性填充和依赖注入,所有依赖都得在 Supplier 里手工拼好。这一点在第五章我会再强调,因为它直接影响你对 BeanDefinitionCustomizer 的实用判断。
方法返回void也值得琢磨。为什么不返回注册好的BeanDefinitionHolder或者String beanName?因为注册动作可能被条件评估器(ConditionEvaluator)直接跳过,这时候根本没有可返回的产物;而且真正的注册直接写进了BeanDefinitionRegistry,调用方如果需要确认状态,可以从 registry 里反查。与其花精力设计一个“可能为 null 的返回值”,不如让上层 API 保持干净。这个取舍很符合 Spring 一贯的风格:能通过容器状态暴露的信息,就不强迫方法签名背负额外返回值。
3. 逐行拆解方法体:从 AnnotatedGenericBeanDefinition 到 BeanDefinitionRegistry
现在进入正题,把方法体从头到尾拆开看。为了保持源码版本的统一性,我用 Spring 5.3 的AnnotatedBeanDefinitionReader.doRegisterBean作为分析对象,完整代码如下:
<T> void doRegisterBean(Class<T> beanClass, @Nullable String name, @Nullable Class<? extends Annotation>[] qualifiers, @Nullable Supplier<T> supplier, @Nullable BeanDefinitionCustomizer[] customizers) { AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(beanClass); if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) { return; } abd.setInstanceSupplier(supplier); ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd); abd.setScope(scopeMetadata.getScopeName()); String beanName = (name != null ? name : this.beanNameGenerator.generateBeanName(abd, this.registry)); AnnotationConfigUtils.processCommonDefinitionAnnotations(abd); if (qualifiers != null) { for (Class<? extends Annotation> qualifier : qualifiers) { if (Primary.class == qualifier) { abd.setPrimary(true); } else if (Lazy.class == qualifier) { abd.setLazyInit(true); } else { abd.addQualifier(new AutowireCandidateQualifier(qualifier)); } } } if (customizers != null) { for (BeanDefinitionCustomizer customizer : customizers) { customizer.customize(abd); } } BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd, beanName); definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry); BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry); }3.1 第一步:包装 AnnotatedGenericBeanDefinition
AnnotatedGenericBeanDefinition是读取类上注解元数据的核心类型。它内部通过StandardAnnotationMetadata获取类的完整元数据,包括类名、父类、实现的接口、方法元数据等。为什么要单独包装一层BeanDefinition?因为 Spring 容器后续的所有操作——实例化、属性填充、AOP 代理判断——都基于BeanDefinition进行,直接拿一个Class对象没法统一处理。用生活化的类比:Class是“设计图纸的原件”,BeanDefinition是“经过整理归档的工程蓝图”,而doRegisterBean()就是那个把原件录入档案系统的动作。
3.2 第二步:条件评估 shouldSkip
if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) { return; }这一行处理的是@Conditional相关注解。默认的ConditionEvaluator会读取类上的@Conditional注解,交给对应的Condition实现去判断是否满足跳过条件。如果判定成立,方法直接 return,连 BeanDefinition 都不会注册。这意味着你后续在容器里查不到任何痕迹,而不是注册了一个被禁用的 Bean。
初次接触源码的朋友容易忽略一个关键点:这里的条件评估只针对“当前类本身”。它不会评估类里@Bean方法上的条件——那些条件的判定发生在ConfigurationClassPostProcessor阶段。但如果你用编程式注册塞进去一个携带条件的类,提前在此处把关就很有必要,可以把明显不该注册的组件拒之门外,避免不必要的元数据解析。
3.3 第三步:注入 Supplier
abd.setInstanceSupplier(supplier);setInstanceSupplier做的事情很直白:如果传入的 supplier 不为空,就把这个函数式接口存进BeanDefinition的instanceSupplier字段里。之后AbstractAutowireCapableBeanFactory创建实例时,发现这个字段不为空,就会直接调用supplier.get()获取实例,而不再走构造器反射路径。
3.4 第四步:解析 Scope 元数据并设置
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd); abd.setScope(scopeMetadata.getScopeName());默认的ScopeMetadataResolver是AnnotationScopeMetadataResolver,它的行为是:读取类上的@Scope注解,取出value作为作用域名(如singleton、prototype、request、session),同时解析proxyMode字段。如果类上没有标注@Scope,返回的作用域名是singleton。注意一点:proxyMode在这里只被解析出来,真正生效要等到后面的applyScopedProxyMode步骤。
这里有一个常被忽视的细节:如果@Scope的proxyMode没有显式指定,解析器会根据配置的默认值处理。很多业务开发者对 request/session 作用域不设proxyMode,结果把这类 Bean 注入到单例 Bean 时报错。原因就在这条链路上——作用域代理模式没有在注册阶段被正确设置,容器生成 BeanDefinition 时根本不知道要创建代理对象。
3.5 第五步:确定 beanName
String beanName = (name != null ? name : this.beanNameGenerator.generateBeanName(abd, this.registry));如果调用方没指定name,就走BeanNameGenerator。默认实现是AnnotationBeanNameGenerator,它的核心逻辑分两段:
- 先看类上有没有
@Component、@Service、@Repository、@Controller这类“组件注解”,如果有且显式指定了 value,就取 value 作为 beanName; - 如果没有,就用类名做 JavaBeans 命名缩减:
MyService变成myService,MyURLProvider这种连续大写的情况会保留原样,避免破坏既有命名。
这解释了为什么你在容器里看到的 bean 名称大多是首字母小写。但是注意,如果你通过@Bean("customName")或registerBean("customName", ...)显式指定了名字,这个名字将完全绕过AnnotationBeanNameGenerator,直接生效。
3.6 第六步:处理通用注解
AnnotationConfigUtils.processCommonDefinitionAnnotations(abd);这一步是把@Lazy、@Primary、@DependsOn、@Role、@Description这几个类注解值读出来,设置到BeanDefinition对应字段上。具体来说:
@Lazy(true)设置lazyInit标志;@Primary设置primary标志;@DependsOn设置依赖的 bean 名称数组;@Role设置角色(ROLE_APPLICATION还是ROLE_INFRASTRUCTURE);@Description设置描述文本。
我第一次读到这里时,才发现很多注解语义其实在“注册阶段”就被提前消化了,而不是等到实例化阶段。这个设计的目的很纯粹:注册阶段把所有能确定的静态信息全部写入蓝图,实例化阶段就只需要专注干活。
3.7 第七步:处理限定符
qualifiers数组在这里被逐个处理:
if (Primary.class == qualifier) { abd.setPrimary(true); } else if (Lazy.class == qualifier) { abd.setLazyInit(true); } else { abd.addQualifier(new AutowireCandidateQualifier(qualifier)); }注意源码用的是==判断类对象本身,也就是说前面两个分支处理的是@Primary和@Lazy这两个“特殊限定符”,它们不会作为限定符塞进集合,而是直接设置对应布尔/标志字段。其他注解类型则统一包装成AutowireCandidateQualifier,成为自动装配候选的限定条件。这个分支设计有点巧妙——它让调用方用一个统一的qualifiers数组表达两类完全不相关的语义。
3.8 第八步:执行自定义器
for (BeanDefinitionCustomizer customizer : customizers) { customizer.customize(abd); }BeanDefinitionCustomizer是编程式注册时代最灵活的口子。它的接口只有一个方法,参数是BeanDefinition,意味着你可以在注册前的最后一刻修改任何属性。后面我会专门用一个章节讲它能改什么、不能改什么。
3.9 第九步:包装代理模式并注册
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd, beanName); definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry); BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry);这里有个很关键的链路:BeanDefinitionHolder只是“beanName + BeanDefinition + aliases”的组合包装。applyScopedProxyMode检查之前解析出的ScopedProxyMode,如果模式的枚举值不是NO,就会调用ScopedProxyCreator.createScopedProxy(...),把原始 BeanDefinition 替换成一个“代理工厂 BeanDefinition”,同时把原始 BeanDefinition 写成 targetBeanDefinition 存进去。最终注册到 registry 的,可能是一个包装后的代理定义,而不是你最初构造的那个abd。
最后BeanDefinitionReaderUtils.registerBeanDefinition做的事情很简单,却很关键:
public static void registerBeanDefinition(BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) { String beanName = definitionHolder.getBeanName(); registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition()); String[] aliases = definitionHolder.getAliases(); if (aliases != null) { for (String alias : aliases) { registry.registerAlias(beanName, alias); } } }一句话总结这一段:它负责把 beanName 和 BeanDefinition 注册进DefaultListableBeanFactory的beanDefinitionMap,顺带处理别名。到这一步,一次编程式的 BeanDefinition 注册才算真正完成。
4. 一个方法两条分支:为什么 @Bean 方法不在 doRegisterBean() 里注册
读到这里,你可能有一个很自然的疑问:既然doRegisterBean()是注册“注解类”的方法,那@Configuration类里声明的@Bean方法跑哪去了?它们是怎么被处理的?
答案是:不走doRegisterBean()这条路径。这是整个注册链路里最容易让人撞墙的分叉点,我单独拿出来讲。
两条注册路径的主线分别是:
| 路径 | 核心类 | 触发时点 | 负责注册的目标 |
|---|---|---|---|
| 路径一 | AnnotatedBeanDefinitionReader | register()阶段 | 被显式传入的类(或@Component扫描结果类) |
| 路径二 | ConfigurationClassBeanDefinitionReader | refresh()阶段的ConfigurationClassPostProcessor | @Configuration类中的@Bean方法、@Import导入的类 |
可以把两条路径理解成两拨不同的工人。第一拨工人负责“把材料清单上的主件登记入库”,第二拨工人负责“把主件图纸里标注的附属构件逐个加工入库”。doRegisterBean()属于第一拨,它只登记AppConfig这类类本身;第二拨工人在refresh()阶段对上AnnotatedBeanDefinitionReader登记的类时,才会解析类上的@Bean注解并生成对应 BeanDefinition。
具体到第二拨工人,它的内部方法长这样(简化版):
private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) { // 解析 @Bean 方法上的作用域、限定符、注解属性,构建 ConfigurationClassBeanDefinition ConfigurationClassBeanDefinition beanDef = new ConfigurationClassBeanDefinition(configClass.getMetadata()); beanDef.setBeanClassName(configClass.getMetadata().getClassName()); beanDef.setFactoryMethodName(beanMethod.getMetadata().getMethodName()); beanDef.setFactoryBeanName(configClass.getMetadata().getClassName()); beanDef.setUniqueFactoryMethodName(beanMethod.getMetadata().getMethodName()); // 处理条件、作用域、通用注解等 // 注册 this.beanDefinitionRegistry.registerBeanDefinition(beanDef.getBeanName(), beanDef); }注意一个细节:这段代码创建的BeanDefinition类型是ConfigurationClassBeanDefinition,而不是AnnotatedGenericBeanDefinition。它额外包含了factoryMethodName和factoryBeanName这两个关键字段,标记“这个 Bean 不是直接由类实例化,而是调用另一个配置类(FactoryBean)上的某个方法生产的”。字段里带factory前缀,也是因为背靠工厂方法模式。
搞懂这条分叉,能解决一个实际困惑:你可以用编程式注册塞入一个带@Bean方法的配置类:
context.register(AppConfig.class);此时如果你立刻调用context.getBean("someBeanFromMethod"),一定会失败,因为@Bean方法还没有被解析。只有当你调用context.refresh()或者让容器继续启动完成刷新,ConfigurationClassPostProcessor才会有机会跑到前面把方法展开。反过来,如果你的代码在refresh()之前就试图操作这些 Bean,就会踩到“这个方法明明写在配置类里,为什么容器找不到”的坑。
4.1 两拨工人的注册结果如何统一
最终所有BeanDefinition都落到同一个DefaultListableBeanFactory.beanDefinitionMap里。不管来源是doRegisterBean()还是ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForBeanMethod(),最后的注册动作都是调用registry.registerBeanDefinition(String, BeanDefinition)。所以从结果看,容器不会区分“这个 BeanDefinition 是编程式注册来的”还是“配置类解析来的”直接通过getBeanDefinition(String)拿到的定义也看不出来源。这个统一入口的存在,才让扩展BeanDefinitionRegistryPostProcessor时可以无差别地遍历并修改所有已注册的定义。
4.2 调试时的身份判断技巧
虽然没有现成的“来源标记”,但通过BeanDefinition的字段能大致推断出来源:
factoryMethodName不为空的,基本来自@Bean方法;beanClassName是配置类全限定名、且没有工厂方法名的,可能来自doRegisterBean()对配置类自身注册;- 带
source标注的,可能是扫描器扫出来的。
我在调试 Spring 扩展时,经常打一个断点在DefaultListableBeanFactory.registerBeanDefinition方法入口,然后观察入参的BeanDefinition类型和字段组合,就能判断当前注入的来源走的是哪条生产线。这个方法非常有效,推荐你也试试。
5. 源码背面的扩展点:registerBean 的自定义通道到底能改什么
源码读多了会发现,真正值得长期记住的不是方法体的每一步,而是它暴露出的扩展点。doRegisterBean()的扩展点非常清晰,集中在四个接口或者回调对象上:
BeanNameGenerator:控制自动 beanName 生成规则;ScopeMetadataResolver:控制作用域注解的解析规则;ConditionEvaluator:控制注册前的条件跳过行为;BeanDefinitionCustomizer:注册前对 BeanDefinition 做任意的属性调整。
前三个都属于“替换默认策略”层面的扩展,平时用得少;真正高频使用的是BeanDefinitionCustomizer,因为这玩意儿太轻量了,用 Lambda 就能写。比如:
context.registerBean( MyService.class, MyService::new, bd -> { bd.setPrimary(true); bd.setInitMethodName("init"); bd.getPropertyValues().add("name", "custom-name"); } );这里我特别想强调一个从源码里反推出来的坑:BeanDefinitionCustomizer的入参虽然叫bd,但它无法改变beanName。因为 beanName 早在 customizer 执行之前就已经计算完毕并存入BeanDefinitionHolder,而且BeanDefinition这个接口根本没有setBeanName()方法。如果你看着源码,在 customizer 里想改名字,编译期就无法通过。所以,想指定 beanName,只能在注册方法入参里传,比如:
context.registerBean("myService", MyService.class, MyService::new);还有一个常见误区:不少人想在BeanDefinitionCustomizer里直接触发其他 Bean 的注册或依赖检查。这是做不到的,因为这个回调执行时,容器还没有进入refresh(),大部分依赖关系也没建立起来。Customizer 的正确用法仅限于“修改 BeanDefinition 的属性”,不是让你在这里做容器操作。
再展开说说 supplier 和@Bean方法的差异,因为它决定了你在写自定义注册时选哪条路。使用Supplier<T>时,Spring 会认为你已经完全接管了实例创建过程,所以不会做自动的构造器注入和属性填充。相比之下,@Bean方法的参数列表会自动完成依赖解析。举个例子,如果你写:
context.registerBean( MyService.class, () -> new MyService() );那MyService想要的依赖一个都不会进来,哪怕容器里有MyDependency也不会自动注入进去。你需要自己在 Supplier 里写new MyService(new MyDependency())。这个行为经常被没读过源码的同事误解,他们以为编程式注册和@Bean方法一样“智能”。
我的建议是三层选择策略:
- 业务组件尽量用
@Component+ 扫描,让它走自动装配; - 需要精细化控制实例创建过程,用
@Bean方法并借助参数注入; - 只有那些希望完全脱离反射、用函数式方式手工组装实例的场景,才用
registerBean+Supplier。
Supplier不是“更高级”的注册方式,它只是“更底层”的注册方式。底层不意味着更好,恰恰意味着你需要承担更多细节。
6. 断点调试与常见误判:验证 BeanDefinition 注册状态的几条可靠路径
源码分析讲再多,终究要落地到调试。我见过不少人在排查“为什么容器里没有这个 Bean”时,往往只会看applicationContext.getBean()是否抛异常。但如果你想确认问题是不是出在注册阶段,有几个更高效率的手段。
6.1 在 doRegisterBean 方法上打条件断点
IDEA 里可以直接在AnnotatedBeanDefinitionReader中找doRegisterBean()方法,在方法入口和BeanDefinitionReaderUtils.registerBeanDefinition这行分别打上断点。条件断点可以写成beanClass.getName().contains("MyService"),这样只有目标类经过时才会停下来。这一步能立刻确认:
- 注册流程是否走到了
doRegisterBean(); shouldSkip是否悄悄把它拦下(如果入口断点命中、出口断点没命中,大概率是被条件评估跳过了);beanName最终是什么;scope和通用注解是否写进了BeanDefinition。
这个方法比看日志不知道高到哪里去了,尤其是排查@Conditional失效问题时,直接在入口看一眼 metadata 里的条件注解,判断链路非常直观。
6.2 通过 ApplicationContext 反查 Definition
如果不想打断点,也可以在代码里临时加一段观察代码:
// 在 refresh() 之后执行 DefaultListableBeanFactory factory = context.getDefaultListableBeanFactory(); String[] names = factory.getBeanDefinitionNames(); for (String name : names) { BeanDefinition bd = factory.getBeanDefinition(name); System.out.println(name + " -> " + bd.getClass() + " -> " + bd.getScope()); }注意getBeanDefinitionNames()返回的是已经成功注册的定义名。如果你在这个列表里看不到目标 Bean,说明问题发生在注册前;如果列表有名字但getBean失败,说明问题发生在实例化阶段。这个“先查注册、再查实例化”的两步排查法,能帮你快速缩小问题半径。
6.3 区分注册阶段错误和实例化阶段错误
经常有人拿着一句NoSuchBeanDefinitionException就开始查@ComponentScan配置,但问题可能根本不在扫描路径上。你按下面的规则分个类,排查效率会高不少:
| 现象 | 大概率问题阶段 | 需要去查的地方 |
|---|---|---|
| 容器里完全找不到 BeanDefinition | 注册阶段 | @Conditional不成立、扫描路径不对、register 没调用 |
| BeanDefinition 存在,注入时报类型不匹配 | 实例化/注入阶段 | 构造器参数、@Qualifier、泛型解析 |
| BeanDefinition 存在,但创建时报错 | 实例化阶段 | 构造器异常、属性填充异常、AOP 代理失败 |
我发现很多新手会把“容器里没有 BeanDefinition”和“容器里没有 Bean 实例”混为一谈,其实二者的诊断路径完全不同。前者是元信息都没进去,后者只是没生产出来。
6.4 一个容易被忽略的坑:注册顺序影响
doRegisterBean()只是把 BeanDefinition 写进 map,但它不负责处理依赖顺序。容器在处理@DependsOn或者其他后处理器时,会根据定义里的依赖关系做排序。所以即使你在注册时发现顺序不对,也不必恐慌,很多排序工作在refresh()阶段才做。真正要担心的是:BeanDefinitionRegistryPostProcessor在doRegisterBean()之后、refresh()之初执行,如果注册的 BeanDefinition 需要在 postProcessor 阶段被遍历到,注册动作本身必须早于refresh()。编程式注册天然满足这个前提,这也是它比@Bean方法更适合做“注册期后处理”的原因。
6.5 观察 scoped proxy 前后的 BeanDefinition 变化
调试 scoped proxy 相关的坑时,建议在两处下断点:一处是AnnotationConfigUtils.applyScopedProxyMode的入参,一处是BeanDefinitionReaderUtils.registerBeanDefinition的入参。你会发现,传入applyScopedProxyMode时还是普通的AnnotatedGenericBeanDefinition,转一圈回来再看,如果proxyMode不是NO,它已经被替换成了ScopedProxyFactoryBean相关的 RootBeanDefinition。不看源码的人会认定你注册的是MyBean,但容器里实际存在的可能是scopedTarget.MyBean。遇到“类名对不上”的诡异现象,往这个方向查,基本上两分钟能定位。
最后分享一个小习惯:我在阅读 Spring 注册相关代码时,都会顺手在DefaultListableBeanFactory.registerBeanDefinition(String, BeanDefinition)方法入口加一个条件断点,条件写成beanDefinition.getBeanClassName() != null && beanDefinition.getBeanClassName().contains("自己的包名")。这样每次启动都能看到自己项目的所有 BeanDefinition 都在什么时点、以什么形状进入了容器。看得多了,你对doRegisterBean()在整个启动流程中的作用,会有一种比任何源码解读都更直观的理解。