前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >聊聊Spring的bean覆盖(存在同名name/id问题),介绍Spring名称生成策略接口BeanNameGenerator【享学Spring】

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

作者头像
YourBatman
发布2019-09-03 16:02:58
13.5K3
发布2019-09-03 16:02:58
举报
文章被收录于专栏:BAT的乌托邦BAT的乌托邦

前言

众所周知,Spring容器可以简单粗暴的把它理解为一个大大的Map,存储着容器所管理的所有的单实例对象。我们从使用getBean(String beanName)方法,根据bean名称就能获得容器内唯一的Bean实例就能“证明”到这一点。

可你是否曾想过:既然它是Map,那万一我们写的@BeanbeanName重名了怎么办呢?Spring框架是怎么来处理这个事的呢?

Spring容器通俗描述

我们把它理解成一个Map,那Map里面的key-value你应该知道:

  • key:beanName
  • value:单例bean对象

其实为了辅助理解,从SingletonBeanRegistry注册Bean的方法中也可以看出:

代码语言:javascript
复制
public interface SingletonBeanRegistry {
	...
	// ===注意它没有remove方法====
	void registerSingleton(String beanName, Object singletonObject);
	Object getSingleton(String beanName);
	boolean containsSingleton(String beanName);
	int getSingletonCount();
	...
}

同样的bean定义信息的注册器BeanDefinitionRegistry也能类比的看到:

代码语言:javascript
复制
public interface BeanDefinitionRegistry extends AliasRegistry {
	...
	void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) throws BeanDefinitionStoreException;
	// 请注意:SingletonBeanRegistry 可没有移除方法~
	void removeBeanDefinition(String beanName) throws NoSuchBeanDefinitionException;
	BeanDefinition getBeanDefinition(String beanName) throws NoSuchBeanDefinitionException;
	boolean containsBeanDefinition(String beanName);
	int getBeanDefinitionCount();
	...
}

在我们现在的Spring应用中,动不动容器就需要管理上千个Bean(更有甚者在过去的one in all应用中可能出现上万个Bean的情况)。

既然Spring容器是个Map,那key的重要性不言而喻,他指向着全局唯一的Bean实例,若key被覆盖了,就相当于Map的key被覆盖一样,旧的value值可能将永远就触达不到了~

从而可见,确保beanName的唯一性意义重大。但是呢管理的Bean多了,怎么去确保这件事肯定就成了一个难题,那么接下来就了解一下Spring它是怎么造的~

beanName的生成规则

我把beanName的生成规则放在最开始描述,是因为我觉得既然涉及到beanName,那么首先就应该知道beanName是怎么来的。

我们知道,Spring提供了非常非常多的方式允许我们向容器内注册一个Bean,下面总结出常用的注册Bean方式对应的BeanName如下:

  1. xml的<bean/>标签方式,由id属性决定(若没指定则为全类名)
  2. @Component模式注解方式(包含其所有派生注解如@Service@Configuration等等)。指定了value值就是value值,否则是类名首字母小写
  3. 此种方式是最为常用,也是大批量注册Bean的首选方式
  4. @Bean方式。指定了value值就是它,否则就是方法名
  5. FactoryBean方式。(它其实需要结合上面任意一种方式使用,beanName请参考如上描述)

这是一个基本的结论。其实大多数时候我们自己都并不会去指定beanName,若没有自己没有指定的话,它怎么来的呢?Spring对它的生成有什么规律可循呢?那么接下来就就研究下这个策略:名称生成策略

BeanNameGenerator

为bean定义生成bean名称的策略接口

BeanNameGenerator接口位于 org.springframework.beans.factory.support 包下面,只声明了一个方法,接受两个参数:definition 被生成名字的BeanDefinition实例;registry 生成名字后注册进的BeanDefinitionRegistry

代码语言:javascript
复制
// @since 2.0.3  是2.0后出来的
public interface BeanNameGenerator {
	// 入参竟然有两个  definition和bean定义注册器
	String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry);
}

看看它的继承树:

BeanNameGenerator有两个实现版本,DefaultBeanNameGeneratorAnnotationBeanNameGenerator。其中DefaultBeanNameGenerator是给资源文件加载bean时使用(BeanDefinitionReader中使用);AnnotationBeanNameGenerator是为了处理注解生成bean name的情况。

DefaultBeanNameGenerator

它是用来处理xml资源文件的Bean name生成器

代码语言:javascript
复制
// @since 2.0.3
public class DefaultBeanNameGenerator implements BeanNameGenerator {

	@Override
	public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
		// isInnerBean 如果是内部类表示true,这个工具类也能处理
		return BeanDefinitionReaderUtils.generateBeanName(definition, registry);
	}

}

将具体的处理方式委托给了BeanDefinitionReaderUtils.generateBeanName这个方法来处理:

代码语言:javascript
复制
public abstract class BeanDefinitionReaderUtils {

	// unique, "#1", "#2" etc will be appended, until the name becomes 
	public static final String GENERATED_BEAN_NAME_SEPARATOR = BeanFactoryUtils.GENERATED_BEAN_NAME_SEPARATOR;

	// isInnerBean:是为了区分内部bean(innerBean)和顶级bean(top-level bean).
	public static String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry, boolean isInnerBean) throws BeanDefinitionStoreException {

		// 拿到Bean定义信息里面的BeanClassName全类名
		// 注意这个不是必须的,因为如果是继承关系,配上父类的依旧行了
		String generatedBeanName = definition.getBeanClassName();
		if (generatedBeanName == null) {
				
			// 若没有配置本类全类名,去拿到父类的全类名+$child"俩表示自己
			if (definition.getParentName() != null) {
				generatedBeanName = definition.getParentName() + "$child";
			}
			// 工厂Bean的  就用方法的名字+"$created"
			else if (definition.getFactoryBeanName() != null) {
				generatedBeanName = definition.getFactoryBeanName() + "$created";
			}
		}
		// 若一个都没找到,抛错~
		if (!StringUtils.hasText(generatedBeanName)) {
			throw new BeanDefinitionStoreException("Unnamed bean definition specifies neither " + "'class' nor 'parent' nor 'factory-bean' - can't generate bean name");
		}

		//isInnerBean=true表示你是内部类的话,名字又增加了如下变化
		String id = generatedBeanName;
		if (isInnerBean) {
			// Inner bean: generate identity hashcode suffix.
			id = generatedBeanName + GENERATED_BEAN_NAME_SEPARATOR + ObjectUtils.getIdentityHexString(definition);
		}
	
		// 如果不是内部类(绝大多数情况下都如此)
		// 此方法注意:一定能够保证到你的BeanName是唯一的~~~~
		else {
			// Top-level bean: use plain class name with unique suffix if necessary.
			// Top-level表示最外层的Bean,也就是说非内部类  这里生成绝对唯一的BeanName~~~~
			return uniqueBeanName(generatedBeanName, registry);
		}
		return id;
	}
	public static String uniqueBeanName(String beanName, BeanDefinitionRegistry registry) {
		String id = beanName;
		int counter = -1;

		// Increase counter until the id is unique.
		while (counter == -1 || registry.containsBeanDefinition(id)) {
			counter++;
			id = beanName + GENERATED_BEAN_NAME_SEPARATOR + counter;
		}
		return id;
	}

}

对于它的处理逻辑,可以总结为如下步骤:

  1. 读取待生成Bean实例的类名称(未必是运行时的实际类型)。
  2. 如果类型为空,则判断是否存在parent bean,如果存在,读取parent bean的name + “$child”。
  3. 如果parent bean 为空,那么判断是否存在factory bean ,如存在,factory bean name + “$created”。 到此处前缀生成完毕
  4. 如果前缀为空,直接抛出异常,没有可以定义这个bean的任何依据。
  5. 前缀存在,判断是否为内部bean(innerBean,此处默认为false),如果是,最终为前缀+分隔符+十六进制的hashcode码
  6. 如果是顶级bean(top-level bean ),则判断前缀+数字的bean是否已存在,循环查询,知道查询到没有使用的id为止。处理完成(所以这个生成器肯定能保证Bean定义的唯一性,不会出现Bean name覆盖问题

需要注意的是,DefaultBeanNameGeneratorSpring中已经几乎处于一个被弃用了的状态,唯一使用地方为:

代码语言:javascript
复制
// @since 11.12.2003
public abstract class AbstractBeanDefinitionReader implements BeanDefinitionReader, EnvironmentCapable {

	private BeanNameGenerator beanNameGenerator = new DefaultBeanNameGenerator();
	public void setBeanNameGenerator(@Nullable BeanNameGenerator beanNameGenerator) {
		this.beanNameGenerator = (beanNameGenerator != null ? beanNameGenerator : new DefaultBeanNameGenerator());
	}	
}

而看看这个类的实现类们:

显然我们现在几乎不会再使用XmlBeanDefinitionReader,所以粗暴的可以理解为:此名称生成器已经废弃~

后来想了想其实这句话这么说不妥,毕竟我们使用@ImportResource的时候还是会导入xml文件进来的,因此各位自己感受吧


AnnotationBeanNameGenerator

javadoc的描述:它能够处理@Component以及它所有的派生注解,并且还支持JavaEE的javax.annotation.@ManagedBean、以及JSR 330的javax.inject.@Named注解。如果注解不指定bean名称,则将基于类的短名称(小写的第一个字母)生成适当的名称。

代码语言:javascript
复制
//  @since 2.5 它的出现是伴随着@Component出现
public class AnnotationBeanNameGenerator implements BeanNameGenerator {

	// 支持的最基本的注解(包含其派生注解)
	private static final String COMPONENT_ANNOTATION_CLASSNAME = "org.springframework.stereotype.Component";

	@Override
	public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
		
		// //判断是否是否是AnnotatedBeanDefinition的子类, AnnotatedBeanDefinition是BeanDefinition的一个子类
		// 显然这个生成器只为AnnotatedBeanDefinition它来自动生成名称
		if (definition instanceof AnnotatedBeanDefinition) {
	
			// determineBeanNameFromAnnotation这个方法简而言之,就是看你的注解有没有标注value值,若指定了就以指定的为准
			// 支持的所有注解:上面已经说明了~~~
			// 此处若配置了多个注解且都指定了value值,但发现value值有不同的,就抛出异常了~~~~~
			String beanName = determineBeanNameFromAnnotation((AnnotatedBeanDefinition) definition);
			if (StringUtils.hasText(beanName)) {
				// Explicit bean name found.
				return beanName;
			}
		}
		// Fallback: generate a unique default bean name.
		// 若没指定,此处叫交给生成器来生成吧~~~
		return buildDefaultBeanName(definition, registry);
	}

	// 它的方法是protected 由此可见若我们想自定义生成器的话  可以继承它  然后复写
	protected String buildDefaultBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
		return buildDefaultBeanName(definition);
	}
	// 这里是先拿到ClassUtils.getShortName 短名称
	protected String buildDefaultBeanName(BeanDefinition definition) {
		String beanClassName = definition.getBeanClassName();
		Assert.state(beanClassName != null, "No bean class name set");
		String shortClassName = ClassUtils.getShortName(beanClassName);
	
		// 调用java.beans.Introspector的方法  首字母小写
		return Introspector.decapitalize(shortClassName);
	}

}

对于它的处理逻辑,可以总结为如下步骤:

  1. 读取所有注解类型
  2. 遍历所有注解类型,找到所有为Component等所有支持的含有非空value属性的注解
  3. fallback到自己生成beanName

在注解大行其道的今天,AnnotationBeanNameGenerator的使用场景就非常非常之多了,整理如下:

AnnotatedBeanDefinitionReader
代码语言:javascript
复制
public class AnnotatedBeanDefinitionReader {
	...
	private BeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator();
	...
}

bean扫描器ClassPathBeanDefinitionScanner中会使用到AnnotatedBeanDefinitionReader去读取Bean定义信息们~

ClassPathBeanDefinitionScanner
代码语言:javascript
复制
public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateComponentProvider {
	private BeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator();
}
ConfigurationClassPostProcessor
代码语言:javascript
复制
public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor, PriorityOrdered, ResourceLoaderAware, BeanClassLoaderAware, EnvironmentAware {
	private BeanNameGenerator componentScanBeanNameGenerator = new AnnotationBeanNameGenerator();
}

同名name覆盖的case演示

由于Spring给我们提供了非常多的方式来定义Bean,所以势必会出现同名Bean的情况,下面举两个例子来感受一把:

case1:同一个配置文件内出现同名Bean
代码语言:javascript
复制
@Configuration
public class RootConfig {

    @Bean("personBean")
    public Person person() {
        return new Person("personBean", 18);
    }
    @Bean("personBean")
    public Person person2() {
        return new Person("personBean----2222", 18);
    }
}

使用@Bean若不指定value值,默认是方法名。但因为同一个类内方法名不能一样(不考虑重载情况),所以此处用手工指定同一个value值模拟

运行测试:

代码语言:javascript
复制
    public static void main(String[] args) throws Exception {
        ApplicationContext context = new AnnotationConfigApplicationContext(RootConfig.class);

        Object personBean = context.getBean("personBean");
        System.out.println(personBean);

    }

// 结果打印:Person{name='personBean', age=18}

调换Person定义的上下的位置(调换两个Bean上下的位置)

代码语言:javascript
复制
@Configuration
public class RootConfig {
	
    @Bean("personBean")
    public Person person2() {
        return new Person("personBean----2222", 18);
    }
	
	// 注意:此处我使用的类型是Child,但是beanName没变
    @Bean("personBean")
    public Child person() {
        return new Child();
    }

}
// 结果打印:Person{name='personBean----2222', age=18}

得出结论:同一个配置文件内同名的Bean以最上面定义的为准

case2:不同配置文件内出现同名Bean
代码语言:javascript
复制
@Configuration
public class RootConfig {

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

@Configuration
public class TempConfig {

    @Bean("personBean")
    public Person person() {
        return new Person("TempConfig----Bean", 18);
    }
}

测试:

代码语言:javascript
复制
    public static void main(String[] args) throws Exception {
    	// 注意此处传值的顺序是先rootConfig  在tempConfig
        ApplicationContext context = new AnnotationConfigApplicationContext(RootConfig.class, TempConfig.class);

        Object personBean = context.getBean("personBean");
        System.out.println(personBean);

    }

// 结果打印:Person{name='TempConfig----Bean', age=18}

交换顺序:先TempConfig.class,再RootConfig.class(代码略),再运行测试:

代码语言:javascript
复制
// 结果打印:Person{name='RootConfig----Bean', age=18}

得出结论:不同配置文件中存在同名Bean,后解析的配置文件会覆盖先解析的配置文件。

此处需要注意的是:配置文件的先后顺序其实会受到@Order来控制,只是若没有@Order注解的话就按照传入的顺序执行解析。 比如上例中传入顺序是TempConfig,再RootConfig,倘若在RootConfig头上加注解@Order(1)那么它就会被先解析,所以这时候最终结果是TempConfig配置类上的Bean生效,输出:Person{name='TempConfig----Bean', age=18} 关于@Configuration配置文件的解析,请参考:【小家Spring】Spring解析@Configuration注解的处理器:ConfigurationClassPostProcessor(ConfigurationClassParser) 里面有对本文非常理解非常中重要的Bean定义的解析、注册顺序说明

case3:同文件中ComponentScan@Bean出现同名Bean
代码语言:javascript
复制
@ComponentScan( ... )
@Configuration
public class RootConfig {

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

@Service("personBean")
public class B implements BInterface { ... }

测试:

代码语言:javascript
复制
    public static void main(String[] args) throws Exception {
        ApplicationContext context = new AnnotationConfigApplicationContext(RootConfig.class);

        Object personBean = context.getBean("personBean");
        System.out.println(personBean);
    }

// 打印结果:Person{name='RootConfig----Bean', age=18}

得出结论:同文件下@Bean的会生效,@ComponentScan扫描进来不会生效

最后你会发现:通过@ComponentScan扫描进来的优先级是最低的,原因就是它扫描进来的Bean定义是最先被注册的~


上面case都是描绘的在同一个Spring容器的情况下出现同名的情况,那么若在不同的容器内出现同名**Bean**呢?

看如下例子:

代码语言:javascript
复制
@Configuration
public class RootConfig {
    @Bean
    public Person person() {
        Person person = new Person();
        person.setName("parent application");
        return person;
    }
}

@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {
    @Bean
    public Person person() {
        Person person = new Person();
        person.setName("child application");
        return person;
    }
}

测试示例:

代码语言:javascript
复制
@Service
public class HelloServiceImpl implements HelloService {

    @Autowired
    private Person person;
    @Override
    public Person getPerson() {
        return person;
    }
}

@RestController
@RequestMapping
public class HelloController {
    @Autowired
    private ApplicationContext applicationContext;
    @Autowired
    private HelloService helloService;
    @Autowired
    private Person person;
    
    @GetMapping("/getPerson")
    public Object getPerson() {
        System.out.println(person);
        System.out.println(helloService.getPerson());

        //从父子容器中获取
        System.out.println(applicationContext.getBean(Person.class));
        System.out.println(applicationContext.getParent().getBean(Person.class));
        return "hello world";
    }
}

请求打印如下:

代码语言:javascript
复制
Person(name=child application, age=null, child=null)
Person(name=parent application, age=null, child=null)
Person(name=child application, age=null, child=null)
Person(name=parent application, age=null, child=null)

可见如果在不同容器内,即使Bean名称相同,它们也是能够和谐共存的。(@Autowired的时候请注意父子容器问题~)

原因简单解释:每个DefaultListableBeanFactory都持有自己的beanDefinitionNames,然后每个容器在初始化refresh()的时候就是按照这个一个一个进行实例化的,所以不同容器之间即使出现同名的BeanName,也不会互相干扰。它的getBean()基本逻辑是:如果本容器自己有,就不会去父容器里寻觅~

此书提示一点,在有些组件比如BeanPostProcessor需要提前初始化的时候,会调用此方法:

代码语言:javascript
复制
String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanPostProcessor.class, true, false);

获取到容器内所有的BeanPostProcessor名称后,挨个getBean()。此处需要注意一点:所有的getBeanNamesForType()方法默认都是不会去父容器里查找的,并且大多数情况下只会处理top-level的类。因此:你若在父容器里定义了一个BeanPostProcessor处理器,它对子容器是不生效的哦~~~

注意:SpringBoot下因为主容器没有主次之分,粗暴可以理解成它只存在一个容器,因此一般都不会存在此类问题


其它case这里就不能再一一例举了,因为组合下来情况太多了,没完没了的。下面会对大家授之以渔,从底层实现上告诉大家,一通则百通~

覆盖规则分析

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

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

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

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

代码语言:javascript
复制
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实例化的结果为准~

举例:

代码语言:javascript
复制
@Configuration
public class RootConfig {
}

测试:

代码语言:javascript
复制
    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:

代码语言:javascript
复制
@Configuration
public class RootConfig {

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

}

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

代码语言:javascript
复制
Person{name='RootConfig----Bean', age=18}

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


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

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

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

  1. 靠程序员自律?
  2. 制度上要求绝对不能定义重复名称的bean?

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

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

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

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

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

解决方案:

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

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

1、书写一个ApplicationContextInitializer实现类:

代码语言:javascript
复制
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、注册到上下文

代码语言:javascript
复制
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文件里增加配置如下:

代码语言:javascript
复制
<context-param>
        <param-name>contextInitializerClasses</param-name>
        <param-value>MyApplicationContextInitializer的全类名</param-value>
 </context-param>

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

这两个常量参见:

代码语言:javascript
复制
// @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";
}

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

代码语言:javascript
复制
@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

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

The last:如果觉得本文对你有帮助,不妨点个赞呗。当然分享到你的朋友圈让更多小伙伴看到也是被作者本人许可的~

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019年06月27日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • Spring容器通俗描述
  • beanName的生成规则
    • BeanNameGenerator
      • DefaultBeanNameGenerator
        • AnnotationBeanNameGenerator
        • 同名name覆盖的case演示
          • case1:同一个配置文件内出现同名Bean
            • case2:不同配置文件内出现同名Bean
              • case3:同文件中ComponentScan和@Bean出现同名Bean
              • 覆盖规则分析
                • 脑洞:BeanDefinition名称和SingletonBean同名了咋办?
                • 针对同名beanName,如何fail-fast?
                  • 总结
                  相关产品与服务
                  容器服务
                  腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档