一、錯誤描述
最近使用 SpringBoot 2.2.11.RELEASE 配合 Feign 開發是出現如下錯誤:
org.springframework.beans.factory.support.BeanDefinitionOverrideException:
Invalid bean definition with name 'XXX.FeignClientSpecification' defined in null:
Cannot register bean definition [Generic bean: class [org.springframework.cloud.openfeign.FeignClientSpecification]; bound.
Description:
The bean 'XXX.FeignClientSpecification' could not be registered.
A bean with that name has already been defined and overriding is disabled.
二、問題原因
根據錯誤描述是 The bean 'XXX.FeignClientSpecification' could not be registered. A bean with that name has already been defined and overriding is disabled. 在項目中定義了兩個 Feign 用戶端 如下所示:
@FeignClient(value = "XXXX")
public interface ApiService1 {
@GetMapping("/user/{username}")
UserDto query(@PathVariable("username") String username);
}
@FeignClient(value = "XXX")
public interface MenuApiService {
@GetMapping("/allMenus")
List<MenuDto> queryAll();
}
因為 兩個 Feign 用戶端中配置的 value 值是一樣的 導緻在啟動時報如上錯誤。
三、解決方法
在 注解 @FeignClient 中添加 contextId 屬性,并且賦予不同的值。即可解決。
四、分析
我們看在 啟動類上添加的 @EnableFeignClients 注解 其中引入了 FeignClientsRegistrar 類如下所示:
那麼我們接下來在分析一下 FeignClientsRegistrar 類的源碼
1、繼承層次
我們看到此類實作了 ImportBeanDefinitionRegistrar 接口,此接口的作用就是動态注冊 Bean。接口資訊如下所示:
public interface ImportBeanDefinitionRegistrar {
default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,
BeanNameGenerator importBeanNameGenerator) {
registerBeanDefinitions(importingClassMetadata, registry);
}
default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
}
}
2、FeignClientsRegistrar 類 中的實作
在 FeignClientsRegistrar 類實作了 ImportBeanDefinitionRegistrar 接口的 registerBeanDefinitions 方法、如下所示:
接下來重點看下 registerFeignClients 方法的實作,此方法 邏輯很簡單,主要就是 @EnableFeignClients 注解中設定的 clients 屬性和 basePackages 屬性掃描項目中所有配置了 @FeignClient 注解的類或接口,方法解析具體可 FeignClient 用戶端。看下這個方法中的主要代碼(其他的可自行檢視源碼)如下所示:
主要代碼就是:
1、調用 getClientName(attributes) 方法擷取主要名稱,源碼如下所示:
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());
}
這個方法的代碼邏輯很簡單,現擷取 contenxtId,如果不存在在擷取 value, value 不存在 擷取name 依次擷取。
2、調用 registerClientConfiguration 方法注冊 Bean, 源碼如下所示:
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
Object configuration) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(FeignClientSpecification.class);
builder.addConstructorArgValue(name);
builder.addConstructorArgValue(configuration);
// 關鍵點:根據 getClientName 方法擷取的值 注冊 Bean 。
registry.registerBeanDefinition(
name + "." + FeignClientSpecification.class.getSimpleName(),
builder.getBeanDefinition());
}
在
getClientName()
源碼中,可以看到 name 就是 @FeignClient 注解中的 contenxtId,name,value, serviceId (已标注過期)。beanName:
name + "." + FeignClientSpecification.class.getSimpleName()
故而我們在使用相同名稱的FeigClient注解時,注入到Ioc的是相同Bean名。是以錯誤是由FeignClientSpecification類引起的。