SpringBoot2.1.x后Feign出现Bean被重复注册,导致项目不能启动

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,451评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,172评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,782评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,709评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,733评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,578评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,320评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,241评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,686评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,878评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,992评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,715评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,336评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,912评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,040评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,173评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,947评论 2 355