SpringBoot中出现NoUniqueBeanDefinitionException的解决方法

Situation背景

  • Springboot:2.6.4
  • Activiti:7.1.0.M4
  • 启动类配置如下
@SpringBootApplication(scanBasePackages = {"io.github.meta.ease.bpm"})@ComponentScan(basePackages ={"io.github.meta.ease.bpm"} )public class QuickstartApplication {    public static void main(String[] args) {        SpringApplication.run(QuickstartApplication.class, args);    }}

Question问题

Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'springAsyncExecutor' defined in class path resource [org/activiti/spring/boot/ProcessEngineAutoConfiguration.class]: Unsatisfied dependency expressed through method 'springAsyncExecutor' parameter 0; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'org.springframework.core.task.TaskExecutor' available: expected single matching bean but found 3: jobTaskScheduler,dataConvertTheadPool,threadPoolat org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:800)at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:541)at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1352)at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1195)at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582)at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:276)at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1389)at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1309)at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:887)at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791)... 105 common frames omittedCaused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'org.springframework.core.task.TaskExecutor' available: expected single matching bean but found 3: jobTaskScheduler,dataConvertTheadPool,threadPoolat org.springframework.beans.factory.config.DependencyDescriptor.resolveNotUnique(DependencyDescriptor.java:220)at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1367)at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1309)at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:887)at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791)... 119 common frames omitted

无法创建 自动配置ProcessEngineAutoConfiguration 中的 bean springAsyncExecutor,由于所依赖的bean 类型 org.springframework.core.task.TaskExecutor无法找到唯一的bean实例。

Answer解决方案

知识储备

@Bean产生一个bean并且交给Spring容器管理,如果即将要产生的Bean依赖另外一个Bean,使用Autowired的依赖策略。而@Autowired依赖原理如下:

1、@Autowired是Spring自带的注解,通过AutowiredAnnotationBeanPostProcessor 类实现的依赖注入

2、@Autowired可以作用在CONSTRUCTOR、METHOD、PARAMETER、FIELD、ANNOTATION_TYPE

3、@Autowired默认是根据类型(byType )进行自动装配的

4、如果有多个类型一样的Bean候选者,需要指定按照名称(byName )进行装配,则需要配合@Qualifier。指定名称后,如果Spring IOC容器中没有对应的组件bean抛出NoSuchBeanDefinitionException。也可以将@Autowired中required配置为false,如果配置为false之后,当没有找到相应bean的时候,系统不会抛异常

简单还原现场问题

注意@ComponentScan(basePackages = {"io.github.meta.ease.bpm"})

  1. 主启动类
package io.github.meta.ease.bpm;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.context.annotation.ComponentScan;@SpringBootApplication@ComponentScan(basePackages = {"io.github.meta.ease.bpm"})public class QuickstartApplication {    public static void main(String[] args) {        SpringApplication.run(QuickstartApplication.class, args);    }}
  1. 注入Bean
package io.github.meta.ease.bpm;import org.springframework.beans.factory.InitializingBean;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;@Configuration(proxyBeanMethods = false)public class ScheduleJobAutoConfiguration implements InitializingBean {    @Bean({"jobTaskScheduler"})    public ThreadPoolTaskScheduler jobTaskScheduler() {        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();        taskScheduler.setThreadNamePrefix("job-scheduler-");        taskScheduler.setPoolSize(Runtime.getRuntime().availableProcessors());        taskScheduler.setDaemon(true);        taskScheduler.setWaitForTasksToCompleteOnShutdown(true);        return taskScheduler;    }    @Bean    public ThreadPoolTaskExecutor dataConvertTheadPool() {        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();        executor.initialize();        return executor;    }    @Override    public void afterPropertiesSet() throws Exception {    }}
  1. 模拟三方jar
package io.activiti.spring.boot;import org.springframework.boot.autoconfigure.AutoConfigureAfter;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.task.TaskExecutor;@Configuration@AutoConfigureAfter(name = {"org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration",        "org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration"})public class ProcessEngineAutoConfiguration {    @Bean    public SpringAsyncExecutor springAsyncExecutor(TaskExecutor applicationTaskExecutor) {        return new SpringAsyncExecutor(applicationTaskExecutor);    }}public class SpringAsyncExecutor {    public SpringAsyncExecutor(TaskExecutor applicationTaskExecutor) {    }}
  1. SpringBoot 自动配置问
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\  io.activiti.spring.boot.ProcessEngineAutoConfiguration,\  io.github.meta.ease.bpm.ScheduleJobAutoConfiguration
  1. 启动案例报错如下:
Parameter 0 of method springAsyncExecutor in io.activiti.spring.boot.ProcessEngineAutoConfiguration required a single bean, but 2 were found:- jobTaskScheduler: defined by method 'jobTaskScheduler' in class path resource [io/github/meta/ease/bpm/ScheduleJobAutoConfiguration.class]- dataConvertTheadPool: defined by method 'dataConvertTheadPool' in class path resource [io/github/meta/ease/bpm/ScheduleJobAutoConfiguration.class]Action:Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumedDisconnected from the target VM, address: '127.0.0.1:49825', transport: 'socket'Process finished with exit code 1

问题分析

spring 注入Bean流程

@Overridepublic void refresh() throws BeansException, IllegalStateException {synchronized (this.startupShutdownMonitor) {StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");// Prepare this context for refreshing.prepareRefresh();// Tell the subclass to refresh the internal bean factory.ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();// Prepare the bean factory for use in this context.prepareBeanFactory(beanFactory);try {// Allows post-processing of the bean factory in context subclasses.postProcessBeanFactory(beanFactory);StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");// Invoke factory processors registered as beans in the context.                //开始注入Bean定义invokeBeanFactoryPostProcessors(beanFactory);

ConfigurationClassPostProcessor

@Overridepublic void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {int registryId = System.identityHashCode(registry);if (this.registriesPostProcessed.contains(registryId)) {throw new IllegalStateException("postProcessBeanDefinitionRegistry already called on this post-processor against " + registry);}if (this.factoriesPostProcessed.contains(registryId)) {throw new IllegalStateException("postProcessBeanFactory already called on this post-processor against " + registry);}this.registriesPostProcessed.add(registryId);        //processConfigBeanDefinitions(registry);}public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {List configCandidates = new ArrayList<>();String[] candidateNames = registry.getBeanDefinitionNames();for (String beanName : candidateNames) {BeanDefinition beanDef = registry.getBeanDefinition(beanName);if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {if (logger.isDebugEnabled()) {logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);}}else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));}}    ......                        // Parse each @Configuration class        // 扫描基于@Config 配置的Bean,正由于是主启动类外部自己加载了ComponentScan注解,导致ComponentScan的扫描的优先级高于自动配置类ConfigurationClassParser parser = new ConfigurationClassParser(this.metadataReaderFactory, this.problemReporter, this.environment,this.resourceLoader, this.componentScanBeanNameGenerator, registry);Set candidates = new LinkedHashSet<>(configCandidates);Set alreadyParsed = new HashSet<>(configCandidates.size());do {StartupStep processConfig = this.applicationStartup.start("spring.context.config-classes.parse"); // 扫描基于@Config 配置的Bean,正由于是主启动类外部自己加载了ComponentScan注解,导致ComponentScan的扫描的优先级高于自动配置类            parser.parse(candidates);parser.validate();}

ConfigurationClassParser:重点逻辑(此处获取的componentScans 属性是Spring单独@ComponentScan配置,导致自定义扫描的jar包提前进入到Spring IOC容器内部

Set componentScans = AnnotationConfigUtils.attributesForRepeatable(sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);if (!componentScans.isEmpty() &&!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {for (AnnotationAttributes componentScan : componentScans) {// The config class is annotated with @ComponentScan -> perform the scan immediatelySet scannedBeanDefinitions =this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());// Check the set of scanned definitions for any further config classes and parse recursively if neededfor (BeanDefinitionHolder holder : scannedBeanDefinitions) {BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();if (bdCand == null) {bdCand = holder.getBeanDefinition();}if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {parse(bdCand.getBeanClassName(), holder.getBeanName());}}}}

主启动类不配置 //@ComponentScan(basePackages ={"io.github.meta.ease.bpm"} )

主启动类单独配置@ComponentScan(basePackages ={"io.github.meta.ease.bpm"} )

从这两个图基本可以确定问题的原因。

继续Debug 后续流程 ComponentScanAnnotationParser、ClassPathBeanDefinitionScanner.doScan(String... basePackages)可以明确问题原因。

结论

  1. 基于SpringBoot的应用在主启动类上同时存在@SpringBootApplication、@ComponentScan注解,在Spring注入Bean的过程中,获取的ComponentScan 注解的属性和单独只配置SpringBootApplication 获得属性不一致。SpringBootApplication 多了如下配置
ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })

AutoConfigurationExcludeFilter匹配注册的自动配置类的 TypeFilter 实现,排除了自动配置相关实现的Bean。

解决方案

  1. 修改自动配置类,适用于有源码的情况下,指定Bean名称。
@Configuration@AutoConfigureAfter(name = {"org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration",        "org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration"})public class ProcessEngineAutoConfiguration {    @Bean    public SpringAsyncExecutor springAsyncExecutor(@Qualifier("applicationTaskExecutor") TaskExecutor applicationTaskExecutor) {        return new SpringAsyncExecutor(applicationTaskExecutor);    }}
  1. 修改主启动类,删除掉@ComponentScan,使用@SpringBootApplication属性配置扫描包路径。
@SpringBootApplication(scanBasePackages = {"io.github.meta.ease.bpm"})//@ComponentScan(basePackages ={"io.github.meta.ease.bpm"} )public class QuickstartApplication {    public static void main(String[] args) {        SpringApplication.run(QuickstartApplication.class, args);    }}
发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章