The bean 'xx.FeignClientSpecification', defined in null, could not be registered

sprinboot升级启动时FeignClient报错

文章目录

  • sprinboot升级启动时FeignClient报错
    • 问题表现
    • 问题分析
    • 解决方案

问题表现

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中,这样方便管理和维护。

你可能感兴趣的:(spring,boot,feign,spring,cloud,spring,boot,spring,cloud,feign,client,java)