在一个项目中,如果有多个FeignClient客户端指向同一个微服务,项目启动的过程中可能会抛出如下的异常The bean 'xxx.FeignClientSpecification' could not be registered. A bean with that name has already been defined and overriding is disabled.

具体异常如下:

***************************
APPLICATION FAILED TO START
***************************

Description:

The bean 'order-service.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
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

解决方法

方法一

按照异常的提示在配置文件中添加如下配置:

# application.yml文件
spring:
  main:
    allow-bean-definition-overriding: true
  • 1.
  • 2.
  • 3.
  • 4.

方法二

在@FeignClient注解里面添加contextId属性,并保证两个客户端中的contextId属性不一致:

@FeignClient(value = "order-service", path = "/order", contextId = "order1")
public interface OrderClient {
}

@FeignClient(value = "order-service", path = "/order", contextId = "order2")
public interface OrderClient {
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

从源码分析原因

@EnableFeignClients注解

在项目的启动类上有一个@EnableFeignClients注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

FeignClientsRegistrar类

FeignClientsRegistrar类会在项目启动过程中注入一些BeanDefinition:

public void registerBeanDefinitions(AnnotationMetadata metadata,
    BeanDefinitionRegistry registry) {
  registerDefaultConfiguration(metadata, registry);
  registerFeignClients(metadata, registry);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

registerFeignClients()

registerFeignClients()通过扫描带有@FeignClient注解的类,为每个带有@FeignClient注解的类向Spring容器中注入两个BeanDefinition:

  • FeignClientSpecification
  • FeignClientFactoryBean
public void registerFeignClients(AnnotationMetadata metadata,
    BeanDefinitionRegistry registry) {

  LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();
  Map<String, Object> attrs = metadata
      .getAnnotationAttributes(EnableFeignClients.class.getName());
  AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
      FeignClient.class);
  final Class<?>[] clients = attrs == null ? null
      : (Class<?>[]) attrs.get("clients");
  if (clients == null || clients.length == 0) {
    ClassPathScanningCandidateComponentProvider scanner = getScanner();
    scanner.setResourceLoader(this.resourceLoader);
    scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
    Set<String> basePackages = getBasePackages(metadata);
    for (String basePackage : basePackages) {
      candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
    }
  }
  else {
    for (Class<?> clazz : clients) {
      candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
    }
  }

  for (BeanDefinition candidateComponent : candidateComponents) {
    if (candidateComponent instanceof AnnotatedBeanDefinition) {
      // verify annotated class is an interface
      AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
      AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
      Assert.isTrue(annotationMetadata.isInterface(),
          "@FeignClient can only be specified on an interface");

      Map<String, Object> attributes = annotationMetadata
          .getAnnotationAttributes(FeignClient.class.getCanonicalName());

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

      registerFeignClient(registry, annotationMetadata, attributes);
    }
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.

getClientName()

FeignClientSpecification在Spring容器中beanName通过注解中的属性来决定:

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());
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.

FeignClientSpecification的beanName默认是使用为contextId.FeignClientSpecification,如果contextId属性不指定,就会使用value.FeignClientSpecification

通过源码分析,我们可以发现,当多个Feign客户端指向同一个服务时,如果不指定contextId,这些客户端在注册过程中可能会因为获取到相同的className而导致冲突,从而引发注册失败的问题。

因此,在配置多个指向相同服务的Feign客户端时,正确设置contextId是至关重要的,以确保每个客户端都能被Spring框架唯一地识别和注册。