The bean 'xx.FeignClientSpecification', defined in null, could not be registered
原文链接:https://blog.csdn.net/CL_YD/article/details/103408028
问题表现
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中,这样方便管理和维护。