Spring源码解析:doRegisterBean()如何完成BeanDefinition注册

发布时间:2026/10/8 3:47:21
Spring源码解析:doRegisterBean()如何完成BeanDefinition注册
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(ClassT beanClass, Nullable String name, Nullable Class? extends Annotation[] qualifiers, Nullable SupplierT supplier, Nullable BeanDefinitionCustomizer[] customizers)先注意一个细节它没有private修饰符是包级可见的。这说明它不是设计给外部业务代码直接调用的外部调用靠的是AnnotatedBeanDefinitionReader.registerBean(...)以及GenericApplicationContext.registerBean(...)这类包装方法。把核心逻辑收敛成包级函数把参数较多的公共入口留给上层包装这本身就是一种务实的设计——公共 API 保持简短复杂逻辑放在内部自由展开。接下来逐个看参数参数类型作用对应什么场景beanClassClassT要注册的配置类或普通组件类核心输入不能为空nameString手动指定的 bean 名称为null时交给BeanNameGenerator自动生成qualifiers注解类型数组额外限定符、Primary、Lazy编程式注册时补充限定信息supplierSupplierT函数式实例供给器Spring 5.0 引入替代构造器实例化的方式customizersBeanDefinitionCustomizer[]对BeanDefinition做二次定制给外部扩展留出口子闭包式的轻量回调这里最值得展开的是supplier参数。Spring 5.0 开始支持函数式注册SupplierT允许你绕过类构造器直接提供一个实例生成函数。比如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(ClassT beanClass, Nullable String name, Nullable Class? extends Annotation[] qualifiers, Nullable SupplierT 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 第一步包装 AnnotatedGenericBeanDefinitionAnnotatedGenericBeanDefinition是读取类上注解元数据的核心类型。它内部通过StandardAnnotationMetadata获取类的完整元数据包括类名、父类、实现的接口、方法元数据等。为什么要单独包装一层BeanDefinition因为 Spring 容器后续的所有操作——实例化、属性填充、AOP 代理判断——都基于BeanDefinition进行直接拿一个Class对象没法统一处理。用生活化的类比Class是“设计图纸的原件”BeanDefinition是“经过整理归档的工程蓝图”而doRegisterBean()就是那个把原件录入档案系统的动作。3.2 第二步条件评估 shouldSkipif (this.conditionEvaluator.shouldSkip(abd.getMetadata())) { return; }这一行处理的是Conditional相关注解。默认的ConditionEvaluator会读取类上的Conditional注解交给对应的Condition实现去判断是否满足跳过条件。如果判定成立方法直接 return连 BeanDefinition 都不会注册。这意味着你后续在容器里查不到任何痕迹而不是注册了一个被禁用的 Bean。初次接触源码的朋友容易忽略一个关键点这里的条件评估只针对“当前类本身”。它不会评估类里Bean方法上的条件——那些条件的判定发生在ConfigurationClassPostProcessor阶段。但如果你用编程式注册塞进去一个携带条件的类提前在此处把关就很有必要可以把明显不该注册的组件拒之门外避免不必要的元数据解析。3.3 第三步注入 Supplierabd.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 第五步确定 beanNameString beanName (name ! null ? name : this.beanNameGenerator.generateBeanName(abd, this.registry));如果调用方没指定name就走BeanNameGenerator。默认实现是AnnotationBeanNameGenerator它的核心逻辑分两段先看类上有没有Component、Service、Repository、Controller这类“组件注解”如果有且显式指定了 value就取 value 作为 beanName如果没有就用类名做 JavaBeans 命名缩减MyService变成myServiceMyURLProvider这种连续大写的情况会保留原样避免破坏既有命名。这解释了为什么你在容器里看到的 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_INFRASTRUCTUREDescription设置描述文本。我第一次读到这里时才发现很多注解语义其实在“注册阶段”就被提前消化了而不是等到实例化阶段。这个设计的目的很纯粹注册阶段把所有能确定的静态信息全部写入蓝图实例化阶段就只需要专注干活。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()这条路径。这是整个注册链路里最容易让人撞墙的分叉点我单独拿出来讲。两条注册路径的主线分别是路径核心类触发时点负责注册的目标路径一AnnotatedBeanDefinitionReaderregister()阶段被显式传入的类或Component扫描结果类路径二ConfigurationClassBeanDefinitionReaderrefresh()阶段的ConfigurationClassPostProcessorConfiguration类中的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方法的差异因为它决定了你在写自定义注册时选哪条路。使用SupplierT时Spring 会认为你已经完全接管了实例创建过程所以不会做自动的构造器注入和属性填充。相比之下Bean方法的参数列表会自动完成依赖解析。举个例子如果你写context.registerBean( MyService.class, () - new MyService() );那MyService想要的依赖一个都不会进来哪怕容器里有MyDependency也不会自动注入进去。你需要自己在 Supplier 里写new MyService(new MyDependency())。这个行为经常被没读过源码的同事误解他们以为编程式注册和Bean方法一样“智能”。我的建议是三层选择策略业务组件尽量用Component 扫描让它走自动装配需要精细化控制实例创建过程用Bean方法并借助参数注入只有那些希望完全脱离反射、用函数式方式手工组装实例的场景才用registerBeanSupplier。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()在整个启动流程中的作用会有一种比任何源码解读都更直观的理解。