一个 Listener 注册了两次——Dubbo 版本升级把掩盖去掉了

场景:Spring Boot 2.1.5 + Dubbo 2.7.6,启动报 BeanDefinitionOverrideException,bean 名称 'dubboBootstrapApplicationListener' 路径:DubboComponentScanRegistrar → registerCommonBeans → registerInfrastructureBean(有去重)vs ServiceAnnotationBeanPostProcessor → postProcessBeanDefinitionRegistry → registerBeans(无去重)

【遗迹】— 异常现象

上篇我们看了 Dubbo LeastActive 负载均衡中 active 计数的反直觉陷阱,这篇来看另一个让你意想不到的问题——Dubbo 版本升级后启动失败,根因竟是自己重复注册了自己。

Dubbo(2.7.6)报错 BeanDefinitionOverrideException(Spring 检测到同名 bean 已存在时抛出的异常)——但你的 @EnableDubbo 只写了一次,没有重复声明任何 bean。翻到 ServiceAnnotationBeanPostProcessor 的源码才发现——不是 Spring 拒绝了你,是 Dubbo 自己注册了同一个 Listener 两次,而 Spring Boot 2.1 后 allowBeanDefinitionOverriding=false(禁止同名 bean 覆盖)把这个长期存在的代码缺陷从"静默覆盖"变成了"启动报错"。

堆栈全文

场景:Spring Boot 2.1.5 + Dubbo 2.7.6,一个 @Configuration 类标注了 @EnableDubbo

堆栈全文

BeanDefinitionOverrideException: Invalid bean definition with name
  'dubboBootstrapApplicationListener' defined in null
  at DefaultListableBeanFactory.registerBeanDefinition(DefaultListableBeanFactory.java:891)
  at AnnotatedBeanDefinitionReader.registerBean(AnnotatedBeanDefinitionReader.java:145)
  at AnnotatedBeanDefinitionRegistryUtils.registerBeans(AnnotatedBeanDefinitionRegistryUtils.java:112)
  at ServiceAnnotationBeanPostProcessor.postProcessBeanDefinitionRegistry(ServiceAnnotationBeanPostProcessor.java:113) ← Dubbo 2.7.6

第一反应

大部分开发者的第一反应:加 spring.main.allow-bean-definition-overriding=true

有些开发者会去搜 Spring 官方配置说明,查到 allowBeanDefinitionOverriding 的文档——但这只能知道这个配置的作用,不知道 Dubbo 为什么会重复注册

确实能启动。但这是掩盖,不是修。我们要问的是:为什么 Dubbo 会重复注册同一个 bean?

【发掘】— 源码追踪

回答这个问题需要从 @EnableDubbo 入手。它同时触发了 Dubbo 的两条代码路径,都在注册 DubboBootstrapApplicationListener

@EnableDubbo 的两面性

@EnableDubbo 不是单一的注解——它内部展开后是这样的:

@EnableDubbo
  ├─ @EnableDubboConfig        → 负责注册 Config Bean(ApplicationConfig 等)
  └─ @DubboComponentScan       → @Import(DubboComponentScanRegistrar.class)
       └─ DubboComponentScanRegistrar
            ├─ 注册 DubboBootstrapApplicationListener  ← 路径一
            └─ 创建 ServiceAnnotationBeanPostProcessor  ← 等待 Spring 回调
                 └─ 也注册 DubboBootstrapApplicationListener  ← 路径二

@EnableDubboConfig 负责读取 dubbo.applicationdubbo.registry 等配置并创建 ApplicationConfigRegistryConfig 等 bean。@DubboComponentScan 扫描 @Service 注解并注册服务 bean。

关键DubboComponentScanRegistrar 在处理 @Import 时,既注册了 DubboBootstrapApplicationListener(路径一),又创建了一个 ServiceAnnotationBeanPostProcessor(路径二也会注册同一个 listener)。两条路径来自同一个 @EnableDubbo,这才是问题的根源——不是用户配置错了,是 Dubbo 自身的设计导致了两条路径都做了同一件事,其中一条忘了做去重。

路径一:DubboComponentScanRegistrar

@EnableDubbo@DubboComponentScan 元标注,后者 @Import(DubboComponentScanRegistrar.class)。Spring 在处理 @Import 时,调用 registerBeanDefinitions()

DubboComponentScanRegistrar.registerBeanDefinitions

// DubboComponentScanRegistrar.java (Dubbo 2.7.6)
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
                                    BeanDefinitionRegistry registry) {
    DubboSpringInitializer.initialize(registry);
    // ↓ 还创建了一个 ServiceAnnotationBeanPostProcessor
    registerServiceAnnotationPostProcessor(packagesToScan, registry);
}

DubboSpringInitializer.initialize() 最终调用 DubboBeanUtils.registerCommonBeans(),其中注册了 DubboBootstrapApplicationListener

// DubboBeanUtils.java (Dubbo 2.7.6)
// @since 2.7.4
registerInfrastructureBean(registry,
    DubboBootstrapApplicationListener.BEAN_NAME,
    DubboBootstrapApplicationListener.class);

registerInfrastructureBean 来自 BeanRegistrar,它有去重——注册前先检查:

// BeanRegistrar.java (spring-context-support)
public static void registerInfrastructureBean(
    BeanDefinitionRegistry registry,
    String beanName, Class<?> beanType) {
    if (!registry.containsBeanDefinition(beanName)) {  // ← 有检查 ✅
        // 不存在才注册
        BeanDefinitionBuilder builder = ...;
        registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
    }
}

ServiceAnnotationBeanPostProcessor 用的 registerBeans 没有这行检查。

排查视角:这段代码是排查看两条路径重复注册的最上游。如果只看 ServiceAnnotationBeanPostProcessor 的第二次注册,容易误以为单一路径有问题。两条路径并排看,才能确认问题在于路径间的重复而非任一端的错误。

路径二:ServiceAnnotationBeanPostProcessor

同一段代码创建的 ServiceAnnotationBeanPostProcessor 实现了 BeanDefinitionRegistryPostProcessor。Spring 在执行 BeanDefinitionRegistryPostProcessor 时,调用了它的 postProcessBeanDefinitionRegistry()

ServiceAnnotationBeanPostProcessor.postProcessBeanDefinitionRegistry

// ServiceAnnotationBeanPostProcessor.java (Dubbo 2.7.6) L113
// @since 2.7.5
registerBeans(registry, DubboBootstrapApplicationListener.class);

registerBeans 来自 com.alibaba.spring.util.AnnotatedBeanDefinitionRegistryUtils——它没有去重,直接调 registry.registerBeanDefinition()

排查视角:这里就是问题断面。在 postProcessBeanDefinitionRegistry L113 设断点,Watch registry.containsBeanDefinition(DubboBootstrapApplicationListener.BEAN_NAME) 返回 true——说明这个 listener 已经被路径一注册过了。第二次注册是无意义的重复。

如果 bean 已存在,走的是 Spring 的逻辑——allowBeanDefinitionOverridingtrue 就覆盖,false 就抛异常。

两条路径的差异

路径 触发阶段 注册方法 去重检查
DubboComponentScanRegistrar @Import 处理 registerInfrastructureBean() containsBeanDefinition
ServiceAnnotationBeanPostProcessor BeanDefinitionRegistryPostProcessor registerBeans() ❌ 直接注册

两条路径都注册了同一个 Listener。一条有检查,另一条没有。

如果 registerBeans 也像 registerInfrastructureBean 一样做了 containsBeanDefinition 检查,Spring Boot 2.1 的默认配置变更就不会触发这个异常——问题不在于两条路径谁先谁后,在于第二条路径缺少幂等防护。

排查锚点:这个异常 = 先查 Dubbo 的这两个类——ServiceAnnotationBeanPostProcessorDubboComponentScanRegistrar——看它们是否都注册了同一个 Listener。代码截图中的文件路径就是你在源码中的搜索入口。

【解读】— 断面分析

堆栈指向哪里 ≠ 问题出在哪里

DefaultListableBeanFactory.registerBeanDefinition() 第 891 行(Spring 5.1)——它是检查员,不是根因:

// DefaultListableBeanFactory.java (Spring 5.1.x)
if (!isAllowBeanDefinitionOverriding()) {
    throw new BeanDefinitionOverrideException(beanName, ...);  // L891
}

堆栈抛在这里,只说明 allowBeanDefinitionOverridingfalse,Dubbo 尝试注册了已存在的 bean。真正要看的不是 Spring 的检查逻辑,是 Dubbo 为什么发出两次注册请求。

实操:在 DefaultListableBeanFactory.registerBeanDefinition() L891 设断点,Watch beanName 看到 dubboBootstrapApplicationListener;Call Stack 回溯到 ServiceAnnotationBeanPostProcessor 层,才能看到是谁发了第二次注册请求。断点在 L891 看症状,回溯到 L113 看病因。

根因在 Spring 的检查之前

ServiceAnnotationBeanPostProcessor 第 113 行才是排查断面——它注册 DubboBootstrapApplicationListener 之前,没有检查这个 bean 是否已经被 DubboComponentScanRegistrar 注册了。

这个问题的微妙之处在于两条路径的执行时序:

BeanDefinitionOverrideExceptionDefaultListableBeanFactory 抛出,但根因在 ServiceAnnotationBeanPostProcessor——它发起第二次注册时没有检查去重。异常抛在终点,病因在起点。

注册时序对比图

Spring 容器生命周期
│
├─ @Import 处理阶段
│   └─ DubboComponentScanRegistrar.registerBeanDefinitions()
│       ├─ registerCommonBeans() → 成功注册 #1
│       └─ 创建 ServiceAnnotationBeanPostProcessor bean 定义
│
├─ BeanDefinitionRegistryPostProcessor 阶段
│   └─ ServiceAnnotationBeanPostProcessor.postProcessBeanDefinitionRegistry()
│       └─ registerBeans() → 重复注册 #2  ← 根因在这里
│           └─ DefaultListableBeanFactory.registerBeanDefinition()
│               ├─ Boot <2.1 / allowOverride=true  → 覆盖
│               └─ Boot 2.1+ / allowOverride=false → BeanDefinitionOverrideException ⚡ ← 异常抛在这里

DubboApplicationContextInitializer 的版本变迁

这里还有一个版本层面的关键背景。Dubbo 2.7.0-2.7.4 的 DubboApplicationContextInitializer 启动时通过 OverrideBeanDefinitionRegistryPostProcessor 主动设置了 allowBeanDefinitionOverriding=true。这意味着早期 Dubbo 版本主动覆盖了 Spring Boot 的配置来掩盖这个重复注册。

Dubbo 2.7.5 移除了 DubboConfigBeanDefinitionConflictProcessor,2.7.8 移除了 OverrideBeanDefinitionRegistryPostProcessor——官方意识到了"替开发者改 Spring 全局配置不是好事"。但从 2.7.5 到 2.7.7 的过渡期,既没有 PostProcessor 来掩盖,又没有在 ServiceAnnotationBeanPostProcessor 里加去重检查,重复注册问题就暴露了。

Dubbo 版本 OverrideBeanDefinitionRegistryPostProcessor registerBeans 去重 结果
2.7.0-2.7.4 有,设 allowOverride=true ❌ 无 静默覆盖,用户无感知
2.7.5-2.7.7 已移除 ❌ 无 BeanDefinitionOverrideException ⚡
2.7.8+ 已移除 ✅ 有 containsBeanDefinition 检查 正常启动
3.x 已移除 ✅ registerCommonBeans 也移除了这个 listener 正常启动

Dubbo 2.7.5 到 2.7.7 的三个版本恰好落在了一个尴尬区间——掩盖去掉了,但修复还没加上。

Spring Boot 2.1 的默认配置改写

Spring Boot 2.1(Spring Framework 5.1)将 spring.main.allow-bean-definition-overriding 的默认值从 true 改为 false。原因是:同名 bean 覆盖通常意味着配置错误,与其静默失败不如显式报错。

// Spring Boot 2.1 Release Notes
// Allow bean definition overriding in the application context
// Changed default from true to false
spring.main.allow-bean-definition-overriding=false

这个变更本身是合理的——它帮助早期发现了很多因组件扫描范围重叠、自动配置冲突导致的问题。只是 Dubbo 的重复注册问题恰好在这次"安全加固"中被一并暴露了。

Dubbo 的修复

// ServiceAnnotationBeanPostProcessor.java (Dubbo 2.7.8+, fixed)
if (!registry.containsBeanDefinition(DubboBootstrapApplicationListener.BEAN_NAME)) {
    registerBeans(registry, DubboBootstrapApplicationListener.class);
}

一行 containsBeanDefinition 检查。但在这个修复之前,Dubbo 2.7.5 到 2.7.7 三个版本,影响了无数升级 Spring Boot 的用户。

有些 Bug 不是修好的——它们是随着框架版本升级,从"被掩盖"变成"被看见"。

Dubbo 不是哪天突然写错了这段代码——它只是写了一个没有做幂等检查的注册方法。Spring Boot 的默认配置变更,让这个长期存在的小缺陷从静默变成了报错。

【收获】— 排查锚点

下次遇到这个异常

BeanDefinitionOverrideException for bean 'dubboBootstrapApplicationListener' → 排查顺序:

  1. 确认 Dubbo 版本:2.7.5-2.7.7 有这个重复注册问题,2.7.8+ 已修复
  2. 确认 Spring Boot 版本:2.1+ 默认 allowBeanDefinitionOverriding=false
  3. 检查注册路径@EnableDubbo 是否同时触发了两条注册路径

修复方案

方案 操作 适用场景
临时 spring.main.allow-bean-definition-overriding=true 正在迁移 Dubbo 3.x 但未完成,需要短期过渡(建议 <1 周后摘除)
推荐 升级 Dubbo 到 2.7.8+ 或 3.x 长期维护项目,2.7.8+ 官方已修复。Dubbo 3.x 已从 registerCommonBeans 移除了 DubboBootstrapApplicationListener 的注册
结构 勿同时用 @EnableDubbo + @DubboComponentScan 检查是否有两个注解同时存在。新项目只用 @EnableDubbo

排查锚点:dubboBootstrapApplicationListener 重复注册 = Dubbo 2.7.5-2.7.7 的 registerBeans 未做去重 + Spring Boot 2.1+ 默认 allowBeanDefinitionOverriding=false + @EnableDubbo 同时触发了两条注册路径

关于 Dubbo 3.x

Dubbo 3.x 官方明确 allow-bean-definition-overriding=true "is not a good practice",不再替开发者设这个配置。如果你正在从 2.7.x 迁移到 3.x,这个配置作为短期过渡可以,长期应该去掉。

下篇我们聊 Dubbo Filter 链配置顺序导致的日志丢失问题——同样是只有读源码才能发现的排障场景。

那次版本升级没有引入新问题——它只是把 Dubbo 早就存在的问题,从静默变成了报错。

🔗 个人博客:https://opencao.cn 📺 公众号:Ai拆代码的曹操 🌟 知识星球:Ai拆代码的曹操