聊聊Spring的bean覆盖(存在同名name/id问题),介绍Spring名称生成策略接口BeanNameGenerator【享学Spring】(下)

简介: 聊聊Spring的bean覆盖(存在同名name/id问题),介绍Spring名称生成策略接口BeanNameGenerator【享学Spring】(下)

覆盖规则分析


通过已经储备的知识我们知道,@Configuration配置文件会在容器启动的时候交给ConfigurationClassPostProcessor去解析,这里面最重要的就是Bean定义的注册顺序,总的规则上来说,一旦你允许Bean定义的覆盖,那么后者会覆盖前者的(后注册的覆盖先注册的)。


此处我对这个处理步骤截图如下:


image.png


为了更深刻的理解,详细内容强烈建议先参见:【小家Spring】Spring解析@Configuration注解的处理器:ConfigurationClassPostProcessor(ConfigurationClassParser)


我们知道注册Bean定义的方法是BeanDefinitionRegistry.registerBeanDefinition(),此接口方法的唯一真正实现就在DefaultListableBeanFactory它这里:


public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {
  @Override
  public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) throws BeanDefinitionStoreException {
    ...
    // 先拿出旧的(可能有  可能木有)  显然此处只讨论不为null的情况~
    BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);
    if (existingDefinition != null) {
      // allowBeanDefinitionOverriding属性值默认是true  后面会通过修改此属性值来达到fastfail的效果~
      if (!isAllowBeanDefinitionOverriding()) {
        throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingDefinition);
      }
      // =======到此处,说明Spring是允许同名的Bean覆盖的~========
      // =============下面的elseif  全部是记录日志===============
      // int ROLE_APPLICATION = 0;
      // int ROLE_SUPPORT = 1;
      // int ROLE_INFRASTRUCTURE = 2;
      // 简单一句话  新的Role比老的大  那就输入日志(因为0一般才是我们自定义的)  如果Spring内建的覆盖了我们的,那起步警告一下吗
      else if (existingDefinition.getRole() < beanDefinition.getRole()) {
        if (logger.isInfoEnabled()) {
          logger.info("Overriding user-defined bean definition for bean '" + beanName +
              "' with a framework-generated bean definition: replacing [" +
              existingDefinition + "] with [" + beanDefinition + "]");
        }
      }
      // 这里是debug信息:就是打印一下说“我被覆盖了”
      else if (!beanDefinition.equals(existingDefinition)) {
        if (logger.isDebugEnabled()) {
          logger.debug("Overriding bean definition for bean '" + beanName +
              "' with a different definition: replacing [" + existingDefinition +
              "] with [" + beanDefinition + "]");
        }
      }
      else {
        if (logger.isTraceEnabled()) {
          logger.trace("Overriding bean definition for bean '" + beanName +
              "' with an equivalent definition: replacing [" + existingDefinition +
              "] with [" + beanDefinition + "]");
        }
      }
      // ===============================
      // 最终执行覆盖~~~~~~
      this.beanDefinitionMap.put(beanName, beanDefinition);
    }
    ...
  }
}


这是Spring处理Bean定义覆盖的核心代码、核心描述,简单吧~

把这块逻辑结合@Configuration对Bean的解析、注册顺序一起理解:那么所有的Bean覆盖的case都可以得到解释了,这就是真正的授之以渔。


脑洞:BeanDefinition名称和SingletonBean同名了咋办?


其实这个问题属于脑洞问题,在真是应用场景中几乎不会发生。因为绝大多数情况、绝大多数小伙伴都不可能自己去手动注册一个SingletonBean的。


单纯从注册单例的方法:SingletonBeanRegistry.registerSingleton()它肯定是后者覆盖前者的。因此针对此种case,可以给出不十分完美的结论:


  • 因为实例化单例Bean是放在了容器初始化的最后第二步,而在这之后coder是不可能再手动registerSingleton()了。因此万一出现同名情况,最终也只会以你自己定义的BeanDefinition实例化的结果为准~


举例:

@Configuration
public class RootConfig {
}


测试:


    public static void main(String[] args) throws Exception {
        ApplicationContext context = new AnnotationConfigApplicationContext(RootConfig.class);
        System.out.println(context.getBean("systemProperties"));
    }
// 打印结果:{java.runtime.name=Java(TM) SE Runtime Environment, sun.boot.library.path=E:...


若我们自己定了一个同名的Bean:

@Configuration
public class RootConfig {
    @Bean("systemProperties")
    public Person person() {
        return new Person("RootConfig----Bean", 18);
    }
}


重新运行上面测试,结果如下:

Person{name='RootConfig----Bean', age=18}


可见我们自定义的Bean已经覆盖了Spring内建注册的beanFactory.registerSingleton(SYSTEM_PROPERTIES_BEAN_NAME, getEnvironment().getSystemProperties())


针对同名beanName,如何fail-fast?

不知道大家是否能意识到:beanName同名它是problem。虽然发生的可能性较小,但一旦发生,这问题还真不好找。


Spring容器既然有这个名称重复问题,我们该如何解决这个问题呢?


靠程序员自律?

制度上要求绝对不能定义重复名称的bean?

我觉得靠人去维护这件事,本身就是非常不靠谱的,因为项目依赖可能比较复杂、开发人员不尽相同等不可控因素太多~~

所以我认为只有通过在程序中引入一种报错机制(fail-fast)才能解决这个问题。


至于是否应该做fail-fast,这个需要具体情况具体而论的,毕竟个别时候通过Bean覆盖也是一种解决问题的技巧~(本人是赞同把fail-fast当做默认行为的)


接下来就是写实现方案的事了。上面也分析了,其实最终只需要想办法把DefaultListableBeanFactory类的allowBeanDefinitionOverriding属性值写为false即可。


Spring的默认方案是如果发生了覆盖,打印输出日志,而此处我们要抛出异常~


解决方案:


还记得我上篇文章介绍的ApplicationContextInitializer这个类吗?它能够在容器启动前,让我们能diy完成一些自定义操作。这么一看,这不正是我们这里想要的吗?


关于ApplicationContextInitializer的原理和使用,请参考:【小家Spring】详解Spring Framework提供的扩展点:ApplicationContextInitializer应用上下文初始化器,以及它在SpringBoot中的应用


1、书写一个ApplicationContextInitializer实现类:


public class MyApplicationContextInitializer implements ApplicationContextInitializer {
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        if (applicationContext instanceof AbstractRefreshableApplicationContext) {
            ((AbstractRefreshableApplicationContext) applicationContext).setAllowBeanDefinitionOverriding(false);
        } else if (applicationContext instanceof GenericApplicationContext) {
            ((GenericApplicationContext) applicationContext).setAllowBeanDefinitionOverriding(false);
        }
    }
}


2、注册到上下文


public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    @Override
    protected ApplicationContextInitializer<?>[] getServletApplicationContextInitializers() {
        return new ApplicationContextInitializer[]{new MyApplicationContextInitializer()};
    }
    @Override
    protected ApplicationContextInitializer<?>[] getRootApplicationContextInitializers() {
        return new ApplicationContextInitializer[]{new MyApplicationContextInitializer()};
    } 
}


说明:此处以注解驱动为示例。如果你是基于web.xml的传统应用,你需要在web.xml文件里增加配置如下:


<context-param>
        <param-name>contextInitializerClasses</param-name>
        <param-value>MyApplicationContextInitializer的全类名</param-value>
 </context-param>


关于contextInitializerClasses这个常量,在上面博文里有解释。其实如果你父子容器都要关闭覆盖,建议使用globalInitializerClassescontextInitializerClasses只解决根容器同名问题)。


这两个常量参见:

// @since 17.02.2003
public class ContextLoader {
  public static final String CONTEXT_INITIALIZER_CLASSES_PARAM = "contextInitializerClasses";
  public static final String GLOBAL_INITIALIZER_CLASSES_PARAM = "globalInitializerClasses";
}


这么依赖,若我们定义如下配置文件:


@Configuration
public class RootConfig {
    @Bean("personBean")
    public Person person() {
        return new Person("RootConfig----Bean", 18);
    }
    @Bean("personBean")
    public Person person2() {
        return new Person("RootConfig----Bean2", 18);
    }
}

启动测试,结果就是启动就抛出异常BeanDefinitionOverrideException:


org.springframework.beans.factory.support.BeanDefinitionOverrideException: Invalid bean definition with name 'personBean' defined in com.config.RootConfig: Cannot register bean definition [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=rootConfig; factoryMethodName=person; initMethodName=null; destroyMethodName=(inferred); defined in com.config.RootConfig] for bean 'personBean': There is already [Generic bean: class [com.fsx.dependency.B]; scope=singleton; abstract=false; lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null; defined in file [E:\dev_env\apache-tomcat-9.0.17\webapps\demo_war_war\WEB-INF\classes\com\fsx\dependency\B.class]] bound.
  at org.springframework.beans.factory.support.DefaultListableBeanFactory.registerBeanDefinition(DefaultListableBeanFactory.java:897)
  at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForBeanMethod(ConfigurationClassBeanDefinitionReader.java:274)
  at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForConfigurationClass(ConfigurationClassBeanDefinitionReader.java:141)

通过fail-fast快速失败能投提醒到程序员重名问题,这样就能够完全避免因为重名危险而导致可能运行期才有发现的错误~


总结


如果一个你曾某天碰到一个问题,最终定位到原因是BeanName重复导致的,那我敢猜测你为了找到这个问题,应该是花费了不少时间的。

因为跟我的经验,很多因为字符串造成的问题,大都不太好找,因为IDE、编译器等对于字符串的感知度都是较弱的(比如给文件、变量重命名的时候你就能感受到,字符串IDE一般都是不处理的~)。


书写本文,旨在帮助大家理解Spring对同名BeanName的处理机制,同时也加深我对它的掌握。顺带或许能够帮助小伙伴们解决某些的问题,这就是它的意义

相关文章
|
7月前
|
XML Java 测试技术
Spring IOC—基于注解配置和管理Bean 万字详解(通俗易懂)
Spring 第三节 IOC——基于注解配置和管理Bean 万字详解!
487 26
|
3月前
|
SQL Java 数据库
解决Java Spring Boot应用中MyBatis-Plus查询问题的策略。
保持技能更新是侦探的重要素质。定期回顾最佳实践和新技术。比如,定期查看MyBatis-Plus的更新和社区的最佳做法,这样才能不断提升查询效率和性能。
147 1
|
9月前
|
存储 Java Spring
【Spring】获取Bean对象需要哪些注解
@Conntroller,@Service,@Repository,@Component,@Configuration,关于Bean对象的五个常用注解
180 12
|
9月前
|
存储 Java 应用服务中间件
【Spring】IoC和DI,控制反转,Bean对象的获取方式
IoC,DI,控制反转容器,Bean的基本常识,类注解@Controller,获取Bean对象的常用三种方式
216 12
|
9月前
|
XML Java 数据格式
Spring容器Bean之XML配置方式
通过对以上内容的掌握,开发人员可以灵活地使用Spring的XML配置方式来管理应用程序的Bean,提高代码的模块化和可维护性。
242 6
|
9月前
|
XML Java 数据格式
🌱 深入Spring的心脏:Bean配置的艺术与实践 🌟
本文深入探讨了Spring框架中Bean配置的奥秘,从基本概念到XML配置文件的使用,再到静态工厂方式实例化Bean的详细步骤,通过实际代码示例帮助读者更好地理解和应用Spring的Bean配置。希望对你的Spring开发之旅有所助益。
416 4
|
存储 Java Spring
原来使用 Spring 实现策略模式可以这么简单!
Hello,大家好,我是鸭血粉丝~ 最近看同事的代码时候,学到了个小技巧,在某些场景下非常挺有用的,这里分享一下给大家。
原来使用 Spring 实现策略模式可以这么简单!
|
2月前
|
Java Spring 容器
SpringBoot自动配置的原理是什么?
Spring Boot自动配置核心在于@EnableAutoConfiguration注解,它通过@Import导入配置选择器,加载META-INF/spring.factories中定义的自动配置类。这些类根据@Conditional系列注解判断是否生效。但Spring Boot 3.0后已弃用spring.factories,改用新格式的.imports文件进行配置。
725 0
|
6月前
|
前端开发 Java 数据库
微服务——SpringBoot使用归纳——Spring Boot集成Thymeleaf模板引擎——Thymeleaf 介绍
本课介绍Spring Boot集成Thymeleaf模板引擎。Thymeleaf是一款现代服务器端Java模板引擎,支持Web和独立环境,可实现自然模板开发,便于团队协作。与传统JSP不同,Thymeleaf模板可以直接在浏览器中打开,方便前端人员查看静态原型。通过在HTML标签中添加扩展属性(如`th:text`),Thymeleaf能够在服务运行时动态替换内容,展示数据库中的数据,同时兼容静态页面展示,为开发带来灵活性和便利性。
296 0
|
2月前
|
缓存 JSON 前端开发
第07课:Spring Boot集成Thymeleaf模板引擎
第07课:Spring Boot集成Thymeleaf模板引擎
368 0
第07课:Spring Boot集成Thymeleaf模板引擎