SpringBoot2.1.x后Feign出现Bean被重复注册,导致项目不能启动 1. 问题起因 2. 问题解答 3. 源码分析

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类中使用。

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")也可以解决

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章