zoukankan      html  css  js  c++  java
  • 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中,这样方便管理和维护。

  • 相关阅读:
    mysql常用命令
    【转】Hibernate级联注解CascadeType参数详解
    【转】el表达式的判断符
    js中使用进行字符串传参
    【转】HTML5 jQuery图片上传前预览
    Win10家庭版安装Docker Desktop报错:Containers Windows Feature is not available
    采用反射机制,得出属性是否忽略
    Windows 我的常用命令
    数据相关需要注意,工作所遇场景
    引入 QueryDsl 开发步骤
  • 原文地址:https://www.cnblogs.com/fswhq/p/13691121.html
Copyright © 2011-2022 走看看