平时跟Spring源码打过照面的同学,肯定见过doRegisterBean()这个名字。它藏在AnnotatedBeanDefinitionReader里,是你注册配置类、手工往容器里塞Bean时必经的那条路。很多人会把注意点放在registerBeanDefinition()上,觉得那才是真正落库的动作,但实际调试一个自定义注册场景时,卡壳往往就卡在doRegisterBean()前面——比如BeanName是怎么生成的?@Scope什么时候被读到的?Supplier到底在哪个环节介入?这些问题的答案全都在这个私有方法里。
这篇文章就把doRegisterBean()掰开揉碎讲清楚。我会从BeanDefinition这个基础概念讲起,再走一遍AnnotationConfigApplicationContext创建时的调用链,然后把方法签名里的每个参数都拆开解释,最后用一段手工注册的Demo把整个过程复现出来。适合正在看Spring源码、自己写Starter或者做框架扩展的Java开发者,看完可以直接照着实操,也能顺手排查掉几个常见的注册失效问题。
1. doRegisterBean()是什么,以及它为什么值得你研究
1.1 先认识BeanDefinition这个“身份证”
Spring容器里管理的不直接是对象,而是一张张BeanDefinition。你可以把它理解成身份证:上面记录了这个Bean的类全名、作用域是单例还是原型、是不是懒加载、依赖哪些其他Bean、初始化方法叫什么。容器启动后,第一件事就是把这些“身份信息”收集全,放到BeanDefinitionRegistry里,之后才开始挨个实例化。
这里有个容易混淆的点:BeanDefinition和Bean实例不是一回事。前者是“计划书”,后者是“成品”。Spring官方文档反复强调,容器的核心流程就是:加载BeanDefinition -> 处理 BeanDefinition之间的关系 -> 实例化。你在配置类里写@Bean方法、在类上标注@Component、用XML定义<bean>,最终都会被转换成一张BeanDefinition放进注册表。所以,理解doRegisterBean()前,脑子里先得有这层映射关系。
1.2 doRegisterBean()和registerBeanDefinition()怎么分工
很多文章会把doRegisterBean()和registerBeanDefinition()混着说,其实它们分工完全不同。registerBeanDefinition()定义在BeanDefinitionRegistry接口里,是DefaultListableBeanFactory实现的底层注册动作,作用非常简单——把传入的BeanDefinition按名字存进内部的ConcurrentHashMap。
而doRegisterBean()是AnnotatedBeanDefinitionReader里的一个私有方法,它做的事更靠前:把一个Class对象转换成AnnotatedGenericBeanDefinition,解析@Scope、@Lazy这些公共注解,自动生成BeanName,再交给BeanDefinitionReaderUtils.registerBeanDefinition()完成最终注册。也就是说,doRegisterBean是“加工+准备”,registerBeanDefinition是“落库”。搞清楚这个先后顺序,后面看源码就不会晕。
从命名习惯上说,Spring内部很多实现类都会包一层doXxx()方法,比如doCreateBean()、doGetBean()。这通常是模板方法模式留下的痕迹,含义是“真正干活的实现”。看到do前缀,第一反应应该是:肯定有一个对外入口方法,会先做参数校验或预处理,然后转到这里执行核心逻辑。
2. 从容器创建到doRegisterBean()的完整调用链
2.1 入口在AnnotatedBeanDefinitionReader.register()
我们平时最常写的启动代码是new AnnotationConfigApplicationContext(AppConfig.class)。这个构造器内部做了三件事:创建AnnotatedBeanDefinitionReader、创建ClassPathBeanDefinitionScanner、然后调用register(componentClasses)把传入的配置类注册进容器。
顺着register()往下走,代码是这样的(以Spring 5.x版本为例,我做了精简):
public class AnnotatedBeanDefinitionReader { public void register(Class<?>... componentClasses) { for (Class<?> componentClass : componentClasses) { registerBean(componentClass); } } public <T> void registerBean(Class<T> beanClass, Object... customizers) { doRegisterBean(beanClass, null, null, null, customizers); } private <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); // 1. 解析 @Conditional 条件注解,命中则不注册 if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) { return; } // 2. 解析 @Scope,默认是 singleton ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd); abd.setScope(scopeMetadata.getScopeName()); // 3. 生成 beanName,显式指定就用显式值,否则走命名策略 String beanName = (name != null ? name : this.beanNameGenerator.generateBeanName(abd, this.registry)); // 4. 处理 @Lazy、@Primary、@DependsOn 等公共注解 AnnotationConfigUtils.processCommonDefinitionAnnotations(abd, this.environment); // 5. 处理 qualifiers,比如 @Qualifier 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)); } } } // 6. 用 Supplier 覆盖默认实例化逻辑 if (supplier != null) { abd.setInstanceSupplier(supplier); } // 7. 执行自定义定制器 if (customizers != null) { for (BeanDefinitionCustomizer customizer : customizers) { customizer.customize(abd); } } // 8. 包装成 BeanDefinitionHolder,转给真正的注册方法 BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd, beanName); BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry); } }这段代码有一个细节很多人没注意到:AnnotatedGenericBeanDefinition在第一步就拿到了类的全部注解元数据。它内部持有StandardAnnotationMetadata,可以直接查询类上有没有@Scope、@Lazy、@Primary这些注解。这也是为什么doRegisterBean()不需要扫描就能处理注解配置,因为它处理的不是类路径下的未知类,而是开发者明确指定的类。
2.2 内置注解处理器为什么也走这个流程
AnnotatedBeanDefinitionReader构造时还会调用AnnotationConfigUtils.registerAnnotationConfigProcessors(registry),把ConfigurationClassPostProcessor、AutowiredAnnotationBeanPostProcessor、CommonAnnotationBeanPostProcessor等几个关键后置处理器注册进容器。
这些内置组件的注册方式特别值得留意:它们不是手动new一个BeanDefinition再往里塞属性,而是同样走doRegisterBean()。比如ConfigurationClassPostProcessor会以internalConfigurationAnnotationProcessor这个名字被注册。这么设计的好处是:内置组件和用户自定义Bean共用一套注册管线,解析Scope、处理公共注解等逻辑不会出现两套行为,你甚至可以注册一个同名的后置处理器把它覆盖掉,容器的人格分裂风险被降到了最低。
对调试有指导意义的是,当你看到internalConfigurationAnnotationProcessor出现在getBeanDefinitionNames()结果里,就知道容器内部的注解驱动基础设施已经就绪。如果这个Bean丢失,多半是因为某个扩展点把整个注册表清掉了——后面第5章会聊到类似问题。
3. 核心参数拆解与BeanName生成逻辑
3.1 五个参数,逐个说明
doRegisterBean()的完整签名带五个参数,分别对应五种注册诉求:
- beanClass:要注册的类。注意它必须是
Class对象,不能传实例。 - name:显式指定的BeanName。传null时走自动命名逻辑。
- qualifiers:限定符数组。用来处理
@Qualifier、@Primary、@Lazy这类“候选者限定”注解。这里有个冷知识:@Primary和@Lazy并不是Qualifier注解,但在Spring内部被当成qualifier的一类简化处理,所以在doRegisterBean里会单独特判。 - supplier:实例化提供者。它传入后会执行
abd.setInstanceSupplier(supplier),也就是把原本“等创建时再反射构造”的逻辑,换成“创建时直接调用这个Supplier拿对象”。这个能力在Spring 5之后越来越重要,因为它是@Bean方法背后偷懒的实现方式之一。 - customizers:自定义定制器数组。每个
BeanDefinitionCustomizer都能拿到AnnotatedGenericBeanDefinition并修改它,适合在注册前改primary、改initMethodName等。
这些参数对应到使用场景上很有意思。比如写一个动态注册Filter的扩展点,你可能想用supplier避免反射的开销;写一个自动配置类想控制多个候选Bean的优先级,你就会用customizers把primary设为true。它们不是摆设,每一个都是框架作者在真实扩展中打磨出来的需求。
3.2 beanName怎么自动生成的
自动生成BeanName的逻辑在AnnotationBeanNameGenerator里,doRegisterBean()通过this.beanNameGenerator.generateBeanName(abd, registry)调用。它的规则分三步:
第一,看类上有没有@Component或@Component的派生注解(比如@Service、@Repository、@Controller)。有的话直接取注解里的value值。第二,如果注解没写value,就取类的短类名,并且把首字母改成小写。第三,如果是匿名类,会退化到SimpleBeanNameGenerator的规则。
需要注意一个细节:首字母小写并不是无脑toLowerCase(),而是调用Introspector.decapitalize()。这意味着如果类名前两个字母都是大写,比如URLService,生成的名字会保持URLService而不是uRLService。这个行为和JavaBeans规范保持一致,也是面试里偶尔会出现的坑。
实际环境里,我更推荐显式指定BeanName。自动命名在重构类名后会静默改变容器里的BeanName,如果某些代码通过字符串硬编码获取Bean,就会在一顿重构后悄然失效。doRegisterBean()留出name参数就是为了让你在关键入口把名字卡死,省得后面排查“怎么多了一个Bean”或者“怎么少了一个Bean”。
4. 手工注册一个BeanDefinition的完整实验
4.1 最小实验:绕过扫描,直接注册
纸上谈兵到此为止,直接上实验。我们要做的是不通过@ComponentScan,也不写@Import,用一个最原始的AnnotatedBeanDefinitionReader把类注册进容器。
先定义两个类,模拟一个“外部服务”,业务场景是自己封装一个报表导出组件,不希望被Spring扫描,而是由代码显式注册:
public class ExternalReportService { private final ReportDataSource dataSource; public ExternalReportService(ReportDataSource dataSource) { this.dataSource = dataSource; } public void generate() { System.out.println("report generated from " + dataSource.getUrl()); } } public class ReportDataSource { private String url = "jdbc:report://local"; public String getUrl() { return url; } }然后在启动入口手动把它们注册进容器:
public class ManualRegistrationDemo { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); AnnotatedBeanDefinitionReader reader = new AnnotatedBeanDefinitionReader(context); reader.registerBean(ReportDataSource.class); reader.registerBean(ExternalReportService.class, (BeanDefinitionCustomizer) bd -> bd.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE)); context.refresh(); ExternalReportService service = context.getBean(ExternalReportService.class); service.generate(); } }运行后你会发现ExternalReportService的构造函数正常被调用,dataSource由Spring自动注入。这里有两个点值得展开:
一是registerBean(ExternalReportService.class, ...)能完成构造注入,是因为doRegisterBean()在注册时把类上的构造器信息写进了AnnotatedGenericBeanDefinition,后续AbstractAutowireCapableBeanFactory在选择构造器时能识别出唯一的构造器参数类型。如果这个类有多个构造器,就要自己写@Autowired标注或通过Supplier指定对象创建方式,否则会报NoUniqueBeanDefinitionException。
二是全程序没有@ComponentScan,证明注册行为不依赖包扫描定位。整个链路就是由doRegisterBean()直接驱动,这是理解Spring内部“类是如何从代码变成Bean”的最佳入口。
4.2 注册之后,立刻能看出影响的几个设置点
doRegisterBean()跑到最后会执行开发者传入的customizers。这个阶段改什么,直接影响后面Bean的装配行为。我实际用得最多的是下面几个:
- 调整Primary属性:
bd.setPrimary(true),解决导入多个同类型候选Bean时的注入歧义。 - 修改依赖描述:
bd.setDependsOn("cacheManager"),把依赖关系往后推迟几层。 - 修改实例化模式:
bd.setScope(ConfigurableBeanFactory.SCOPE_PROTOTYPE),让该Bean每次获取都是新实例。 - 接上初始化回调:
bd.setInitMethodName("init"),给无@PostConstruct的外部类补充初始化逻辑。
这里稍微补充一下Supplier。很多人误以为Supplier就是“在注册时立刻创建对象”,其实不是。它只是把实例供应商放进BeanDefinition,真正的调用发生在容器实例化阶段。这意味着如果你传supplier = () -> new Something(),每创建一个该Bean的实例都会调用一次这个lambda。对于原型作用域Bean来说,Supplier比反射构造更快,因为它绕过了Class.getDeclaredConstructor()的解析开销;对于单例Bean来说,它只有第一次创建时会调用一次。这个选择值得在性能敏感的自定义注册场景里优先考虑。
5. 实操中常见问题与排查清单
5.1 注册了却没生效
这是问得最多的一个现象。代码明明调用了registerBean(),但refresh后getBean()却报NoSuchBeanDefinitionException。
第一排查方向是@Conditional。Spring在doRegisterBean()一开始就会调用conditionEvaluator.shouldSkip(),如果类上标了@Conditional(SomeCondition.class)且条件不满足,会直接return跳过注册,整个过程没有任何报错,也没有日志提醒,看起来就是“注册了个寂寞”。
第二排查方向是注册时机。一次refresh()代表容器完成了整个“加载BeanDefinition -> 实例化”的生命周期。如果在context.refresh()之后才调用reader.registerBean(),新增的BeanDefinition不会触发实例化,除非手动再调用一次refresh()或preInstantiateSingletons()。所以最安全的做法是在refresh之前把该注册的都注册完。
第三排查方向看类本身有没有被ConfigurationClassPostProcessor干预。如果传给registerBean()的类上有@Configuration标记,它会被当作完整配置类处理,内部@Bean方法会被解析成多个BeanDefinition;如果只是个普通类,就只创建一个BeanDefinition。两者预期不同,一旦混淆就会出现“注册了但Bean数量对不上”的疑惑。
5.2 重复注册与同名覆盖
Spring默认是允许BeanDefinition覆盖的,DefaultListableBeanFactory里的allowBeanDefinitionOverriding默认值为true。当两次doRegisterBean()注册了同一个beanName时,第二次的BeanDefinition会把第一次替换掉,默认只在日志里打一条INFO。
有次我在一个插件化项目里调试,两条初始化链路分别注册了两个同名组件,结果实例化出来的Bean行为完全不对,排查很久才发现是后一个BeanDefinition覆盖了前一个。后来我在自定义注册逻辑里加了防御:
if (beanFactory.containsBeanDefinition("reportService")) { // 检查已有BeanDefinition,决定是跳过还是替换 throw new IllegalStateException("reportService已经存在,请检查是否重复注册"); }更彻底一些,可以在初始化容器前设置beanFactory.setAllowBeanDefinitionOverriding(false),让重复注册直接抛异常,把问题在启动阶段暴露出来,而不是留到运行时。
5.3 什么时候更适合用Supplier代替反射构造
doRegisterBean()把Supplier作为独立参数,和beanClass并列,是有原因的。最典型的使用场景是:这个Bean的构造函数参数无法由Spring容器直接提供,而是来自一个运行时配置对象。比如从System.getProperty()读取数据库连接、动态创建线程池的ThreadPoolExecutor、或者从另一个非Spring管理的框架里获取单例对象。
reader.registerBean(TaskExecutor.class, () -> { ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 4, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100), new NamedThreadFactory("report-worker")); return ExecutorServiceAdapter.wrap(executor); });用Supplier时有一个很隐蔽的坑:AnnotatedGenericBeanDefinition虽然设置了类信息,但如果Supplier返回的是一个代理对象或实现类,而BeanDefinition里的beanClass还是TaskExecutor接口,某些依赖类型检查的地方会认为类型不匹配。此时最好在customizers里同步修正bd.setBeanClass(实际类.class),或者干脆让Supplier返回类型严格对上声明类型,否则会碰到奇怪的BeanNotOfRequiredTypeException。
结尾
把doRegisterBean()这个私有方法看完,再回头看Spring那几百行启动代码,心里会有种“原来没那么多魔法”的踏实感。它解决的核心问题很简单:一个类怎么变成一张注册表里的BeanDefinition,以及在这个加工过程中,框架给你塞了哪些可插拔的扩展点。我在实际写中间件和Starter时,最常用到它的地方就是动态注册Filter、定时任务和外部服务组件;这些组件通常不在默认扫描路径下,用reader.registerBean()手动注册比硬堆@Import更直观,也比写XML更安全。
最后分享一个我长期保留的排查习惯:凡是不确定某个Bean是谁注册的、在哪个阶段注册的,就先在doRegisterBean()入口处打断点,然后看调用栈。调用栈会清清楚楚告诉你注册的来源是@ComponentScan、@Import、@Bean方法还是你自己的动态注册代码。别在getBean()那里反复怀疑人生,问题基本都出现在“注册”这个环节,而不是“获取”这个环节。把这个方法吃透,你再看其它doXxx()方法,比如doCreateBean()、doGetBean(),就会发现自己已经会读Spring源码了。