最近提升項目的SpringCloud版本後出錯誤導致項目無法啓動
關鍵詞
The bean 'xxx.FeignClientSpecification' could not be registered. A bean with that name has already been defined and overriding is disabled.
版本信息
升級前版本
SpringBoot | SpringCloud |
2.0.6.RELEASE | Finchley.SR2 |
升級後版本
SpringBoot | SpringCloud |
2.2.5.RELEASE | Hoxton.SR3 |
錯誤內容
2020-03-27 14:35:02.481 [ main:10483 ] - [ERROR ,,, ] o.s.boot.diagnostics.LoggingFailureAnalysisReporter#report:40 -
***************************
APPLICATION FAILED TO START
***************************Description:
The bean 'xx-xxx.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
錯誤原因:
說的很明顯BeanName重複了, 容器內出現了兩個xx-xxx.FeignClientSpecification的實現類
分析:
首先,我們項目內沒有名稱帶有FeignClientSpecification的類或接口,所以我認爲這應該是Fegin框架自己命名的,
名稱特點是"服務名-FeignClientSpecification"
網上解決方案:
方案一:
同一個服務的接口合併到同類中去, 就是將同一個service的接口整合到同一個FeignClient中, 方便統一
方案二:
開啓Bean覆蓋, 說法是2.0.x版本默認就是true, 新版2.2.x變爲了false
spring.main.allow-bean-definition-overriding=true
個人分析:
方案一, 根本就不方便, 嚴重破壞了單一原則的設計模式, 一個服務幾十甚至上百個不同業務的接口堆在一個類上就方便維護了?
方案二: 確實簡單粗暴, 反正我們注入是通過接口類型注入的, 名稱重複問題不大頂多就是有些警告而已, 但是我認爲新版本官方默認不允許BeanName重複肯定是有道理的, 所以我也不打算開啓.
所以我通過源碼分析找到了方案三: 設置@FeignClient中的contextId字段
源碼分析:
既然說是BeanName名稱重複, 那就必須先找到這個名稱怎麼來的, 通過關鍵字"could not be registered."搜索框架源碼找到提示位置
org.springframework.boot.diagnostics.analyzer.BeanDefinitionOverrideFailureAnalyzer#getDescription
private String getDescription(BeanDefinitionOverrideException ex) {
StringWriter description = new StringWriter();
PrintWriter printer = new PrintWriter(description);
printer.printf("The bean '%s'", ex.getBeanName());
if (ex.getBeanDefinition().getResourceDescription() != null) {
printer.printf(", defined in %s,", ex.getBeanDefinition().getResourceDescription());
}
printer.printf(" could not be registered. A bean with that name has already been defined ");
if (ex.getExistingDefinition().getResourceDescription() != null) {
printer.printf("in %s ", ex.getExistingDefinition().getResourceDescription());
}
printer.printf("and overriding is disabled.");
return description.toString();
}
看到提示重複name的值來源是ex.getBeanName()
在此處下一個條件斷點"xx-xxx.FeignClientSpecification".equals(ex.getBeanName())得到調用棧
可以看到調用棧並不深, 通過調用棧向上尋找觸發來源
發現這個錯誤是在SpringBoot的run方法執行過程中拋出來的的錯誤, 我們知道這個方法主要就是SpringBoot的生命週期執行方法
任何一個環節都有可能拋出異常, 由於我不知道這個Bean註冊的時機是在生命週期的哪一段就知道能一段段試了嗎?
我換了一個思路, 我找這個異常類的構造方法並在裏邊下斷點不就知道這個異常是誰new的了嗎
org.springframework.beans.factory.support.BeanDefinitionOverrideException
重新運行
果不其然發現了其實例化的調用棧, 向上找就發現了BeanName重複判斷,配置值開關是否打開判斷的相關代碼, 但是不夠, 繼續向上找, 發現了名稱的來源
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);
registry.registerBeanDefinition(
name + "." + FeignClientSpecification.class.getSimpleName(),
builder.getBeanDefinition());
}
看到名稱來源是 name+"."+FeignClientSpecification的簡單名稱, 可以看到後綴是寫死的沒有修改的可能, 我就從name能不能修改方面下手, 繼續向上尋找name的來源
發現了beanName是通過一個getClientName()方法來的, 進入getClientName()方法
org.springframework.cloud.openfeign.FeignClientsRegistrar#getClientName
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());
}
通過內存變量查看client變量的內容, 我通過值內容初步判斷斷裏邊的值就是我們@FeignClient註解裏邊的鍵和值
那我就知道了這段代碼是優先取contextId的值作爲name的, 如果取不到才取別的, 都取不到就報錯.
順序是contextId->value->name->serviceId
那我通過猜測去到@FeignClient註解看看是否有contextId字段
事實也證實了我的猜測, 確實有這個字段而且註釋也說的很清楚
意思: 這將用作Bean名稱,而不是名稱(如果存在),但不會用作服務ID。
解決方法:
@FeignClient中填寫contextId字段信息即可(PS:我們項目很多Fegin接口,我改了很久...但我沒想到別的方法)
@FeignClient(value = "xx-xxx", qualifier = "itemTemplateApiclient", contextId = "itemTemplateApiclient")
public interface TtemTemplateApiClient extends ItemTemplateApi {
@FeignClient中qualifier參數是我們之前SpringBoot2.0.x版本加的, 因爲之前SpringIOC容器裏邊用的是qualifier參數作爲某個name, 不指定的話沒關係,但是啓動的時候會有提示, 我喜歡乾淨的控制檯信息我才加的, 對於這次的問題是不影響的.
疑問,官方爲什麼不直接取我們接口的類名作爲name呢? 可能是擔心不同包的同名類,也可能是fallback也是繼承Fegin的時候就會出現同接口的兩個類
算了吧,問題解決了就好...