乐趣区

关于spring:Spring竟然可以创建重复名称的bean一次项目中存在多个bean名称重复问题的排查

作者:京东科技 韩国凯

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

家喻户晓,在 Spring 中时不可能创立两个名称雷同的 bean 的,否则会在启动时报错:

然而我却在咱们的 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 名称却不会报错呢?

能够看到咱们创立的 bean 是失常的,并且从性能上来说也是能够应用的。

二、问题的排查过程

2.1 尝试间接找到创立反复 bean 地位

首先 debug 尝试找到创立反复 bean 时的相干信息,看看有没有什么思路

而后重启我的项目,抉择 debug 模式,然而在运行之后 IDEA 提醒断点被跳过了

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

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

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

将断点设置到创立 reids bean 处

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

那么咱们的思路就很简略了。

在 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 中配置的属性

咱们从真正创立要拆卸的 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 对象。

从 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 的流程:

其实答案曾经很显著了:

如果咱们创立的是一个一般 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 的名称就不可反复。

退出移动版