覆盖规则分析
通过已经储备的知识我们知道,@Configuration配置文件会在容器启动的时候交给ConfigurationClassPostProcessor去解析,这里面最重要的就是Bean定义的注册顺序,总的规则上来说,一旦你允许Bean定义的覆盖,那么后者会覆盖前者的(后注册的覆盖先注册的)。
此处我对这个处理步骤截图如下:
为了更深刻的理解,详细内容强烈建议先参见:【小家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
这个常量,在上面博文里有解释。其实如果你父子容器
都要关闭覆盖,建议使用globalInitializerClasses
(contextInitializerClasses
只解决根容器同名问题)。
这两个常量参见:
// @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的处理机制,同时也加深我对它的掌握。顺带或许能够帮助小伙伴们解决某些的问题,这就是它的意义