聊聊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的处理机制,同时也加深我对它的掌握。顺带或许能够帮助小伙伴们解决某些的问题,这就是它的意义

相关文章
|
22天前
|
消息中间件 Java 调度
Spring Boot 3.3 后台任务处理的高效策略
【10月更文挑战第18天】 在现代应用程序中,后台任务处理对于提升用户体验和系统性能至关重要。Spring Boot 3.3提供了多种机制来实现后台任务处理,包括异步方法、任务调度和使用消息系统。本文将探讨这些机制的最佳实践,帮助开发者提高应用程序的效率和响应速度。
29 0
|
8天前
|
缓存 Java Spring
实战指南:四种调整 Spring Bean 初始化顺序的方案
本文探讨了如何调整 Spring Boot 中 Bean 的初始化顺序,以满足业务需求。文章通过四种方案进行了详细分析: 1. **方案一 (@Order)**:通过 `@Order` 注解设置 Bean 的初始化顺序,但发现 `@PostConstruct` 会影响顺序。 2. **方案二 (SmartInitializingSingleton)**:在所有单例 Bean 初始化后执行额外的初始化工作,但无法精确控制特定 Bean 的顺序。 3. **方案三 (@DependsOn)**:通过 `@DependsOn` 注解指定 Bean 之间的依赖关系,成功实现顺序控制,但耦合性较高。
实战指南:四种调整 Spring Bean 初始化顺序的方案
|
1月前
|
XML Java 数据格式
Spring从入门到入土(bean的一些子标签及注解的使用)
本文详细介绍了Spring框架中Bean的创建和使用,包括使用XML配置文件中的标签和注解来创建和管理Bean,以及如何通过构造器、Setter方法和属性注入来配置Bean。
66 9
Spring从入门到入土(bean的一些子标签及注解的使用)
|
22天前
|
存储 安全 Java
|
27天前
|
Java 测试技术 Windows
咦!Spring容器里为什么没有我需要的Bean?
【10月更文挑战第11天】项目经理给小菜分配了一个紧急需求,小菜迅速搭建了一个SpringBoot项目并完成了开发。然而,启动测试时发现接口404,原因是控制器包不在默认扫描路径下。通过配置`@ComponentScan`的`basePackages`字段,解决了问题。总结:`@SpringBootApplication`默认只扫描当前包下的组件,需要扫描其他包时需配置`@ComponentScan`。
|
1月前
|
Java 开发者 Spring
Spring bean的生命周期详解!
本文详细解析Spring Bean的生命周期及其核心概念,并深入源码分析。Spring Bean是Spring框架的核心,由容器管理其生命周期。从实例化到销毁,共经历十个阶段,包括属性赋值、接口回调、初始化及销毁等。通过剖析`BeanFactory`、`ApplicationContext`等关键接口与类,帮助你深入了解Spring Bean的管理机制。希望本文能助你更好地掌握Spring Bean生命周期。
69 1
|
1月前
|
Java Spring
获取spring工厂中bean对象的两种方式
获取spring工厂中bean对象的两种方式
37 1
|
22天前
|
消息中间件 监控 Java
Spring Boot 3.3 后台任务处理:最佳实践与高效策略
【10月更文挑战第10天】 在现代应用程序中,后台任务处理对于提高应用程序的响应性和吞吐量至关重要。Spring Boot 3.3提供了多种机制来实现高效的后台任务处理,包括异步方法、任务调度和使用消息队列等。本文将探讨这些机制的最佳实践和高效策略。
62 0
|
1月前
|
安全 算法 Java
强大!基于Spring Boot 3.3 六种策略识别上传文件类型
【10月更文挑战第1天】在Web开发中,文件上传是一个常见的功能需求。然而,如何确保上传的文件类型符合预期,防止恶意文件入侵,是开发者必须面对的挑战。本文将围绕“基于Spring Boot 3.3 六种策略识别上传文件类型”这一主题,分享一些工作学习中的技术干货,帮助大家提升文件上传的安全性和效率。
45 0
|
1月前
|
自然语言处理 JavaScript Java
Spring 实现 3 种异步流式接口,干掉接口超时烦恼
本文介绍了处理耗时接口的几种异步流式技术,包括 `ResponseBodyEmitter`、`SseEmitter` 和 `StreamingResponseBody`。这些工具可在执行耗时操作时不断向客户端响应处理结果,提升用户体验和系统性能。`ResponseBodyEmitter` 适用于动态生成内容场景,如文件上传进度;`SseEmitter` 用于实时消息推送,如状态更新;`StreamingResponseBody` 则适合大数据量传输,避免内存溢出。文中提供了具体示例和 GitHub 地址,帮助读者更好地理解和应用这些技术。
171 0