Spring重名bean

在Spring框架中,当你尝试定义两个具有相同名称的Bean时,会遇到问题,因为Spring的容器需要确保每个Bean都有一个唯一的标识符(ID或名称)。如果你不小心尝试注册两个同名的Bean,Spring将抛出异常,告知你存在冲突。

一、Bean重名冲突,通常有2种场景

1、相同的类名,不同的包(也就是说只是bean的名字重复了,类本身是不一样的)
   这种情况,对应的异常是:ConflictingBeanDefinitionException
   
2、同一个类,多次被Bean申明(不仅bean的名字重复了,更重要的是同一个类)
   这种情况,对应的异常是:BeanDefinitionOverrideException

出现的异常:


BeanDefinitionOverrideException ConflictingBeanDefinitionException 区别
BeanDefinitionOverrideException和ConflictingBeanDefinitionException是Spring框架中与Bean定义冲突相关的异常。

BeanDefinitionOverrideException异常发生在Spring容器中存在相同的Bean名称,但是它们有不兼容的定义时。这通常发生在尝试注册两个具有相同名称但定义不同的Bean时。

ConflictingBeanDefinitionException异常是BeanDefinitionOverrideException的特例,它表示有一个Bean定义覆盖了另一个,并且这个覆盖是冲突的,比如一个是单例的,而另一个是原型的。

二、解决方案

方案一:更换类名
在两个类都没有手动设置Bean的名称的时候,最简单的办法就是修改其中一个冲突的类的名字,这样就能避免名称冲突。

方案二:手动设置Bean的名称
手动设置@Bean注解的名称来避免冲突。例如,将原来的@Bean注解改为@Bean("bean1"),这样就可以将Bean的名称修改为bean1,避免和自动配置的Bean名称冲突。

方案三:使用@Primary注解
@Primary注解可以用来指定当存在多个同类型的Bean时,哪个Bean应该被优先考虑。如果不想改变类名也不想手动设置注解中的名字的话,可以采用这种方案。

方案四:自定义@ComponentScan排除规则
@ComponentScan注解可以用来指定Spring在启动时扫描哪些包,并排除某些特定的类。通过自定义排除规则,我们可以阻止Spring创建不需要的Bean

方案五:自定义TypeExcludeFilter
如果@ComponentScan的排除规则与@SpringBootApplication的默认排除规则冲突,我们可以通过自定义TypeExcludeFilter来解决问题

方案六:自定义TypeExcludeFilter
最直接的办法当然是修改 Bean 的类名,不让他们重复。但是这样可能涉及的代码改动比较多。那么这时可以从 Bean 的注册上着手。
根据上面的源码,既然是因为 Bean 在往注册表中注册的过程中产生的同名冲突,已知注册表的数据结构是不能改变的,那么我们只能从 Bean 的命名进行着手。
所以我们可以继承和重写 AnnotationBeanNameGenerator 的 generateBeanName 方法,修改 Bean 命名逻辑,再修改项目的 Bean 名称生成器来避免 bean 名称冲突。
示例代码如下:
@Slf4j
@MapperScan("com.xxx.**.mapper")
@SpringBootApplication(scanBasePackages="com.xx.**", nameGenerator = IdpApplicationRunner.SpringBeanNameGenerator.class)
public class IdpApplicationRunner {

    public static class SpringBeanNameGenerator extends AnnotationBeanNameGenerator {
        @Override
        public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
            String shortClassName = super.generateBeanName(definition, registry);

            // 如果存在相同名称的bean,则将要注册的bean的名称设置为全路径名
            if (registry.containsBeanDefinition(shortClassName)) {
                return definition.getBeanClassName();
            }

            return shortClassName;
        }
    }

    public static void main(String[] args) {
        SpringApplication.run(IdpApplicationRunner.class, args);
    }

}

I. 多实例Bean的选择
这个场景可以说是比较常见的,现在提倡面向接口编程嘛,当一个接口有多个实例时,怎么注入和引用就需要我们额外关注下了

基本使用姿势
首先定义一个接口和两个简单的实现类,并演示一下我们通常的用法
一个输出的接口定义如下

public interface IPrint {
void print(String msg);
}
对应给两个实现

@Component
public class ConsolePrint implements IPrint {

@Override
public void print(String msg) {
    System.out.println("console print: " + msg);
}

}

@Slf4j
@Component
public class LogPrint implements IPrint {
@Override
public void print(String msg) {
log.info(“log print: {}”, msg);
}
}
下面就是我们一般的引用方式

@Autowired注解时,属性名即为默认的Bean名,如下面的logPrint就是获取beanName=logPrint的bean
@Resource(name=xxx) 直接指定Bean的name,来唯一选择匹配的bean

@Component
public class NormalPrintDemo {
@Resource(name = “consolePrint”)
private IPrint consolePrint;

@Autowired
private IPrint logPrint;

@PostConstruct
public void init() {
    consolePrint.print(" console print!!!");
    logPrint.print(" log print!!!");
}

}
上面是两种常见的使用姿势,此外还可以借助@Primary注解来声明默认的注入bean

@Primary注解
这个注解就是为了解决当有多个bean满足注入条件时,有这个注解的实例被选中
根据上面的作用说明,很明显可以得知一点

@Primary注解的使用有唯一性要求:即对应上面的case,一个接口的子类中,只能有一个实现上有这个注解

假设将这个注解放在LogPrint上之后,如下

@Slf4j
@Component
@Primary
public class LogPrint implements IPrint {
@Override
public void print(String msg) {
log.info(“log print: {}”, msg);
}
}
结合上面的常用姿势,加上这个注解之后,我们的测试用例应该至少包含下面几个

@Resource 指定beanName的是否会被@Primary影响
前面的@Autowired注解 + 属性名的方式,是按照第一节的方式选择呢,还是选择被@Primary标识的实例
@Autowired + 随意的一个非beanName的属性,验证是否会选中@Primary标识的注解

@Component
public class PrintDemoBean {

@Resource(name = "logPrint")
private IPrint print;

/**
 * 下面的注解不指定name,则实例为logPrint
 */
@Autowired
private IPrint consolePrint;

// logPrint的选择,由@Primary注解决定
@Autowired
private IPrint logPrint;

// logPrint的选择,由@Primary注解决定
@Autowired(required = false)
private IPrint xxxPrint;

@PostConstruct
public void init() {
    print.print("expect logPrint for [print]");
    consolePrint.print(" expect logPrint for [consolePrint]");
    logPrint.print("expect logPrint for [logPrint]");
    xxxPrint.print("expect logPrint for [xxxPrint]");
}

}
执行结果如下

2018-10-22 19:42:40.234 INFO 61966 — [ main] c.g.h.b.b.choose.sameclz.LogPrint : log print: expect logPrint for [print]
2018-10-22 19:42:40.235 INFO 61966 — [ main] c.g.h.b.b.choose.sameclz.LogPrint : log print: expect consolePrint for [consolePrint]
2018-10-22 19:42:40.235 INFO 61966 — [ main] c.g.h.b.b.choose.sameclz.LogPrint : log print: expect logPrint for [logPrint]
2018-10-22 19:42:40.235 INFO 61966 — [ main] c.g.h.b.b.choose.sameclz.LogPrint : log print: expect logPrint for [xxxPrint]
小结
根据前面的执行,因此可以知晓,选择bean的方式如下
存在@Primary注解时

@Resource注解指定name时,根据name来查找对应的beanbr/>@Autowired注解,全部都用@Primary标识的注解
@Primary注解要求唯一(非广义的唯一性,并不是指只能用一个@Primary,具体看前面)
不存在@Primary注解时

@Resource注解指定name时,根据name来查找对应的beanbr/>@Autowired注解时,根据属性名去查对应的Bean,如果查不到则抛异常;如果查到,那即是它了
II. 重名Bean的问题
在我们实际的业务开发中,有多个bean名为xxx的异常应该算是比较常见的,也就是说应该不能有两个bean叫同一个name;但考虑下下面这个场景

A的服务,依赖B和C的服务;而B和C是两个完全独立的第三方服务,他们各自都提供了一个beanName=xxxService的bean,对于A而言,Spring容器中就会有BeanName冲突的问题了,而且这种场景,对A而言,也是不可控的啊,这种情况下改怎么办?

同名Bean
先来个case演示下同名bean的情况,如下定义两个bean,除了包路径不一样外,类名相同,通过@Component注解方式声明bean,因此两个bean的beanName都是SameA

package com.git.hui.boot.beanorder.choose.samename.a;
import org.springframework.stereotype.Component;

/**

Created by @author yihui in 21:32 18/10/22.br/>*/
@Component
public class SameA {
private String text ;
public SameA() {
text = “a sameA!”;
}

public void print() {
System.out.println(text);
}
}

package com.git.hui.boot.beanorder.choose.samename.b;

import org.springframework.stereotype.Component;

/**

Created by @author yihui in 21:33 18/10/22.br/>*/
@Component
public class SameA {
private String text;

public SameA() {
text = “B SameA”;
}

public void print() {
System.out.println(text);
}
}

接下来测试下引用,是否有问题
package com.git.hui.boot.beanorder.choose.samename;

import com.git.hui.boot.beanorder.choose.samename.a.SameA;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

/**

Created by @author yihui in 21:32 18/10/22.br/>*/
@Component
public class SameDemo {

@Autowired
private SameA sameA;

@PostConstruct
public void init() {
sameA.print();
}
}

执行之后,毫不意外的抛出了异常,堆栈信息如下

org.springframework.beans.factory.BeanDefinitionStoreException: Failed to parse configuration class [com.git.hui.boot.beanorder.Application]; nested exception is org.springframework.context.annotation.ConflictingBeanDefinitionException: Annotation-specified bean name ‘sameA’ for bean class [com.git.hui.boot.beanorder.choose.samename.b.SameA] conflicts with existing, non-compatible bean definition of same name and class [com.git.hui.boot.beanorder.choose.samename.a.SameA]
at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:184) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE]
at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:316) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE]
at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:233) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE]
at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:271) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE]
at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:91) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:694) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:532) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:762) [spring-boot-2.0.4.RELEASE.jar:2.0.4.RELEASE]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:398) [spring-boot-2.0.4.RELEASE.jar:2.0.4.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:330) [spring-boot-2.0.4.RELEASE.jar:2.0.4.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1258) [spring-boot-2.0.4.RELEASE.jar:2.0.4.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1246) [spring-boot-2.0.4.RELEASE.jar:2.0.4.RELEASE]
at com.git.hui.boot.beanorder.Application.main(Application.java:15) [classes/:na]
Caused by: org.springframework.context.annotation.ConflictingBeanDefinitionException: Annotation-specified bean name ‘sameA’ for bean class [com.git.hui.boot.beanorder.choose.samename.b.SameA] conflicts with existing, non-compatible bean definition of same name and class [com.git.hui.boot.beanorder.choose.samename.a.SameA]
at org.springframework.context.annotation.ClassPathBeanDefinitionScanner.checkCandidate(ClassPathBeanDefinitionScanner.java:348) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE]
at org.springframework.context.annotation.ClassPathBeanDefinitionScanner.doScan(ClassPathBeanDefinitionScanner.java:286) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE]
at org.springframework.context.annotation.ComponentScanAnnotationParser.parse(ComponentScanAnnotationParser.java:132) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE]
at org.springframework.context.annotation.ConfigurationClassParser.doProcessConfigurationClass(ConfigurationClassParser.java:288) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE]
at org.springframework.context.annotation.ConfigurationClassParser.processConfigurationClass(ConfigurationClassParser.java:245) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE]
at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:202) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE]
at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:170) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE]
… 12 common frames omitted
同名问题规避
如果真的出现了上面这个问题,该怎么解决呢?如果这些bean是我们可控的,最简单的方式就是不要同名,定义的时候指定beanName,如下
@Component(“aSameA”)
public class SameA {
private String text ;
public SameA() {
text = “a sameA!”;
}

public void print() {
    System.out.println(text);
}

}
如果完全不可控呢?正如前面说的两个第三方服务我都得依赖,但是他们有同名的bean,怎么破?

一个解决方法就是排除掉其中一个同名的bean的自动加载,采用主动注册的方式注册这个bean

排除自动扫描的bean的方式如下,在启动类添加注解@ComponentScan并指定其中的excludeFilters属性

@SpringBootApplication
@ComponentScan(excludeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = SameA.class)})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
然后自定义一个bean的配置类

package com.git.hui.boot.beanorder.choose.samename;

import com.git.hui.boot.beanorder.choose.samename.a.SameA;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**

  • Created by @author yihui in 22:14 18/10/22.
    */
    @Configuration
    public class AutoConfig {
    @Bean
    public SameA mySameA() {
    return new SameA();
    }
    }
    其他的代码和之前没有区别,再次执行,结果如下, 最后的输出为 a sameA!,根据类型来选择了实例化的bean了
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值