写在前面
这个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里,直接启动应用,抛出来的异常信息是:
1 | 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 |
很多人碰到这种错误时,就乱配置一通,找不到下手的办法。其实耐心排查下,是很简单的。
抛出异常的原因
异常信息写得很清楚了,在spring context里需要注入/获取到一个DataSource
bean,但是现在spring context里出现了两个,它们的名字是:h2DataSource1,h2DataSource2
那么有两个问题:
- 应用是在哪里要注入/获取到一个
DataSource
bean? - h2DataSource1,h2DataSource2 是在哪里定义的?
使用 Java Exception Breakpoint
在IDE里,新建一个断点,类型是Java Exception Breakpoint
(如果不清楚怎么添加,可以搜索对应IDE的使用文档),异常类是上面抛出来的NoUniqueBeanDefinitionException
。
当断点停住时,查看栈,可以很清楚地找到是在DataSourceInitializer.init() line: 71
这里要获取DataSource
:
1 | Thread [main] (Suspended (exception NoUniqueBeanDefinitionException)) |
定位哪里要注入/使用DataSource
要获取DataSource
具体的代码是:
1 | //org.springframework.boot.autoconfigure.jdbc.DataSourceInitializer.init() |
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怎么在断点情况下执行代码):
1 | this.getBeanDefinition("h2DataSource1") |
返回的信息是:
1 | 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); |
可以很清楚地定位到h2DataSource1
这个bean是在 com.example.demo.expected.single.DemoExpectedSingleApplication
里定义的。
所以上面两个问题的答案是:
- 是spring boot代码里的
DataSourceInitializer.init() line: 71
这里要获取DataSource
,并且只允许有一个DataSource
实例 - h2DataSource1,h2DataSource2 是在
com.example.demo.expected.single.DemoExpectedSingleApplication
里定义的
解决问题
上面排查到的原因是:应用定义了两个DataSource
实例,但是spring boot却要求只有一个。那么有两种办法来解决:
- 使用
@Primary
来指定一个优先使用的DataSource
,这样子spring boot里自动初始的代码会获取到@Primary
的bean - 把spring boot自动初始化
DataSource
相关的代码禁止掉,应用自己来控制所有的DataSource
相关的bean
禁止的办法有两种:
在main函数上配置exclude
1
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class })
在application.properties里配置:
1
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration
总结
- 排查spring初始化问题时,灵活使用Java Exception Breakpoint
- 从异常栈上,可以很容易找到哪里要注入/使用bean
- 从
BeanDefinition
可以找到bean是在哪里定义的(哪个Configuration类/xml)