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;
    • FeignClientsRegistrarregisterBeanDefinitions方法内容如下:
      @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方法,发现将一个名为nameconfiguration注册到了IOC容器中,其中configuration是一个FeignClientSpecification类型的对象,来自于@FeignClientconfiguration属性,而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值,而几个@FeignClientvalue值一样,那么在注册FeignClientSpecification的时候必定会出现beanName重复;
  • 我想springboot 2.x将允许beanName重复的配置值从true改为false,应该是为了注册到IOC容器和使用IOC容器的bean更加安全和规范,避免同名bean被覆盖,也避免使用beanName注入时类型错误;
  • 那这个FeignClientSpecification有什么用呢?其实这个类是FeignClient的一些配置,比如重试、超时、日志策略,而FeignClient设计的思路是,同一个service,使用同一个configuration,方便管理,但有时候我们并不是把同一个service的所有接口都放在一个FeignClient里,而是分散开来;
  • 再回到registerDefaultConfiguration方法,这个方法注册了一个全局通用的配置,当某一个FeignClient的配置为null的时候,就是用这个default的配置。

解决方案

解决方案有二:

    1. 简单粗暴:spring.main.allow-bean-definition-overriding=true,但隐患有二:一是假设真有beanName相同但真实对象不同,而注入的时候使用了beanName注入,可能导致异常;二是假设需要配置configuration,只在某一个FeignClient配置了configuration,可能导致失效或不应该使用configuration的FeignClient也使用配置策略,因为允许重写就导致同一个名称的bean到底对应哪一个对象,严重依赖于注册顺序。
    2. 更多考虑:把同一个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中,这样方便管理和维护。

posted @ 2020-09-22 18:40  枫树湾河桥  阅读(1088)  评论(0编辑  收藏  举报
Live2D