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中,这样方便管理和维护。

浙公网安备 33010602011771号