1. 问题起因
由于项目的服务拆分,旧项目的SpringBoot版本为2.0.4,拆分后的项目版本为2.2.6.RELEASE。但是在启动服务后出现了下面的异常:
***************************
APPLICATION FAILED TO START
***************************
Description:
The bean 'study.FeignClientSpecification' could not be registered. A bean with that name has already been defined and overriding is disabled.
Action:
Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true
原因:有多个接口类上存在@FeignClient(value = "study")
注解。
SpringBoot给出的解决方案是加入spring.main.allow-bean-definition-overriding=true
注解(相同名字的bean将会被覆盖)。
且必须是使用@Confiuration容器注册的bean才会覆盖,要是使用@Service方式将同名的bean注入到容器。
spring.main.allow-bean-definition-overriding=true
不会其作用,直接就会出现重名的异常。
疑问?
- 为什么旧项目没有在配置文件中没有使用该注解?
- 使用
spring.main.allow-bean-definition-overriding=true
配置有什么后果?
2. 问题解答
2.1 为什么旧项目没有使用该注解
旧项目为SpringBoot2.0.4,而新项目为SpringBoot2.2.6。
在SpringBoot2.2.6打开spring.main.allow-bean-definition-overriding=true
源码:
/**
* Sets if bean definition overriding, by registering a definition with the same name
* as an existing definition, should be allowed. Defaults to {@code false}.
* @param allowBeanDefinitionOverriding if overriding is allowed
* @since 2.1.0
* @see DefaultListableBeanFactory#setAllowBeanDefinitionOverriding(boolean)
*/
public void setAllowBeanDefinitionOverriding(boolean allowBeanDefinitionOverriding) {
this.allowBeanDefinitionOverriding = allowBeanDefinitionOverriding;
}
上面注解的含义是在SpringBoot的2.1.0版本后才新加的配置。若不配置SpringBoot默认值为false。
而该配置是在Spring的org.springframework.beans.factory.support.DefaultListableBeanFactory
类中使用。
Spring的allowBeanDefinitionOverriding
默认值为true。
springBoot2.0.4版本SpringBoot没有提供
spring.main.allow-bean-definition-overriding
的配置。即使用spring使用自己的默认值true(即允许启动重名类覆盖)。
而springBoot2.1.0版本后,若不配置spring.main.allow-bean-definition-overriding
配置,SpringBoot将会将默认值false
传到spring的DefaultListableBeanFactory
类。即不允许开启重名类的覆盖。
2.2 解决上述异常的方案
解决方案1:
配置类上增加:
spring:
main:
allow-bean-definition-overriding: true
这种方案有一种隐患,即项目很大,或者引入很多第三方jar时(出现重名的SpringBean)。项目在启动的时候不会出现重名的异常,而是会出现某个bean找不到的异常【@Autowried注入】(排查时发现bean会注册到Spring容器,Spring容器却找不到,会比较迷惑)
解决方案2:推荐
设置contextId。
例如:@FeignClient(value = "study",contextId = "qrStudyApi")**
感谢繁书_的方案:
解决方案3:
一个项目中只去设置一个@FeignClient(value = "study")
接口。【这就会导致Feign的API类过于冗余】。
2.3 使用spring.main.allow-bean-definition-overriding=true配置有什么后果?
若同一个项目中不同的Spring容器声明相同name的bean,就会出现覆盖现象。(当然不设置该配置,若存在上述情况会在项目启动的时候出现异常)。
模拟:
@Slf4j
public class BBService {
public void test(){
log.info("BBService.test()");
}
public void bbTest(){
log.info("BBService.bbTest()");
}
}
@Slf4j
public class AAService {
public void test(){
log.info("AAService.test()");
}
}
在不同的容器中注册Bean,bean的名字相同:
@Configuration
public class N1Config {
@Bean
public AAService aaService(){
return new AAService();
}
}
@Configuration
public class N2Config {
@Bean
public BBService aaService(){
return new BBService();
}
}
启动项目
@RestController
public class TestOver {
@Autowired
private BBService bbService;
@Autowired
private AAService aaService;
@GetMapping("/over/test")
public void test(){
bbService.test();
bbService.bbTest();
aaService.test();
}
}
出现异常:
***************************
APPLICATION FAILED TO START
***************************
Description:
Field aaService in com.tellme.controller.TestOver required a bean of type 'com.tellme.AAService' that could not be found.
The injection point has the following annotations:
- @org.springframework.beans.factory.annotation.Autowired(required=true)
Action:
Consider defining a bean of type 'com.tellme.AAService' in your configuration.
发现com.tellme.AAService
这个类的bean并没有被注册。原因就是同名被覆盖bean。
3. 源码分析
public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory
implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {
/** Whether to allow re-registration of a different definition with the same name. */
private boolean allowBeanDefinitionOverriding = true;
//---------------------------------------------------------------------
// Implementation of BeanDefinitionRegistry interface
//---------------------------------------------------------------------
//将Configuration容器的bean注册到Spring中
@Override
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
throws BeanDefinitionStoreException {
Assert.hasText(beanName, "Bean name must not be empty");
Assert.notNull(beanDefinition, "BeanDefinition must not be null");
if (beanDefinition instanceof AbstractBeanDefinition) {
try {
((AbstractBeanDefinition) beanDefinition).validate();
}
catch (BeanDefinitionValidationException ex) {
throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
"Validation of bean definition failed", ex);
}
}
//判断bean的name是否在容器中存在?
BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);
//若是存在
if (existingDefinition != null) {
//是否允许覆盖?【这就是出现异常的原因】
if (!isAllowBeanDefinitionOverriding()) {
//不允许覆盖的话,直接项目启动的时候抛出异常
throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingDefinition);
}
else if (existingDefinition.getRole() < beanDefinition.getRole()) {
// e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTURE
if (logger.isInfoEnabled()) {
logger.info("Overriding user-defined bean definition for bean '" + beanName +
"' with a framework-generated bean definition: replacing [" +
existingDefinition + "] with [" + beanDefinition + "]");
}
}
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 + "]");
}
}
//将beanDefinition再次放入到容器中(覆盖)
this.beanDefinitionMap.put(beanName, beanDefinition);
}
else {
//在容器中不存在,那么放入到容器中
...
}
if (existingDefinition != null || containsSingleton(beanName)) {
resetBeanDefinition(beanName);
}
}
}
注册带有FeignClient注解的类到Spring容器:
源码位置:org.springframework.cloud.openfeign.FeignClientsRegistrar#registerClientConfiguration
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
Object configuration) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientSpecification.class);
builder.addConstructorArgValue(name);
builder.addConstructorArgValue(configuration);
//带有FeignClient的类注册均是`name.FeignClientSpecification.class.getSimpleName()`的格式
registry.registerBeanDefinition(
name + "." + FeignClientSpecification.class.getSimpleName(),
builder.getBeanDefinition());
}
带有FeignClient注解接口在spring容器中注册的bean名字为name + "." +FeignClientSpecification.class.getSimpleName()
。
关键name的值如何获取
源码位置:org.springframework.cloud.openfeign.FeignClientsRegistrar#registerFeignClients
关键代码:
public void registerFeignClients(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
...
String name = getClientName(attributes);
registerClientConfiguration(registry, name,
attributes.get("configuration"));
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
}
获取client的名字:
private String getClientName(Map<String, Object> client) {
if (client == null) {
return null;
}
String value = (String) client.get("contextId");
if (!StringUtils.hasText(value)) {
value = (String) client.get("value");
}
if (!StringUtils.hasText(value)) {
value = (String) client.get("name");
}
if (!StringUtils.hasText(value)) {
value = (String) client.get("serviceId");
}
if (StringUtils.hasText(value)) {
return value;
}
throw new IllegalStateException("Either 'name' or 'value' must be provided in @"
+ FeignClient.class.getSimpleName());
}
若设置contextId
,那么那么将使用contextId
的值,而不使用value的值。可以避免@FeignClient相同value值导致重复bean的问题。
故:@FeignClient(value = "study",contextId = "qrStudyApi")也可以解决