Spring竟然可以创建“重复”名称的bean?—一次项目中存在多个bean名称重复问题的排查

spring,竟然,可以,创建,重复,名称,bean,一次,项目,存在,多个,问题,排查 · 浏览次数 : 399

小编点评

3.1 为什么spring可以存在“重复“名称的bean我们这里重新梳理下bean的创建流程:在spring注入一个普通bean的过程中,会将通过反射创建的空属性对象赋值,如果发现其依赖的属性也是一个bean,那么会首先去获取这个bean,如果获取不到的话则会转而去创建bean。而此时要创建的bean成为innerBean,并不会被spring其他bean共享,所以可以在名称上是重复的。 3.2 innerBean的用法还是我们刚刚的例子,我们可以将其改写成下面的这个样子: <bean id=\"cacheClusterConfigProvider\" class=\"com.wangyin.rediscluster.provider.CacheClusterConfigProvider\"> <property name=\"providers\"> <list> <!--# 引用ccProviderRef--> <ref bean=\"ccProviderRef\"></ref> </list> </property></bean><!--# 定义了一个公共的ccProviderRef--><bean id=\"ccProviderRef\" class=\"com.wangyin.rediscluster.provider.CCProvider\"> <!--# 替换为当前环境的R2M 3C配置中心地址(详见上方R2M 3C服务地址)--> <property name=\"address\" value=\"${r2m.zkConnection}\"/> <!--# 替换为R2M集群名--> <property name=\"appName\" value=\"${r2m.appName}\"/> <!--# 替换为当前环境的客户端对应配置中心token口令(参考上方token获取方式)--> <property name=\"token\" value=\"${r2m.token}\"/> <!--# 替换为集群认证密码--> <property name=\"password\" value=\"${r2m.password}\"/></bean>在上面的例子中我们定义了一个普通bean,并将其引用到我们想要的属性中。此时ccProviderRef作为一个普通bean,是可以被其他bean引用的,但是此时bean的名称就不可重复。。归纳总结以上内容,生成内容时需要带简单的排版

正文

作者:京东科技 韩国凯

一、项目中存在了名称重复的bean

众所周知,在Spring中时不能够创建两个名称相同的bean的,否则会在启动时报错:

image-20230322100944642

但是我却在我们的spring项目中发现了两个相同名称的bean,并且项目也可以正常启动,对应的bean也可以正常使用。

因为项目原因中会用到多个redis集群,所以有配置了多个redis环境,并且在id上做了区分。

但是在配置redis环境的时候,两个环境beanid却是相同的。

<bean id="cacheClusterConfigProvider" class="com.xxx.rediscluster.provider.CacheClusterConfigProvider">
    <property name="providers">
        <list>
            //创建了一个名为 ccProvider 的bean
            <bean id="ccProvider" class="com.xxx.rediscluster.provider.CCProvider">
                <!--# 替换为当前环境的R2M 3C配置中心地址(详见上方R2M 3C服务地址)-->
                <property name="address" value="${r2m.zkConnection}"/>
                <!--# 替换为R2M集群名-->
                <property name="appName" value="${r2m.appName}"/>
                <!--# 替换为当前环境的客户端对应配置中心token口令(参考上方token获取方式)-->
                <property name="token" value="${r2m.token}"/>
                <!--# 替换为集群认证密码-->
                <property name="password" value="${r2m.password}"/>
            </bean>
        </list>
    </property>
</bean>

<bean id="tjCacheClusterConfigProvider" class="com.xxx.rediscluster.provider.CacheClusterConfigProvider">
    <property name="providers">
        <list>
            //这里竟然也是 ccProvider 
            <bean id="ccProvider" class="com.xxx.rediscluster.provider.CCProvider">
                <!--# 替换为当前环境的R2M 3C配置中心地址(详见上方R2M 3C服务地址)-->
                <property name="address" value="${r2m.tj.zkConnection}"/>
                <!--# 替换为R2M集群名-->
                <property name="appName" value="${r2m.tj.appName}"/>
                <!--# 替换为当前环境的客户端对应配置中心token口令(参考上方token获取方式)-->
                <property name="token" value="${r2m.tj.token}"/>
                <!--# 替换为集群认证密码-->
                <property name="password" value="${r2m.tj.password}"/>
            </bean>
        </list>
    </property>
</bean>


大家也都知道,<bean>标签可以声明一个bean,是肯定会被spring解析并且使用的,那么为什么在这里面两个相同的bean名称却不会报错呢?

image-20230322103204708

可以看到我们创建的bean是正常的,并且从功能上来说也是可以使用的。

二、问题的排查过程

2.1 尝试直接找到创建重复bean位置

首先debug尝试找到创建重复bean时的相关信息,看看有没有什么思路

image-20230322103624912

然后重启项目,选择debug模式,但是在运行之后IDEA提示断点被跳过了

image-20230322104033613

查阅了一些资料跟方式都不起作用,遂放弃此思路。

2.2 从创建其父bean开始寻找思路

放弃了上述思路后想到,可以凭借之前学习的spring源码从代码层面去排查此问题

将断点设置到创建reids bean处

image-20230322104714244

果然,断点在这里是能进来的

image-20230322104804469

那么我们的思路就很简单了。

在spring中,装配属性的步骤发生在:populateBean(beanName, mbd, instanceWrapper)的过程中,如果发现其属性也是一个bean,那么会先获取bean,如果不存在则会先创建其属性bean,然后创建完成之后将属性bean赋值给要装配的bean。

//循环要装配bean的所有属性
for (PropertyValue pv : original) {
   if (pv.isConverted()) {
      deepCopy.add(pv);
   }
   else {
      String propertyName = pv.getName();
      Object originalValue = pv.getValue();
      //获取真正要装配的bean
      Object resolvedValue = valueResolver.resolveValueIfNecessary(pv, originalValue);
      Object convertedValue = resolvedValue;
      boolean convertible = bw.isWritableProperty(propertyName) &&
            !PropertyAccessorUtils.isNestedOrIndexedProperty(propertyName);
   }
}


从debug中也可以看出,我们bean的属性只有一个,也就是providers,符合我们在上面xml中配置的属性

image-20230322105338830

我们从真正创建要装配的bean的地方开始找找什么时候开始创建bean的

private Object resolveInnerBean(Object argName, String innerBeanName, BeanDefinition innerBd) {
   RootBeanDefinition mbd = null;
   try {
      ...
      // 真正创建bean的地方
      Object innerBean = this.beanFactory.createBean(actualInnerBeanName, mbd, null);
      if (innerBean instanceof FactoryBean) {
         boolean synthetic = mbd.isSynthetic();
         return this.beanFactory.getObjectFromFactoryBean(
               (FactoryBean<?>) innerBean, actualInnerBeanName, !synthetic);
      }
      else {
         return innerBean;
      }
   }
   catch (BeansException ex) {
      throw new BeanCreationException(
            this.beanDefinition.getResourceDescription(), this.beanName,
            "Cannot create inner bean '" + innerBeanName + "' " +
            (mbd != null && mbd.getBeanClassName() != null ? "of type [" + mbd.getBeanClassName() + "] " : "") +
            "while setting " + argName, ex);
   }
}


createBean(actualInnerBeanName, mbd, null)这行代码如果有小伙伴阅读过spring源码一定不陌生,通过这个方法可以获得要创建的bean对象。

image-20230322152949119

从debug中也可以看到真正要创建的beanName已经换成了我们的想要装配的属性ccProvider

至此我们已经发现了,和我们的预期一致,<bean>标签无论在什么位置确实会创建一个bean对象。

那么为什么这里的beanName不怕重复呢?

2.3 为什么这里的bean不会出现重复的问题

回顾刚刚之前提到的spring不允许重复名称的bean,其实很好理解,因为我们在创建bean的过程中,会将创建好的bean以beanName为key放到缓存的map中,如果我们有两个相同名称的bean,那么当存在重复的bean时,第二个bean会将第一个bean给覆盖掉。

这样的话,就不存在唯一性了,别的bean需要依赖重复的bean的时候有可能返回的并不是同一个bean。

那么为什么这里两个bean并不会重复呢?

其实细心的读者已经发现了,这里变量名称是innerBean,说明他是一个内部bean,那么innerBean与普通的bean有什么不同呢?为什么innerBean并不会产生 名称重复的问题呢?

我们重新梳理下创建普通bean的流程:

innerbean.drawio

其实答案已经很明显了:

如果我们创建的是一个普通bean,在创建完成之后会将bean放置到缓存中,如果有其他bean要使用直接从缓存中取走就可以了,而beanName不能重复也是基于此考虑。

而创建innerBean则基于createBean()原子性操作前提,只会返回创建好的bean,并不会将其加入到spring的bean缓存中,因此也就不存在beanName重复的问题了

三、总结

3.1 为什么spring可以存在”重复“名称的bean

我们这里重新梳理下bean的创建流程:

在spring注入一个普通bean的过程中,会将通过反射创建的空属性对象赋值,如果发现其依赖的属性也是一个bean,那么会首先去获取这个bean,如果获取不到的话则会转而去创建bean。

而此时要创建的bean成为innerBean,并不会被spring其他bean共享,所以可以在名称上是重复的。

3.2 innerBean的用法

还是我们刚刚的例子,我们可以将其改写成下面的这个样子:

<bean id="cacheClusterConfigProvider" class="com.wangyin.rediscluster.provider.CacheClusterConfigProvider">
    <property name="providers">
        <list>
            <!--# 引用ccProviderRef-->
            <ref bean="ccProviderRef"></ref>
        </list>
    </property>
</bean>

<!--# 定义了一个公共的ccProviderRef-->
<bean id="ccProviderRef" class="com.wangyin.rediscluster.provider.CCProvider">
    <!--# 替换为当前环境的R2M 3C配置中心地址(详见上方R2M 3C服务地址)-->
    <property name="address" value="${r2m.zkConnection}"/>
    <!--# 替换为R2M集群名-->
    <property name="appName" value="${r2m.appName}"/>
    <!--# 替换为当前环境的客户端对应配置中心token口令(参考上方token获取方式)-->
    <property name="token" value="${r2m.token}"/>
    <!--# 替换为集群认证密码-->
    <property name="password" value="${r2m.password}"/>
</bean>


在上面的例子中我们定义了一个普通bean,并将其引用到我们想要的属性中。

此时ccProviderRef作为一个普通bean,是可以被其他bean引用的,但是此时bean的名称就不可重复。

与Spring竟然可以创建“重复”名称的bean?—一次项目中存在多个bean名称重复问题的排查相似的内容:

Spring竟然可以创建“重复”名称的bean?—一次项目中存在多个bean名称重复问题的排查

众所周知,在Spring中时不能够创建两个名称相同的bean的,否则会在启动时报错:但是我却在我们的spring项目中发现了两个相同名称的bean,并且项目也可以正常启动,对应的bean也可以正常使用。

13年过去了,Spring官方竟然真的支持Bean的异步初始化了!

你好呀,我是歪歪。 两年前我曾经发布过这样的一篇文章《我是真没想到,这个面试题居然从11年前就开始讨论了,而官方今年才表态。》 文章主要就是由这个面试题引起: Spring 在启动期间会做类扫描,以单例模式放入 ioc。但是 spring 只是一个个类进行处理,如果为了加速,我们取消 spring

Spring的三种依赖注入的方式

1、什么是依赖注入 依赖注入(Dependency Injection,简称DI),是IOC的一种别称,用来减少对象间的依赖关系。 提起依赖注入,就少不了IOC。 IOC(Inversion of Control,控制反转)是一种设计思想,它将原本在程序中手动创建对象的控制权,交由Spring框架来

使用 Spring 实现控制反转和依赖注入

使用 Spring 实现控制反转和依赖注入 概述 在本文中,我们将介绍IoC(控制反转)和DI(依赖注入)的概念,以及如何在Spring框架中实现它们。 什么是控制反转? 控制反转是软件工程中的一个原则,它将对象或程序的某些部分的控制权转移给容器或框架。我们最常在面向对象编程的上下文中使用它。 与传

Spring 对于事务上的应用的详细说明

1. Spring 对于事务上的应用的详细说明 @目录1. Spring 对于事务上的应用的详细说明每博一文案2. 事务概述3. 引入事务场景3.1 第一步:准备数据库表3.2 第二步:创建包结构3.3 第三步:准备对应数据库映射的 Bean 类3.4 第四步:编写持久层3.5 第五步:编写业务层3

谈谈 Spring 的过滤器和拦截器

我们在进行 Web 应用开发时,时常需要对请求进行拦截或处理,故 Spring 为我们提供了过滤器和拦截器来应对这种情况。那么两者之间有什么不同呢?本文将详细讲解两者的区别和对应的使用场景。

有意思!一个关于 Spring 历史的在线小游戏

发现 Spring One 的官网上有个好玩的彩蛋,分享给大家! 进到Spring One的官网,可以看到右下角有个类似马里奥游戏中的金币图标。 点击该金币之后,会打开一个新的页面,进入下面这样一个名为:The History Of Spring 的在线小游戏 你可以使用上下左右的方向键来控制Spr

手把手教你解决spring boot导入swagger2版本冲突问题,刘老师教编程

手把手教你解决spring boot导入swagger2版本冲突问题 本文仅为个人理解,欢迎大家批评指错 首先Spring Boot 3 和 Swagger 2 不兼容。在 Spring Boot 3 中,应该使用 Springdoc 或其他与 Spring Boot 3 兼容的 API 文档工具来

Spring MVC 中使用 RESTFul 编程风格

1. Spring MVC 中使用 RESTFul 编程风格 @目录1. Spring MVC 中使用 RESTFul 编程风格2. RESTFul 编程风格2.1 RESTFul 是什么2.2 RESTFul风格与传统方式对比3. Spring MVC 中使用 RESTFul 编程风格(增删改查)

Spring Reactor基本介绍和案例

1. Reactor 对比 1.1 Reactor 线程模型 Reactor 线程模型就是通过 单个线程 使用 Java NIO 包中的 Selector 的 select()方法,进行监听。当获取到事件(如 accept、read 等)后,就会分配(dispatch)事件进行相应的事件处理(han