美文网首页
SpringBoot2.1.x后Feign出现Bean被重复注册

SpringBoot2.1.x后Feign出现Bean被重复注册

作者: 小胖学编程 | 来源:发表于2021-01-16 17:46 被阅读0次

    1. 问题起因

    由于项目的服务拆分,旧项目的SpringBoot版本为2.0.4,拆分后的项目版本为2.2.6.RELEASE。但是在启动服务后出现了下面的异常:

    ***************************
    APPLICATION FAILED TO START
    ***************************
    
    Description:
    
    The bean 'study.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
    

    原因:有多个接口类上存在@FeignClient(value = "study")注解。

    SpringBoot给出的解决方案是加入spring.main.allow-bean-definition-overriding=true注解(相同名字的bean将会被覆盖)。

    且必须是使用@Confiuration容器注册的bean才会覆盖,要是使用@Service方式将同名的bean注入到容器。spring.main.allow-bean-definition-overriding=true不会其作用,直接就会出现重名的异常。

    疑问?

    1. 为什么旧项目没有在配置文件中没有使用该注解?
    2. 使用spring.main.allow-bean-definition-overriding=true配置有什么后果?

    2. 问题解答

    2.1 为什么旧项目没有使用该注解

    旧项目为SpringBoot2.0.4,而新项目为SpringBoot2.2.6。

    在SpringBoot2.2.6打开spring.main.allow-bean-definition-overriding=true源码:

        /**
         * Sets if bean definition overriding, by registering a definition with the same name
         * as an existing definition, should be allowed. Defaults to {@code false}.
         * @param allowBeanDefinitionOverriding if overriding is allowed
         * @since 2.1.0
         * @see DefaultListableBeanFactory#setAllowBeanDefinitionOverriding(boolean)
         */
        public void setAllowBeanDefinitionOverriding(boolean allowBeanDefinitionOverriding) {
            this.allowBeanDefinitionOverriding = allowBeanDefinitionOverriding;
        }
    

    上面注解的含义是在SpringBoot的2.1.0版本后才新加的配置。若不配置SpringBoot默认值为false。

    而该配置是在Spring的org.springframework.beans.factory.support.DefaultListableBeanFactory类中使用。

    image.png

    Spring的allowBeanDefinitionOverriding默认值为true。

    springBoot2.0.4版本SpringBoot没有提供spring.main.allow-bean-definition-overriding的配置。即使用spring使用自己的默认值true(即允许启动重名类覆盖)。
    而springBoot2.1.0版本后,若不配置spring.main.allow-bean-definition-overriding配置,SpringBoot将会将默认值false传到spring的DefaultListableBeanFactory类。即不允许开启重名类的覆盖。

    2.2 解决上述异常的方案

    解决方案1:

    配置类上增加:

    spring:
      main:
        allow-bean-definition-overriding: true
    

    这种方案有一种隐患,即项目很大,或者引入很多第三方jar时(出现重名的SpringBean)。项目在启动的时候不会出现重名的异常,而是会出现某个bean找不到的异常【@Autowried注入】(排查时发现bean会注册到Spring容器,Spring容器却找不到,会比较迷惑)

    解决方案2:推荐

    设置contextId。

    例如:@FeignClient(value = "study",contextId = "qrStudyApi")**

    感谢繁书_的方案:

    解决方案3:

    一个项目中只去设置一个@FeignClient(value = "study")接口。【这就会导致Feign的API类过于冗余】。

    2.3 使用spring.main.allow-bean-definition-overriding=true配置有什么后果?

    若同一个项目中不同的Spring容器声明相同name的bean,就会出现覆盖现象。(当然不设置该配置,若存在上述情况会在项目启动的时候出现异常)。

    模拟:

    @Slf4j
    public class BBService {
    
        public void test(){
            log.info("BBService.test()");
        }
    
        public void bbTest(){
            log.info("BBService.bbTest()");
        }
    }
    
    @Slf4j
    public class AAService {
        public void test(){
            log.info("AAService.test()");
        }
    }
    

    在不同的容器中注册Bean,bean的名字相同:

    @Configuration
    public class N1Config {
        @Bean
        public AAService aaService(){
            return new AAService();
        }
    }
    
    @Configuration
    public class N2Config {
        @Bean
        public BBService aaService(){
            return new BBService();
        }
    }
    

    启动项目

    @RestController
    public class TestOver {
    
        @Autowired
        private BBService bbService;
    
        @Autowired
        private AAService aaService;
    
        @GetMapping("/over/test")
        public void test(){
            bbService.test();
            bbService.bbTest();
            aaService.test();
        }
    }
    

    出现异常:

    ***************************
    APPLICATION FAILED TO START
    ***************************
    
    Description:
    
    Field aaService in com.tellme.controller.TestOver required a bean of type 'com.tellme.AAService' that could not be found.
    
    The injection point has the following annotations:
        - @org.springframework.beans.factory.annotation.Autowired(required=true)
    
    
    Action:
    
    Consider defining a bean of type 'com.tellme.AAService' in your configuration.
    

    发现com.tellme.AAService这个类的bean并没有被注册。原因就是同名被覆盖bean。

    3. 源码分析

    public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory
            implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {
        /** Whether to allow re-registration of a different definition with the same name. */
        private boolean allowBeanDefinitionOverriding = true;
    
    
        //---------------------------------------------------------------------
        // Implementation of BeanDefinitionRegistry interface
        //---------------------------------------------------------------------
    
        //将Configuration容器的bean注册到Spring中
        @Override
        public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
                throws BeanDefinitionStoreException {
    
            Assert.hasText(beanName, "Bean name must not be empty");
            Assert.notNull(beanDefinition, "BeanDefinition must not be null");
    
            if (beanDefinition instanceof AbstractBeanDefinition) {
                try {
                    ((AbstractBeanDefinition) beanDefinition).validate();
                }
                catch (BeanDefinitionValidationException ex) {
                    throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
                            "Validation of bean definition failed", ex);
                }
            }
            //判断bean的name是否在容器中存在?
            BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);
            //若是存在
            if (existingDefinition != null) {
                //是否允许覆盖?【这就是出现异常的原因】
                if (!isAllowBeanDefinitionOverriding()) {
                   //不允许覆盖的话,直接项目启动的时候抛出异常
                    throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingDefinition);
                }
                else if (existingDefinition.getRole() < beanDefinition.getRole()) {
                    // e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTURE
                    if (logger.isInfoEnabled()) {
                        logger.info("Overriding user-defined bean definition for bean '" + beanName +
                                "' with a framework-generated bean definition: replacing [" +
                                existingDefinition + "] with [" + beanDefinition + "]");
                    }
                }
                else if (!beanDefinition.equals(existingDefinition)) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Overriding bean definition for bean '" + beanName +
                                "' with a different definition: replacing [" + existingDefinition +
                                "] with [" + beanDefinition + "]");
                    }
                }
                else {
                    if (logger.isTraceEnabled()) {
                        logger.trace("Overriding bean definition for bean '" + beanName +
                                "' with an equivalent definition: replacing [" + existingDefinition +
                                "] with [" + beanDefinition + "]");
                    }
                }
                //将beanDefinition再次放入到容器中(覆盖)
                this.beanDefinitionMap.put(beanName, beanDefinition);
            }
            else {
                //在容器中不存在,那么放入到容器中
                 ...
            }
            if (existingDefinition != null || containsSingleton(beanName)) {
                resetBeanDefinition(beanName);
            }
        }
    }
    

    注册带有FeignClient注解的类到Spring容器:

    源码位置:org.springframework.cloud.openfeign.FeignClientsRegistrar#registerClientConfiguration

        private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
                Object configuration) {
            BeanDefinitionBuilder builder = BeanDefinitionBuilder
                    .genericBeanDefinition(FeignClientSpecification.class);
            builder.addConstructorArgValue(name);
            builder.addConstructorArgValue(configuration);
            //带有FeignClient的类注册均是`name.FeignClientSpecification.class.getSimpleName()`的格式
            registry.registerBeanDefinition(
                    name + "." + FeignClientSpecification.class.getSimpleName(),
                    builder.getBeanDefinition());
        }
    

    带有FeignClient注解接口在spring容器中注册的bean名字为name + "." +FeignClientSpecification.class.getSimpleName()

    关键name的值如何获取

    源码位置:org.springframework.cloud.openfeign.FeignClientsRegistrar#registerFeignClients
    关键代码:

        public void registerFeignClients(AnnotationMetadata metadata,
                BeanDefinitionRegistry registry) {
         ...
                        String name = getClientName(attributes);
                        registerClientConfiguration(registry, name,
                                attributes.get("configuration"));
    
                        registerFeignClient(registry, annotationMetadata, attributes);
                    }
                }
            }
        }
    

    获取client的名字:

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

    若设置contextId,那么那么将使用contextId的值,而不使用value的值。可以避免@FeignClient相同value值导致重复bean的问题。

    故:@FeignClient(value = "study",contextId = "qrStudyApi")也可以解决

    相关文章

      网友评论

          本文标题:SpringBoot2.1.x后Feign出现Bean被重复注册

          本文链接:https://www.haomeiwen.com/subject/exgnaktx.html