什么是NoSuchBeanDefinitionException,我该如何修复它?豆子不存在,它没有被注册预期单个匹配的豆子,但发现 2 个(或更多)使用错误的豆名更高级的案例数组、集合和映射

2022-08-31 14:47:00

关于春季的例外情况,请解释以下情况:NoSuchBeanDefinitionException

  • 这是什么意思?
  • 在什么条件下会被抛出?
  • 我该如何预防?

这篇文章旨在成为关于NoSuchBeanDefinitionException在使用Spring的应用程序中出现的全面问答。


答案 1

NoSuchBeanDefinitionException的javadoc解释了

当 要求 a 提供找不到定义的 Bean 实例时引发异常。这可能指向不存在的 Bean、非唯一 Bean 或没有关联 Bean 定义的手动注册的单一实例。BeanFactory

BeanFactory基本上是代表Spring的控制反转容器的抽象。它在内部和外部向应用程序公开 Bean。当它无法找到或检索这些豆子时,它会抛出一个 .NoSuchBeanDefinitionException

以下是(或相关类)无法找到Bean的简单原因,以及如何确保它找到。BeanFactory


豆子不存在,它没有被注册

在下面的示例中

@Configuration
public class Example {
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Example.class);
        ctx.getBean(Foo.class);
    }
}

class Foo {}   

我们还没有通过方法,扫描,XML定义或任何其他方式为该类型注册bean定义。因此,管理的 没有指示从何处获取 由 请求的 bean。上面的片段抛出Foo@Bean@ComponentBeanFactoryAnnotationConfigApplicationContextgetBean(Foo.class)

Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException:
    No qualifying bean of type [com.example.Foo] is defined

同样,在尝试满足依赖项时可能会引发异常。例如@Autowired

@Configuration
@ComponentScan
public class Example {
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Example.class);
    }
}

@Component
class Foo { @Autowired Bar bar; }
class Bar { }

此处,通过 注册了 Bean 定义。但是春天对.因此,在尝试自动连接 Bean 实例的字段时,它找不到相应的 Bean。它抛出(嵌套在不满意的独立性异常中)Foo@ComponentScanBarbarFoo)

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: 
    No qualifying bean of type [com.example.Bar] found for dependency [com.example.Bar]: 
        expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

有多种方法可以注册 Bean 定义。

  • @Bean类或 XML 配置中的方法@Configuration<bean>
  • @Component(及其元注释,例如)通过或以XML形式@Repository@ComponentScan<context:component-scan ... />
  • 手动通过 GenericApplicationContext#registerBeanDefinition
  • 手动通过BeanDefinitionRegistryPostProcessor

...以及更多。

确保预期的豆子已正确注册。

一个常见的错误是多次注册bean,即。为同一类型混合上述选项。例如,我可能有

@Component
public class Foo {}

和 XML 配置

<context:component-scan base-packages="com.example" />
<bean name="eg-different-name" class="com.example.Foo />

这样的配置将注册两个类型的bean,一个带有name,另一个带有name。确保您没有意外注册的豆子比您想要的多。这导致我们...Foofooeg-different-name

如果同时使用 XML 和基于批注的配置,请确保从另一个配置导入一个配置。XML 提供

<import resource=""/>

而Java提供了@ImportResource注释。

预期单个匹配的豆子,但发现 2 个(或更多)

有时,您需要为同一类型(或接口)使用多个Bean。例如,您的应用程序可能使用两个数据库,一个 MySQL 实例和一个 Oracle 实例。在这种情况下,您将有两个bean来管理与每个bean的连接。对于(简化的)示例,如下所示DataSource

@Configuration
public class Example {
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Example.class);
        System.out.println(ctx.getBean(DataSource.class));
    }
    @Bean(name = "mysql")
    public DataSource mysql() { return new MySQL(); }
    @Bean(name = "oracle")
    public DataSource oracle() { return new Oracle(); }
}
interface DataSource{}
class MySQL implements DataSource {}
class Oracle implements DataSource {}

抛出

Exception in thread "main" org.springframework.beans.factory.NoUniqueBeanDefinitionException: 
    No qualifying bean of type [com.example.DataSource] is defined:
        expected single matching bean but found 2: oracle,mysql

因为通过方法注册的两个bean都满足BeanFactory#getBean(Class)的要求,即。它们都实现了 .在这个例子中,Spring没有机制来区分或优先考虑两者。但这种机制是存在的。@BeanDataSource

您可以使用@Primary(及其在 XML 中的等效项),如文档本文中所述。通过此更改

@Bean(name = "mysql")
@Primary
public DataSource mysql() { return new MySQL(); } 

前面的代码段不会引发异常,而是返回 bean。mysql

您还可以使用(及其在 XML 中的等效项)来更好地控制 Bean 选择过程,如文档中所述。虽然主要用于按类型自动连线,但允许您按名称自动连线。例如@Qualifier@Autowired@Qualifier

@Bean(name = "mysql")
@Qualifier(value = "main")
public DataSource mysql() { return new MySQL(); }

现在可以注射为

@Qualifier("main") // or @Qualifier("mysql"), to use the bean name
private DataSource dataSource;

没有问题。@Resource也是一种选择。

使用错误的豆名

正如有多种方法可以注册 Bean 一样,也有多种方法可以命名它们。

@Bean名字

此豆的名称,如果是复数,则为该豆的别名。如果未指定,则 Bean 的名称是带注释的方法的名称。如果指定,则忽略方法名称。

<bean>具有表示 Bean 的唯一标识符的属性,并且可用于在 (XML) ID 中创建一个或多个非法别名。idname

@Component及其元注释具有价值

该值可能指示逻辑组件名称的建议,以便在自动检测到组件的情况下将其转换为Spring Bean。

如果未指定,则会为带注释的类型(通常是类型名称的驼峰大小写版本)自动生成 Bean 名称。例如,成为其豆名。Bean 名称区分大小写。另请注意,错误的名称/大小写通常发生在字符串 like 或 XML 配置文件引用的 Bean 中。MyClassNamemyClassName@DependsOn("my BeanName")

@Qualifier,如前所述,允许您向 Bean 添加更多别名。

确保在引用 Bean 时使用正确的名称。


更高级的案例

配置 文件

Bean 定义概要文件允许您有条件地注册 Bean。@Profile,具体来说,

指示当一个或多个指定的配置文件处于活动状态时,组件符合注册条件。

概要文件是一种命名的逻辑分组,可以通过 ConfigurableEnvironment.setActiveProfiles(java.lang.String...) 以编程方式激活,也可以通过将属性设置为 JVM 系统属性、环境变量或 Web 应用程序中的 Servlet 上下文参数来声明性地.xml Web 应用程序。配置文件也可以在集成测试中通过@ActiveProfiles注释以声明方式激活。spring.profiles.active

请考虑未设置属性的示例。spring.profiles.active

@Configuration
@ComponentScan
public class Example {
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Example.class);
        System.out.println(Arrays.toString(ctx.getEnvironment().getActiveProfiles()));
        System.out.println(ctx.getBean(Foo.class));
    }
}

@Profile(value = "StackOverflow")
@Component
class Foo {
}

这将显示没有活动的配置文件,并为bean抛出一个。由于配置文件未处于活动状态,因此未注册 Bean。NoSuchBeanDefinitionExceptionFooStackOverflow

相反,如果我在注册相应的配置文件时初始化ApplicationContext

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("StackOverflow");
ctx.register(Example.class);
ctx.refresh();

豆子已注册,可以返回/注入。

AOP 代理

Spring经常使用AOP代理来实现高级行为。一些示例包括:

为了实现这一目标,Spring有两种选择:

  1. 使用 JDK 的 Proxy 类在运行时创建动态类的实例,该实例仅实现 Bean 的接口,并将所有方法调用委托给实际的 Bean 实例。
  2. 使用 CGLIB 代理在运行时创建动态类的实例,该实例实现目标 Bean 的接口和具体类型,并将所有方法调用委托给实际的 Bean 实例。

以这个 JDK 代理为例(通过 的默认值@EnableAsyncproxyTargetClassfalse)

@Configuration
@EnableAsync
public class Example {
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Example.class);
        System.out.println(ctx.getBean(HttpClientImpl.class).getClass());
    }
}

interface HttpClient {
    void doGetAsync();
}

@Component
class HttpClientImpl implements HttpClient {
    @Async
    public void doGetAsync() {
        System.out.println(Thread.currentThread());
    }
}

在这里,Spring试图找到一种我们希望找到的类型的豆子,因为该类型清楚地标注了。但是,相反,我们得到了一个例外HttpClientImpl@Component

Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: 
    No qualifying bean of type [com.example.HttpClientImpl] is defined

Spring包裹了豆子,并通过一个只实现的对象将其暴露出来。所以你可以用HttpClientImplProxyHttpClient

ctx.getBean(HttpClient.class) // returns a dynamic class: com.example.$Proxy33
// or
@Autowired private HttpClient httpClient;

始终建议对接口进行编程。当你不能的时候,你可以告诉Spring使用CGLIM代理。例如,使用@EnableAsync,可以将 proxyTargetClass 设置为 。类似的注释(等)具有相似的属性。XML 还将具有等效的配置选项。trueEnableTransactionManagement

ApplicationContext层次结构 - 春季 MVC

Spring允许您使用ConfigableApplicationContext#setParent(ApplicationContext)构建具有其他实例作为父实例的实例。子上下文将有权访问父上下文中的 Bean,但事实并非如此。这篇文章详细介绍了何时有用,特别是在Spring MVC中。ApplicationContextApplicationContext

在典型的Spring MVC应用程序中,定义两个上下文:一个用于整个应用程序(根),另一个专门用于DispatcherServlet(路由,处理程序方法,控制器)。您可以在此处获取更多详细信息:

在官方文档中也有很好的解释,在这里

Spring MVC 配置中的一个常见错误是在带有注释的类的根上下文中或在 XML 中声明 WebMVC 配置,但@Controller servlet 上下文中的 bean。由于根上下文无法到达 servlet 上下文以查找任何 Bean,因此不会注册任何处理程序,并且所有请求都失败,并显示 404。您不会看到 ,但效果是相同的。@EnableWebMvc@Configuration<mvc:annotation-driven />NoSuchBeanDefinitionException

确保您的bean在适当的上下文中注册,即。在那里,它们可以通过为WebMVC注册的bean(,,等)找到。最好的解决方案是正确分离豆类。负责路由和处理请求,因此所有相关的 Bean 都应进入其上下文。加载根上下文的 应该初始化应用程序其余部分所需的任何 Bean:服务、存储库等。HandlerMappingHandlerAdapterViewResolverExceptionResolverDispatcherServletContextLoaderListener

数组、集合和映射

一些已知类型的豆类由Spring以特殊方式处理。例如,如果您尝试将 数组注入到字段中MovieCatalog

@Autowired
private MovieCatalog[] movieCatalogs;

Spring会找到所有类型的豆子,将它们包装在一个数组中,然后注入该数组。这在讨论@Autowired的Spring文档中进行了描述。类似的行为也适用于 、 和注入目标。MovieCatalogSetListCollection

对于注射目标,如果键类型为 .,则 Spring 也会以这种方式运行。例如,如果您有MapString

@Autowired
private Map<String, MovieCatalog> movies;

Spring 将找到所有类型的 bean,并将它们作为值添加到 a 中,其中相应的键将是它们的 bean 名称。MovieCatalogMap

如前所述,如果没有请求类型的豆子可用,则Spring将抛出一个.但是,有时,您只想声明这些集合类型的bean,例如NoSuchBeanDefinitionException

@Bean
public List<Foo> fooList() {
    return Arrays.asList(new Foo());
}

并注射它们

@Autowired
private List<Foo> foos;

在这个例子中,Spring会失败,因为你的上下文中没有豆子。但你不想要一颗豆子,你想要一颗豆子。在 Spring 4.3 之前,您必须使用@ResourceNoSuchBeanDefinitionExceptionFooFooList<Foo>

对于本身定义为集合/映射或数组类型的 Bean,是一个很好的解决方案,通过唯一名称引用特定的集合或数组 Bean。也就是说,从4.3开始,集合/映射和数组类型也可以通过Spring的类型匹配算法进行匹配,只要元素类型信息保留在返回类型签名或集合继承层次结构中即可。在这种情况下,可以使用限定符值在相同类型的集合中进行选择,如上一段所述。@Resource@Autowired@Bean

这适用于构造函数、setter 和字段注入。

@Resource
private List<Foo> foos;
// or since 4.3
public Example(@Autowired List<Foo> foos) {}

但是,对于方法,它将失败,即。@Bean

@Bean
public Bar other(List<Foo> foos) {
    new Bar(foos);
}

在这里,Spring 会忽略任何方法或对方法进行注释,因为它是一个方法,因此无法应用文档中描述的行为。但是,您可以使用Spring Expression Language(SpEL)通过其名称来指代bean。在上面的示例中,您可以使用@Resource@Autowired@Bean

@Bean
public Bar other(@Value("#{fooList}") List<Foo> foos) {
    new Bar(foos);
}

引用命名的豆子并注入它。fooList


答案 2

推荐