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

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