深入Spring Boot:怎样排查expected single matching bean but found 2的异常

简介: 写在前面这个demo来说明怎么排查一个常见的spring expected single matching bean but found 2的异常。

写在前面

这个demo来说明怎么排查一个常见的spring expected single matching bean but found 2的异常。

https://github.com/hengyunabc/spring-boot-inside/tree/master/demo-expected-single

调试排查 expected single matching bean but found 2 的错误

把工程导入IDE里,直接启动应用,抛出来的异常信息是:

Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'javax.sql.DataSource' available: expected single matching bean but found 2: h2DataSource1,h2DataSource2
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1041) ~[spring-beans-4.3.9.RELEASE.jar:4.3.9.RELEASE]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:345) ~[spring-beans-4.3.9.RELEASE.jar:4.3.9.RELEASE]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:340) ~[spring-beans-4.3.9.RELEASE.jar:4.3.9.RELEASE]
    at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1090) ~[spring-context-4.3.9.RELEASE.jar:4.3.9.RELEASE]
    at org.springframework.boot.autoconfigure.jdbc.DataSourceInitializer.init(DataSourceInitializer.java:71) ~[spring-boot-autoconfigure-1.4.7.RELEASE.jar:1.4.7.RELEASE]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_112]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_112]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_112]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_112]
    at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleElement.invoke(InitDestroyAnnotationBeanPostProcessor.java:366) ~[spring-beans-4.3.9.RELEASE.jar:4.3.9.RELEASE]
    at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleMetadata.invokeInitMethods(InitDestroyAnnotationBeanPostProcessor.java:311) ~[spring-beans-4.3.9.RELEASE.jar:4.3.9.RELEASE]
    at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:134) ~[spring-beans-4.3.9.RELEASE.jar:4.3.9.RELEASE]
    ... 30 common frames omitted

很多人碰到这种错误时,就乱配置一通,找不到下手的办法。其实耐心排查下,是很简单的。

抛出异常的原因

异常信息写得很清楚了,在spring context里需要注入/获取到一个DataSource bean,但是现在spring context里出现了两个,它们的名字是:h2DataSource1,h2DataSource2

那么有两个问题:

  1. 应用是在哪里要注入/获取到一个DataSource bean?
  2. h2DataSource1,h2DataSource2 是在哪里定义的?

使用 Java Exception Breakpoint

在IDE里,新建一个断点,类型是Java Exception Breakpoint(如果不清楚怎么添加,可以搜索对应IDE的使用文档),异常类是上面抛出来的NoUniqueBeanDefinitionException

当断点停住时,查看栈,可以很清楚地找到是在DataSourceInitializer.init() line: 71这里要获取DataSource

Thread [main] (Suspended (exception NoUniqueBeanDefinitionException))
    owns: ConcurrentHashMap<K,V>  (id=49)
    owns: Object  (id=50)
    DefaultListableBeanFactory.resolveNamedBean(Class<T>, Object...) line: 1041
    DefaultListableBeanFactory.getBean(Class<T>, Object...) line: 345
    DefaultListableBeanFactory.getBean(Class<T>) line: 340
    AnnotationConfigEmbeddedWebApplicationContext(AbstractApplicationContext).getBean(Class<T>) line: 1090
    DataSourceInitializer.init() line: 71
    NativeMethodAccessorImpl.invoke0(Method, Object, Object[]) line: not available [native method]
    NativeMethodAccessorImpl.invoke(Object, Object[]) line: 62
    DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 43
    Method.invoke(Object, Object...) line: 498
    InitDestroyAnnotationBeanPostProcessor$LifecycleElement.invoke(Object) line: 366
    InitDestroyAnnotationBeanPostProcessor$LifecycleMetadata.invokeInitMethods(Object, String) line: 311
    CommonAnnotationBeanPostProcessor(InitDestroyAnnotationBeanPostProcessor).postProcessBeforeInitialization(Object, String) line: 134
    DefaultListableBeanFactory(AbstractAutowireCapableBeanFactory).applyBeanPostProcessorsBeforeInitialization(Object, String) line: 409
    DefaultListableBeanFactory(AbstractAutowireCapableBeanFactory).initializeBean(String, Object, RootBeanDefinition) line: 1620
    DefaultListableBeanFactory(AbstractAutowireCapableBeanFactory).doCreateBean(String, RootBeanDefinition, Object[]) line: 555
    DefaultListableBeanFactory(AbstractAutowireCapableBeanFactory).createBean(String, RootBeanDefinition, Object[]) line: 483
    AbstractBeanFactory$1.getObject() line: 306
    DefaultListableBeanFactory(DefaultSingletonBeanRegistry).getSingleton(String, ObjectFactory<?>) line: 230
    DefaultListableBeanFactory(AbstractBeanFactory).doGetBean(String, Class<T>, Object[], boolean) line: 302
    DefaultListableBeanFactory(AbstractBeanFactory).getBean(String, Class<T>, Object...) line: 220
    DefaultListableBeanFactory.resolveNamedBean(Class<T>, Object...) line: 1018
    DefaultListableBeanFactory.getBean(Class<T>, Object...) line: 345
    DefaultListableBeanFactory.getBean(Class<T>) line: 340
    DataSourceInitializerPostProcessor.postProcessAfterInitialization(Object, String) line: 62
    DefaultListableBeanFactory(AbstractAutowireCapableBeanFactory).applyBeanPostProcessorsAfterInitialization(Object, String) line: 423
    DefaultListableBeanFactory(AbstractAutowireCapableBeanFactory).initializeBean(String, Object, RootBeanDefinition) line: 1633
    DefaultListableBeanFactory(AbstractAutowireCapableBeanFactory).doCreateBean(String, RootBeanDefinition, Object[]) line: 555
    DefaultListableBeanFactory(AbstractAutowireCapableBeanFactory).createBean(String, RootBeanDefinition, Object[]) line: 483
    AbstractBeanFactory$1.getObject() line: 306
    DefaultListableBeanFactory(DefaultSingletonBeanRegistry).getSingleton(String, ObjectFactory<?>) line: 230
    DefaultListableBeanFactory(AbstractBeanFactory).doGetBean(String, Class<T>, Object[], boolean) line: 302
    DefaultListableBeanFactory(AbstractBeanFactory).getBean(String) line: 197
    DefaultListableBeanFactory.preInstantiateSingletons() line: 761
    AnnotationConfigEmbeddedWebApplicationContext(AbstractApplicationContext).finishBeanFactoryInitialization(ConfigurableListableBeanFactory) line: 867
    AnnotationConfigEmbeddedWebApplicationContext(AbstractApplicationContext).refresh() line: 543
    AnnotationConfigEmbeddedWebApplicationContext(EmbeddedWebApplicationContext).refresh() line: 122
    SpringApplication.refresh(ApplicationContext) line: 762
    SpringApplication.refreshContext(ConfigurableApplicationContext) line: 372
    SpringApplication.run(String...) line: 316
    SpringApplication.run(Object[], String[]) line: 1187
    SpringApplication.run(Object, String...) line: 1176
    DemoExpectedSingleApplication.main(String[]) line: 17

定位哪里要注入/使用DataSource

要获取DataSource具体的代码是:

//org.springframework.boot.autoconfigure.jdbc.DataSourceInitializer.init()
    @PostConstruct
    public void init() {
        if (!this.properties.isInitialize()) {
            logger.debug("Initialization disabled (not running DDL scripts)");
            return;
        }
        if (this.applicationContext.getBeanNamesForType(DataSource.class, false,
                false).length > 0) {
            this.dataSource = this.applicationContext.getBean(DataSource.class);
        }
        if (this.dataSource == null) {
            logger.debug("No DataSource found so not initializing");
            return;
        }
        runSchemaScripts();
    }

this.applicationContext.getBean(DataSource.class); 要求spring context里只有一个DataSource的bean,但是应用里有两个,所以抛出了NoUniqueBeanDefinitionException

BeanDefinition获取bean具体定义的代码

我们再来看 h2DataSource1,h2DataSource2 是在哪里定义的?

上面进程断在了DefaultListableBeanFactory.resolveNamedBean(Class<T>, Object...) 函数里的 throw new NoUniqueBeanDefinitionException(requiredType, candidates.keySet()); 这一行。

那么我们在这里执行一下(如果不清楚,先搜索下IDE怎么在断点情况下执行代码):

this.getBeanDefinition("h2DataSource1")

返回的信息是:

Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=demoExpectedSingleApplication; factoryMethodName=h2DataSource1; initMethodName=null; destroyMethodName=(inferred);
defined in com.example.demo.expected.single.DemoExpectedSingleApplication

可以很清楚地定位到h2DataSource1这个bean是在 com.example.demo.expected.single.DemoExpectedSingleApplication里定义的。

所以上面两个问题的答案是:

  1. 是spring boot代码里的DataSourceInitializer.init() line: 71这里要获取DataSource,并且只允许有一个DataSource实例
  2. h2DataSource1,h2DataSource2 是在com.example.demo.expected.single.DemoExpectedSingleApplication里定义的

解决问题

上面排查到的原因是:应用定义了两个DataSource实例,但是spring boot却要求只有一个。那么有两种办法来解决:

  1. 使用@Primary来指定一个优先使用的DataSource,这样子spring boot里自动初始的代码会获取到@Primary的bean
  2. 把spring boot自动初始化DataSource相关的代码禁止掉,应用自己来控制所有的DataSource相关的bean

禁止的办法有两种:

  1. 在main函数上配置exclude


    @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class })

  2. 在application.properties里配置:


    spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration

总结

  • 排查spring初始化问题时,灵活使用Java Exception Breakpoint
  • 从异常栈上,可以很容易找到哪里要注入/使用bean
  • BeanDefinition可以找到bean是在哪里定义的(哪个Configuration类/xml)
相关文章
|
XML Java 测试技术
Spring IOC—基于注解配置和管理Bean 万字详解(通俗易懂)
Spring 第三节 IOC——基于注解配置和管理Bean 万字详解!
1013 26
|
JSON Java 数据格式
微服务——SpringBoot使用归纳——Spring Boot中的全局异常处理——处理系统异常
本文介绍了在Spring Boot项目中如何通过创建`GlobalExceptionHandler`类来全局处理系统异常。通过使用`@ControllerAdvice`注解,可以拦截项目中的各种异常,并结合`@ExceptionHandler`注解针对特定异常(如参数缺失、空指针等)进行定制化处理。文中详细展示了处理参数缺失异常和空指针异常的示例代码,并说明了通过拦截`Exception`父类实现统一异常处理的方法。虽然拦截`Exception`可一劳永逸,但为便于问题排查,建议优先处理常见异常,最后再兜底处理未知异常,确保返回给调用方的信息友好且明确。
1522 0
微服务——SpringBoot使用归纳——Spring Boot中的全局异常处理——处理系统异常
|
JSON Java 数据格式
微服务——SpringBoot使用归纳——Spring Boot中的全局异常处理——拦截自定义异常
本文介绍了在实际项目中如何拦截自定义异常。首先,通过定义异常信息枚举类 `BusinessMsgEnum`,统一管理业务异常的代码和消息。接着,创建自定义业务异常类 `BusinessErrorException`,并在其构造方法中传入枚举类以实现异常信息的封装。最后,利用 `GlobalExceptionHandler` 拦截并处理自定义异常,返回标准的 JSON 响应格式。文章还提供了示例代码和测试方法,展示了全局异常处理在 Spring Boot 项目中的应用价值。
629 0
|
存储 Java Spring
【Spring】获取Bean对象需要哪些注解
@Conntroller,@Service,@Repository,@Component,@Configuration,关于Bean对象的五个常用注解
472 12
|
存储 Java 应用服务中间件
【Spring】IoC和DI,控制反转,Bean对象的获取方式
IoC,DI,控制反转容器,Bean的基本常识,类注解@Controller,获取Bean对象的常用三种方式
592 12
|
XML Java 数据格式
Spring容器Bean之XML配置方式
通过对以上内容的掌握,开发人员可以灵活地使用Spring的XML配置方式来管理应用程序的Bean,提高代码的模块化和可维护性。
538 6
|
XML Java 数据格式
🌱 深入Spring的心脏:Bean配置的艺术与实践 🌟
本文深入探讨了Spring框架中Bean配置的奥秘,从基本概念到XML配置文件的使用,再到静态工厂方式实例化Bean的详细步骤,通过实际代码示例帮助读者更好地理解和应用Spring的Bean配置。希望对你的Spring开发之旅有所助益。
|
安全 Java 开发者
Spring容器中的bean是线程安全的吗?
Spring容器中的bean默认为单例模式,多线程环境下若操作共享成员变量,易引发线程安全问题。Spring未对单例bean做线程安全处理,需开发者自行解决。通常,Spring bean(如Controller、Service、Dao)无状态变化,故多为线程安全。若涉及线程安全问题,可通过编码或设置bean作用域为prototype解决。
427 1
|
10月前
|
Java Spring 容器
SpringBoot自动配置的原理是什么?
Spring Boot自动配置核心在于@EnableAutoConfiguration注解,它通过@Import导入配置选择器,加载META-INF/spring.factories中定义的自动配置类。这些类根据@Conditional系列注解判断是否生效。但Spring Boot 3.0后已弃用spring.factories,改用新格式的.imports文件进行配置。
1373 0
|
11月前
|
人工智能 Java 测试技术
Spring Boot 集成 JUnit 单元测试
本文介绍了在Spring Boot中使用JUnit 5进行单元测试的常用方法与技巧,包括添加依赖、编写测试类、使用@SpringBootTest参数、自动装配测试模块(如JSON、MVC、WebFlux、JDBC等),以及@MockBean和@SpyBean的应用。内容实用,适合Java开发者参考学习。
1224 0