Feign之重复出现的FeignClientSpecification

场景复现

依赖:

  1. Spring Boot 2.1.6.RELEASE
  2. Eureka Client 2.1.0.RELEASE
  3. OpenFeign 2.1.0.RELEASE

我们创建两个项目, ahao-server服务提供方和ahao-client服务调用方.
Eureka可以使用我弄的一个开箱即用Eureka

ahao-server创建一个显示当前时间的controller, 同时注册到eureka上.
假设端口为http://localhost:8080

1
2
3
4
5
6
7
8
9
@RestController
@RequestMapping("/ahao")
public class TimeController {
@RequestMapping("/date")
public String date() { return "现在日期是:" + new SimpleDateFormat("yyyy年MM月dd日").format(new Date()); }

@RequestMapping("/time")
public String time() { return "现在时间是:" + new SimpleDateFormat("hh时mm分ss秒").format(new Date()); }
}

ahao-client创建一个controller和两个feign客户端, 同时注册到eureka上.
假设端口为http://localhost:8081

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RestController
@RequestMapping("/ahao")
public class TimeController {
@Autowired private DateApi dateApi;
@Autowired private TimeApi timeApi;
@RequestMapping("/date")
public String date() { return dateApi.date(); }
@RequestMapping("/time")
public String time() { return timeApi.time(); }
}


@FeignClient(value = "AHAO-SERVER", path = "/ahao")
public interface DateApi {
@RequestMapping("/date") String date();
}
@FeignClient(value = "AHAO-SERVER", path = "/ahao")
public interface TimeApi {
@RequestMapping("/time") String time();
}

运行ahao-client报错

1
2
3
4
5
6
7
8
9
10
***************************
APPLICATION FAILED TO START
***************************
Description:
The bean 'AHAO-SERVER.FeignClientSpecification', defined in null, could not be registered. A bean with that name has already been defined in null and overriding is disabled.

Action:
Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true

Process finished with exit code 1

这个叫做AHAO-SERVER.FeignClientSpecificationBean是从哪来的???

问题所在

Spring打印出了异常堆栈. 我们跟进去看一下.

1
2
3
4
5
6
7
// org.springframework.cloud.openfeign.FeignClientsRegistrar
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, Object configuration) {
// 省略部分代码
registry.registerBeanDefinition(name + "." + FeignClientSpecification.class.getSimpleName(), builder.getBeanDefinition());
}
}

我们可以看到, 这里注册了一个Bean, 名字就是AHAO-SERVER.FeignClientSpecification.
这个name是从外部传进来的.

1
2
3
4
5
6
7
8
9
10
11
12
13
// org.springframework.cloud.openfeign.FeignClientsRegistrar
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
// 省略部分代码
Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(FeignClient.class.getCanonicalName());

String name = getClientName(attributes);
registerClientConfiguration(registry, name, attributes.get("configuration"));

registerFeignClient(registry, annotationMetadata, attributes);
// 省略部分代码
}
}

可以看到, name应该是从注解中的属性取值来的, 再看看getClientName()方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
// org.springframework.cloud.openfeign.FeignClientsRegistrar
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
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());
}
}

一目了然了, 我们声明@FeignClient注解时, 只使用了value属性, 所以产生了冲突, 只要加上contextId就好了.

解决方案

加上contextId属性即可.

1
2
3
4
5
6
7
8
@FeignClient(value = "AHAO-SERVER", path = "/ahao", contextId = "AHAO-SERVER-DATE")
public interface DateApi {
@RequestMapping("/date") String date();
}
@FeignClient(value = "AHAO-SERVER", path = "/ahao", contextId = "AHAO-SERVER-TIME")
public interface TimeApi {
@RequestMapping("/time") String time();
}

Spring Boot/Cloud 2.0.x 版本

我们切换到Spring Boot/Cloud 2.0.x版本, 发现没有contextId属性, 但是启动的时候可以正常启动, 不会报错Bean冲突.
看下Spring Boot/Cloud 2.0.x版本的源码.

1
2
3
4
5
6
7
8
9
10
11
12
// org.springframework.cloud.openfeign.FeignClientsRegistrar
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
private String getClientName(Map<String, Object> client) {
if (client == null) { return null; }

String 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());
}
}

看下getClientName()方法, 里面也没有使用contextId. 也就是会创建两个同名AHAO-SERVER.FeignClientSpecificationBean.
后来翻了下issue发现了答案.

spring boot 2.0.x
spring.main.allow-bean-definition-overriding default value is “true”
spring boot 2.1.x default value changed to “false”

原来是允许Bean重复定义所以才没有报错. 关键在spring.main.allow-bean-definition-overriding这个属性.

  • Spring Boot 2.0.x 默认是 true.
  • Spring Boot 2.1.x 默认是 false.

参考资料