最近提升项目的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 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的时候就会出现同接口的两个类
算了吧,问题解决了就好...