多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

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

Spring源码解析:doRegisterBean如何完成BeanDefinition注册 在排查一个Bean重复定义的诡异问题时我顺着Spring的启动日志一路追到了doRegisterBean()这个方法。当时项目里有两个Component扫描路径交叉覆盖了同一个类结果启动时直接抛了BeanDefinitionStoreException报错信息指向的就是这个方法内部的覆盖检查逻辑。从那以后每次跟BeanDefinition打交道我都会下意识地回到doRegisterBean()这个源头来看。doRegisterBean()是SpringBeanDefinition注册链路里最核心的一环它负责把解析好的BeanDefinition登记到容器的注册表中。这篇文章我会从方法定位、完整流程、源码逐段拆解、动态注册实践到常见坑位完整讲透这个方法适合正在啃Spring源码的初学者也适合遇到Bean注册相关疑难杂症想查根因的开发者。1. doRegisterBean的定位与核心职责1.1 从registerBeanDefinition到doRegisterBean的演进在Spring 5.1之前AnnotatedBeanDefinitionReader和ClassPathBeanDefinitionScanner内部都是直接调用registry.registerBeanDefinition()来完成注册工作的。Spring 5.1之后代码里多了一层封装注册的具体逻辑被挪到了doRegisterBean()方法里而doRegisterBean()内部再调用registerBeanDefinition()往DefaultListableBeanFactory的注册表里塞数据。这个方法名的do前缀很有Spring特色跟doGetBean、doCreateBean一样表示真正干活的实现方法。理解这个名字的来历有助于你后续读Spring源码时快速定位主线逻辑带do的都是具体实现去掉do的方法是门面入口。从职责划分来看doRegisterBean()只做跟构造并登记BeanDefinition相关的事情不做Bean实例化不做属性填充更不做初始化。它的边界非常清晰把如何描述这个Bean的信息类名、作用域、懒加载、初始化方法等组装成BeanDefinition然后交给容器注册表保存起来。1.2 这个方法的调用入口都有谁doRegisterBean()在整个Spring框架里主要有两个调用来源。第一个是AnnotatedBeanDefinitionReader它在处理注解驱动的Bean注册时比如AnnotationConfigApplicationContext.register()传入的配置类或普通Bean类会调用doRegisterBean()。第二个是ClassPathBeanDefinitionScanner在做包扫描时扫描器会把每个符合条件的class文件解析成一个候选ScannedGenericBeanDefinition随后也通过doRegisterBean()完成登记。除了这两个核心来源Spring自己的内部扩展机制也会用到这个方法。比如ConfigurationClassBeanDefinitionReader在解析Bean方法时虽然走的是loadBeanDefinitionsForBeanMethod()那条链路但最终注册BeanDefinition时仍然会经过类似registerBeanDefinition的逻辑。理解了这些调用入口你就能明白doRegisterBean()在整个BeanDefinition声明周期里处于哪个环节它是定义解析完成之后、真正登记进容器注册表之前的最后一道工序。2. 核心流程拆解注册一个BeanDefinition到底经历什么2.1 从类名到BeanDefinition的转化如果传入doRegisterBean()的是一个Class对象比如你手动调用register(SomeClass.class)方法第一步会先决定要不要给这个类生成一个BeanDefinition。源码里有一个关键分支如果ClassUtils.isPresent()判断失败或者传入的是class但需要被跳过就不会继续往下走。正常情况下方法会创建一个AnnotatedGenericBeanDefinition把类的元数据AnnotationMetadata封装进去。这里有一个容易忽略的点AnnotatedGenericBeanDefinition不是简单包一层Class它会读取类上的注解元数据。比如类上标注了Scope、Lazy、Primary、DependsOn这些注解这些注解的元信息会被提取出来作为后续属性填充的依据。正因为这一步把注解信息读进了BeanDefinition后面AnnotationConfigUtils.processCommonDefinitionAnnotations()才能根据这些元数据去设置作用域、延迟初始化等属性。2.2 Scope、Lazy与注解属性的处理顺序doRegisterBean()里有一个专门处理公共注解的方法调用AnnotationConfigUtils.processCommonDefinitionAnnotations(abd)。它会依次处理Lazy、Primary、DependsOn、Role、Description这些注解。如果你在类上写了Lazy(true)这里的逻辑就会把abd.setLazyInit(true)执行掉。Scope的处理比较特殊它不在processCommonDefinitionAnnotations()里面而是由doRegisterBean()开头那段代码通过ScopedProxyMode来判断。如果Scope指定了proxyMode ScopedProxyMode.TARGET_CLASS或INTERFACES方法会走ScopedProxyCreator.createScopedProxy()创建一个代理注册如果只是普通的proxyMode NO就直接把当前BeanDefinition注册进去。这块的逻辑顺序对使用有实际影响你如果想通过自定义BeanDefinitionRegistryPostProcessor去修改某个BeanDefinition的scope一定要记住doRegisterBean()是在扫描/解析阶段就处理完注解了你的后置处理器拿到的是已经处理过的结果。3. 覆盖规则与beanName生成两个最容易踩坑的细节3.1 allowBeanDefinitionOverriding到底在管什么doRegisterBean()走到最后会调用registry.registerBeanDefinition(beanName, definitionToUse)。如果你用的是DefaultListableBeanFactory这个方法内部有非常关键的覆盖检查逻辑一旦发现beanDefinitionMap里已经存在同名的BeanDefinition且allowBeanDefinitionOverriding为false直接抛出BeanDefinitionStoreException。这个allowBeanDefinitionOverriding就是Spring Boot 2.1之后默认关闭的那个开关。很多人在升级Spring Boot版本后发现启动报错BeanDefinitionOverrideException根因就在这儿多个配置源定义了同名的Bean而注册时覆盖检查不通过。结合doRegisterBean()的流程来看覆盖检查发生在所有解析完成之后。这意味着哪怕两个Bean方法的返回值类型完全相同只要方法名不一样生成的beanName不同就不会触发覆盖检查。反过来如果两个Component类在扫描时生成了相同的beanName覆盖检查一定会介入。理解这层关系排查启动冲突时会快很多。3.2 beanNameGenerator的默认策略与自定义方案doRegisterBean()的签名里有一个BeanNameGenerator参数。如果没有显式传入Spring会使用AnnotationBeanNameGenerator。这个生成器的默认策略非常直接如果类上有Component或其派生注解Service、Repository、Controller就用注解里的value值作为beanName如果没有显式指定value就用类的短名称并首字母小写。自定义BeanNameGenerator的典型场景是那些类名缩写不满足命名规范的老项目。我在一个遗留系统里见过无数个类似UserDAOImpl这种类名如果按默认策略生成的beanName就是userDAOImpl跟团队的命名预期完全不一致。解决方案就是注册一个自定义的BeanNameGenerator在generateBeanName()里加入自己的规则。在doRegisterBean()这个层面beanNameGenerator参数是直接透传的。想验证自定义生成器是否生效可以在doRegisterBean()的入口打断点直接看传入的generator对象是不是你注册的那一个排查效率非常高。4. 源码逐段拆解doRegisterBean为什么这么写4.1 方法签名与参数含义先看方法签名T void doRegisterBean(ClassT beanClass, Nullable SupplierT instanceSupplier, Nullable String name, Nullable Class? extends Annotation[] qualifiers, BeanNameGenerator beanNameGenerator, Nullable MapString, Object attributeOverrides)beanClass是要注册的目标类instanceSupplier用来走Supplier方式直接供应实例通常跟Bean的lambda风格注册相关name是显式指定的beanNamequalifiers是一组注解类型通常用来补充限定符比如QualifierbeanNameGenerator是名称生成器attributeOverrides是属性的覆盖映射。这套参数设计很有讲究它几乎覆盖了注册一个BeanDefinition需要的所有可变维度。你传入的name和qualifiers会直接影响最终注册到容器里的beanName以及后续通过Qualifier查找时的匹配关系。4.2 方法体内的完整执行顺序源码大致按以下顺序执行我把关键步骤整理出来供对照走读AnnotatedGenericBeanDefinition abd new AnnotatedGenericBeanDefinition(beanClass); if (instanceSupplier ! null) { abd.setInstanceSupplier(instanceSupplier); } // processCommonDefinitionAnnotations会处理Lazy、Primary、DependsOn等 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)); } } } ScopeMetadata scopeMetadata this.scopeMetadataResolver.resolveScopeMetadata(abd); abd.setScope(scopeMetadata.getScopeName()); if (attributeOverrides ! null) { abd.setAttributes(attributeOverrides); } String beanName (name ! null ? name : beanNameGenerator.generateBeanName(abd, this.registry)); AnnotationConfigUtils.processCommonDefinitionAnnotations(abd, beanName); BeanDefinitionHolder definitionHolder new BeanDefinitionHolder(abd, beanName); definitionHolder AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry); this.registry.registerBeanDefinition(definitionHolder.getBeanName(), definitionHolder.getBeanDefinition());这里有一个细节值得注意processCommonDefinitionAnnotations被调用了两次。第一次是处理类上的通用注解第二次传入beanName再次处理这时会额外解析DependsOn注解里可能写的按名称依赖。为什么要传beanName再处理一次因为DependsOn里依赖的值可能是一个还没确定下来的别名拿到最终的beanName后再解析一次结果更准确。运营代码走读你会发现这个方法其实没有任何魔法就是按顺序组装信息、设置属性、生成名称、注册登记。Spring框架里很多核心方法都是这种流水线式的结构逐行读下来并不费劲真正容易晕的是方法之间的调用关系。5. 动态注册BeanDefinition的完整可落地示例5.1 借助AnnotatedBeanDefinitionReader手动注册实战里用得比较多的场景是不想扫描整个包只想针对某个类手动注册BeanDefinition。用AnnotatedBeanDefinitionReader非常直接AnnotationConfigApplicationContext context new AnnotationConfigApplicationContext(); AnnotatedBeanDefinitionReader reader new AnnotatedBeanDefinitionReader(context); reader.register(MyService.class); context.refresh();这段代码最终就是走doRegisterBean()完成注册的。如果你想定制beanName可以换BeanDefinitionRegistry的方式手动构造创建AnnotatedGenericBeanDefinition设置属性再调用registry.registerBeanDefinition()。如果你想更精细地控制BeanDefinition比如设置scope为prototype或者添加qualifier可以照doRegisterBean()内部的逻辑手工完成AnnotatedGenericBeanDefinition abd new AnnotatedGenericBeanDefinition(MyService.class); abd.setScope(ConfigurableBeanFactory.SCOPE_PROTOTYPE); abd.setPrimary(true); DefaultListableBeanFactory registry (DefaultListableBeanFactory) context.getBeanFactory(); registry.registerBeanDefinition(myServiceAlias, abd);注意手动构造BeanDefinition并直接调用registerBeanDefinition跟doRegisterBean()的区别在于后者会额外处理ScopedProxyMode、属性覆盖、限定符等逻辑。如果你需要完整的注解语义建议还是走reader.register()。5.2 利用ScopeMetadataResolver处理代理模式当你的类使用了Scope(proxyMode ScopedProxyMode.TARGET_CLASS)时doRegisterBean()内部会创建一个ScopedProxyFactoryBean的BeanDefinition来替换原有定义。手动注册时如果想复刻这个逻辑可以写个简单的工具方法ScopeMetadataResolver resolver new AnnotationScopeMetadataResolver(); ScopeMetadata metadata resolver.resolveScopeMetadata(abd); abd.setScope(metadata.getScopeName()); BeanDefinitionHolder holder AnnotationConfigUtils.applyScopedProxyMode(metadata, new BeanDefinitionHolder(abd, beanName), registry); registry.registerBeanDefinition(holder.getBeanName(), holder.getBeanDefinition());这段代码其实就是从doRegisterBean()里抠出来的关键片段。理解它之后以后看到容器里出现scopedTarget.开头命名的BeanDefinition就不会觉得奇怪了——那是代理模式注册时的副产品。6. 常见问题与排坑实录6.1 BeanDefinitionOverrideException同名Bean覆盖被拒现象项目启动时抛出BeanDefinitionOverrideException提示某个beanName被重复注册且allowBeanDefinitionOverriding为false。排查思路先看报错的beanName对应哪些类再去看扫描路径里是不是存在同名类或者不同包下有没有同名短类名。Spring Boot 2.1之后的默认策略是禁止覆盖如果你确认重复注册不是代码缺陷、而是确实需要后注册的覆盖先注册的可以通过配置spring.main.allow-bean-definition-overridingtrue打开覆盖。但真正该做的是消除根源而不是盲目放开开关。6.2 beanName与预期不一致大小写与缩写问题现象手动注册MyDemoService后通过getBean(myDemoService)能拿到但按getBean(MyDemoService)拿不到或者团队里习惯用全大写缩写。原因默认的AnnotationBeanNameGenerator会把短类名首字母转小写并不做其他转换。处理方案自定义BeanNameGenerator或者在注册时显式指定name参数。我建议在项目里定义一个统一的生成器把注册和查名的规则固定下来减少团队协作时的认知成本。6.3 手动注册的Bean拿不到扩展注解语义现象手动new了一个GenericBeanDefinition注册进去结果Autowired、Value在这些Bean里不生效。原因手动构造GenericBeanDefinition没有经过AnnotationConfigUtils.processCommonDefinitionAnnotations()很多注解元数据没有被提取。处理方案用AnnotatedGenericBeanDefinition替代GenericBeanDefinition或者走AnnotatedBeanDefinitionReader注册。这样doRegisterBean()里的注解处理逻辑才会被触发Autowired等行为才符合预期。6.4 断点排查技巧拿到完整的注册上下文在实际调试过程中我发现doRegisterBean()是一个非常理想的断点位置。因为这个方法能拿到beanClass、beanName、BeanDefinition、registry四个关键对象你可以在一个断点里同时审查这次注册到底注册了谁、注册成了什么名字、定义信息是否完整。排查BeanDefinition相关问题时我个人习惯在以下三个位置打条件断点断点位置看什么典型异常场景doRegisterBean()入口beanClass、name、qualifiers确认哪个类被异常注册registerBeanDefinition()前一行beanName、abd属性确认BeanDefinition内容是否正确DefaultListableBeanFactory.registerBeanDefinition()内部beanDefinitionMap、覆盖检查条件排查名称冲突、覆盖失败这个组合断点思路对任何BeanDefinition相关问题的定位都有效实测下来定位效率非常高。6.5 一些实操心得我在多个项目里反复踩过BeanDefinition注册相关的坑这里把几条经验分享出来。一是不要轻易全局关闭覆盖检查。allowBeanDefinitionOverridingtrue虽然能快速让项目启动成功但会让容器里的BeanDefinition变得不可预测。你根本不知道哪个定义被后加载的同名定义顶掉了线上排查成本极高。宁可花时间把重复定义的来源找出来也别图一时方便开这个开关。二是尽量用Bean方法做动态注册。Bean方法的返回值类型和配置方式都很直观生成的beanName默认是方法名且能自动处理依赖注入比手工构造BeanDefinition要安全得多。doRegisterBean()这类底层方法更适合框架开发者或需要深度定制的人使用。三是手动注册时一定要校验beanName的唯一性。调用registerBeanDefinition()之前可以先检查containsBeanDefinition(beanName)或者利用Registry的isBeanNameInUse()方法预判冲突。提前拦截比事后排查快得多。四是别忽略ScopedProxyMode带出来的额外定义。如果你用Scope(proxyMode TARGET_CLASS)容器里会多出scopedTarget.xxx的BeanDefinition。排查冲突时看到这种命名不要以为是被恶意注册的它是代理模式的正常产物。五是锁定doRegisterBean()的日志入口。Spring在registerBeanDefinition()时不会打详细的info日志但给org.springframework.beans.factory包开DEBUG日志后你能看到Registering bean definition相关的输出。线上问题排查时这个开关比断点更实用。个人项目里的经验是读源码时不要急着把每个方法都读透抓住doRegisterBean()这种承上启下的关键节点顺着调用链上下游各自展开很快就能建立起完整的认知地图。以后再遇到任何BeanDefinition相关的问题第一反应就应该是这个定义是在哪里注册的、用什么名字注册的、经历了哪些处理这时候doRegisterBean()就是你脑子里最清晰的那块拼图。
返回列表