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")也可以解決