歡迎光臨
每天分享高質量文章

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

(點選上方公眾號,可快速關註)


來源:hengyunabc,

blog.csdn.net/hengyunabc/article/details/78762121

寫在前面

這個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  (id=49)

    owns: Object  (id=50)

    DefaultListableBeanFactory.resolveNamedBean(Class, Object…) line: 1041

    DefaultListableBeanFactory.getBean(Class, Object…) line: 345

    DefaultListableBeanFactory.getBean(Class) line: 340

    AnnotationConfigEmbeddedWebApplicationContext(AbstractApplicationContext).getBean(Class) 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, Object[], boolean) line: 302

    DefaultListableBeanFactory(AbstractBeanFactory).getBean(String, Class, Object…) line: 220

    DefaultListableBeanFactory.resolveNamedBean(Class, Object…) line: 1018

    DefaultListableBeanFactory.getBean(Class, Object…) line: 345

    DefaultListableBeanFactory.getBean(Class) 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, 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, 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

禁止的辦法有兩種:

在main函式上配置exclude

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

在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)

看完本文有收穫?請轉發分享給更多人

關註「ImportNew」,提升Java技能

贊(0)

分享創造快樂