sprinboot升級啓動時FeignClient報錯
問題表現
springboot從1.x升級到2.x後,解決了好多好多問題,什麼maven依賴、import package變化、包衝突、編譯不通過、application.properties配置變更等一系列問題後,終於來到了啓動環節,啓動後控制檯提示ApplicationContext啓動失敗,裏面有一句The bean 'xx.FeignClientSpecification', defined in null, could not be registered. A bean with that name has already been defined in null and overriding is disabled.
問題分析
很明顯是兩個Bean註冊到Spring容器中的名稱相同,但是有沒有開啓spring.main.allow-bean-definition-overriding=true
。
爲什麼1.x中可以正常啓動,2.x就不行呢?因爲1.x中spring.main.allow-bean-definition-overriding
默認是 true
,而2.x中默認是false
。
到這裏已經有一個很簡單的解決方案:在application.properties裏面添加一行spring.main.allow-bean-definition-overriding=true
,但是這並不是最完美的方案,爲什麼2.x要設置爲false
,爲什麼FeignClient的bean名稱會相同?如何去避免FeignClient在IOC容器中的名稱相同能?
首先簡單理以下FeignClient的註冊原理:
- 在啓動類上添加
@EnableFeignClients
註解,然後在Feign接口上添加@FeignClient
註解,該接口就會被註冊到IOC容器; @EnableFeignClients
註解上有一個@Import(FeignClientsRegistrar.class)
,這個FeignClientsRegistrar
類負責加載和註冊FeignClient;FeignClientsRegistrar
的registerBeanDefinitions
方法內容如下:@Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { registerDefaultConfiguration(metadata, registry); registerFeignClients(metadata, registry); }
- 暫時不看第一行的
registerDefaultConfiguration
方法,直接進registerFeignClients
方法查看; - 這個方法的核心是找出所有
@FeignClient
註解的接口,並依此註冊,但註冊時並不是僅僅註冊FeignClient本身:registerClientConfiguration(registry, name,attributes.get("configuration")); registerFeignClient(registry, annotationMetadata, attributes);
- 和
registerBeanDefinitions
類似,依然是先註冊一個configuration
,再註冊FeignClient; - 依然暫時不看
registerClientConfiguration
方法,直接進入registerFeignClient
方法,發現註冊FeignClient使用的是FeignClient對應接口的className作爲beanName的,因此不可能重複,這時候問題就回到了我們暫時不看的兩個方法; - 先進入
registerClientConfiguration
方法,發現將一個名爲name
的configuration
註冊到了IOC容器中,其中configuration
是一個FeignClientSpecification
類型的對象,來自於@FeignClient
的configuration
屬性,而name
的獲取方法如下: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()); }
- 可以看出:name來自於
@FeignClient
的一個屬性,到底取哪一個值,又一個優先級:contextId、value、name、serviceId,如果@FeignClient
註解只指定了value
值,而幾個@FeignClient
的value
值一樣,那麼在註冊FeignClientSpecification
的時候必定會出現beanName重複; - 我想springboot 2.x將允許beanName重複的配置值從true改爲false,應該是爲了註冊到IOC容器和使用IOC容器的bean更加安全和規範,避免同名bean被覆蓋,也避免使用beanName注入時類型錯誤;
- 那這個
FeignClientSpecification
有什麼用呢?其實這個類是FeignClient的一些配置,比如重試、超時、日誌策略,而FeignClient設計的思路是,同一個service,使用同一個configuration,方便管理,但有時候我們並不是把同一個service的所有接口都放在一個FeignClient裏,而是分散開來; - 再回到
registerDefaultConfiguration
方法,這個方法註冊了一個全局通用的配置,當某一個FeignClient的配置爲null的時候,就是用這個default的配置。
解決方案
解決方案有二:
- 簡單粗暴:
spring.main.allow-bean-definition-overriding=true
,但隱患有二:一是假設真有beanName相同但真實對象不同,而注入的時候使用了beanName注入,可能導致異常;二是假設需要配置configuration,只在某一個FeignClient配置了configuration,可能導致失效或不應該使用configuration的FeignClient也使用配置策略,因爲允許重寫就導致同一個名稱的bean到底對應哪一個對象,嚴重依賴於註冊順序。 - 更多考慮:把同一個service的所有接口整合到同一個FeignClient接口中,如果整合有困難,可以考慮指定contextId,因爲contextId的優先級最高,註冊到IOC容器的名稱也會因爲contextId的不同而不同。但也有一個隱患:指定contextId可能會導致每個FeignClient都需要指定同一個configuration纔可以讓同一個service的配置策略生效
/** * 1.5.21 */ @FeignClient(EurekaService.SID) @RequestMapping(EurekaService.CONTEXT) public interface SidFeignClient { } /** * 2.1.6 */ @FeignClient(value = EurekaService.SID, contextId = "sidFeignClient") @RequestMapping(EurekaService.CONTEXT) public interface SidFeignClient { }
綜上所訴,最好的辦法是將同一個service的接口整合到同一個FeignClient中,這樣方便管理和維護。