什么是NoSuchBeanDefinitionException并如何解决它?

73

请解释Spring中NoSuchBeanDefinitionException异常的以下内容:

  • 它是什么意思?
  • 在什么条件下会抛出该异常?
  • 如何避免它?

本文章旨在全面介绍使用Spring框架的应用程序中出现NoSuchBeanDefinitionException的相关问题。

1个回答

130

NoSuchBeanDefinitionException的{{javadoc文档}}解释了:

BeanFactory被要求提供一个无法找到定义的bean实例时抛出异常。这可能指向一个不存在的bean,一个不唯一的bean,或一个手动注册的没有关联bean定义的单例实例。

BeanFactory基本上是表示Spring的控制反转容器的抽象。它在应用程序内部和外部公开bean。当它无法找到或检索这些bean时,它会抛出NoSuchBeanDefinitionException异常。

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


该 bean 不存在,它未被注册

在下面的示例中

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

class Foo {}   

我们还没有通过@Bean方法、@Component扫描、XML定义或其他方式为类型Foo注册Bean定义。由AnnotationConfigApplicationContext管理的BeanFactory因此没有指示从哪里获取getBean(Foo.class)请求的bean。上面的片段会抛出异常。
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 { }

这里通过@ComponentScan注册了一个Foo的bean定义。但是Spring并不知道Bar。因此,在尝试自动装配Foo bean实例的bar字段时,它无法找到相应的bean。它会抛出(嵌套在UnsatisfiedDependencyException中)

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定义。

  • @Configuration类中的@Bean方法或XML配置中的<bean>
  • @Component(和其元注释,例如@Repository)通过@ComponentScan或XML中的<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 />

这样的配置将注册两个类型为Foo的bean,一个名为foo,另一个名为eg-different-name。确保您不会意外地注册更多的bean。这就引出了下面的问题...
如果您同时使用XML和注解配置,请确保从其中一个导入另一个。XML提供了...
<import resource=""/>

Java提供了@ImportResource注释。

期望找到一个匹配的bean,但实际找到了2个或更多

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

@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方法注册的两个bean都满足BeanFactory#getBean(Class)的要求,即它们都实现了DataSource接口。在此示例中,Spring没有区分或优先处理这两个bean的机制。但是,这样的机制是存在的。
您可以使用@Primary(以及其在XML中的等效方式),如文档这篇文章所述。通过这种更改。
@Bean(name = "mysql")
@Primary
public DataSource mysql() { return new MySQL(); } 

前面的代码片段不会抛出异常,而是返回mysql bean。

您还可以使用@Qualifier(以及其在XML中的等效项)来更好地控制bean选择过程,如文档所述。虽然@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的方式一样,也有多种命名它们的方式。

@Beanname

这个bean的名称,或者如果是复数,这个bean的别名。如果未指定,则bean的名称是带注释的方法的名称。如果指定,则忽略方法名称。

<bean>id 属性来表示 bean的唯一标识符,而 name 可以用于创建一个或多个别名,但在(XML)id中是非法的。

@Component和它的元注解value

该值可能表示逻辑组件名称的建议,如果自动检测到组件,则将其转换为Spring Bean。

如果未指定,自动为注释类型生成Bean名称,通常是类型名称的小驼峰版本。例如,MyClassName变成myClassName作为其Bean名称。Bean名称区分大小写。还要注意,通过字符串引用@DependsOn("my BeanName")或XML配置文件引用的Bean名称/大写错误通常会发生。

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

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


更高级的案例

配置文件

Bean定义配置文件 允许您有条件地注册bean。 @Profile , 特别是,

表示当一个或多个指定的配置文件处于激活状态时,组件就有资格进行注册。
配置文件是一种命名的逻辑分组,可以通过编程方式通过ConfigurableEnvironment.setActiveProfiles(java.lang.String...)激活,也可以通过在JVM系统属性、环境变量或web.xml中作为Servlet上下文参数设置spring.profiles.active属性来声明性地激活。在集成测试中,配置文件还可以通过@ActiveProfiles注释声明性地激活。
考虑以下示例,其中未设置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 {
}

这将显示没有活动配置文件,并为 Foo bean 抛出 NoSuchBeanDefinitionException 异常。由于未激活 StackOverflow 配置文件,因此该 bean 未注册。
相反,如果在注册相应的配置文件时初始化 ApplicationContext
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("StackOverflow");
ctx.register(Example.class);
ctx.refresh();

这个bean已经被注册,可以被返回或注入。

AOP代理

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

为了实现这一点,Spring 有两个选择:
  1. 使用 JDK 的 Proxy 类在运行时创建一个动态类的实例,该实例仅实现您的 bean 接口并将所有方法调用委托给实际的 bean 实例。
  2. 使用 CGLIB 代理,在运行时创建一个动态类的实例,该实例实现目标 bean 的接口和具体类型,并将所有方法调用委托给实际的 bean 实例。

以下是 JDK 代理的示例(通过 @EnableAsync 的默认 proxyTargetClassfalse 实现)

@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的bean,我们期望能够找到,因为该类型已经明确地使用@Component进行了注释。然而,我们却得到了一个异常。
Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: 
    No qualifying bean of type [com.example.HttpClientImpl] is defined

Spring将HttpClientImpl bean封装并通过一个仅实现了HttpClient接口的Proxy对象进行暴露。因此,您可以使用以下方式检索它:

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

建议始终面向接口编程。当你无法这样做时,你可以告诉Spring使用CGLIB代理。例如,使用@EnableAsync,你可以将proxyTargetClass设置为true。类似的注释(EnableTransactionManagement等)也有类似的属性。XML也有相应的配置选项。

ApplicationContext层次结构 - Spring MVC

Spring可以使用ConfigurableApplicationContext#setParent(ApplicationContext)方法生成具有父级ApplicationContext的ApplicationContext。子Context将能够访问父Context中的Bean,但反之则不行。这篇文章详细介绍了何时使用此功能,特别是在Spring MVC中。
在典型的Spring MVC应用程序中,您需要定义两个上下文:一个为整个应用程序(根)和一个专门为DispatcherServlet(路由、处理方法、控制器)定义的。您可以在这里获取更多细节:

官方文档这里也有详细的解释。

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

请确保您的bean已在适当的上下文中注册,即可以被WebMVC注册的bean(HandlerMapping、HandlerAdapter、ViewResolver、ExceptionResolver等)找到。最好的解决方案是正确地隔离bean。DispatcherServlet负责路由和处理请求,因此所有相关的bean都应该放在它的上下文中。ContextLoaderListener负责加载根上下文,应该初始化应用程序其余部分需要的任何bean:服务、存储库等。

数组、集合和映射

Spring以特殊的方式处理某些已知类型的bean。例如,如果您尝试将一个MovieCatalog数组注入到一个字段中,则Spring会自动创建一个包含所有MovieCatalog bean的数组。
@Autowired
private MovieCatalog[] movieCatalogs;

Spring会查找所有类型为MovieCatalog的bean,将它们封装在一个数组中,并注入该数组。这在Spring文档中讨论的@Autowired中有描述。类似的行为也适用于SetListCollection注入目标。
对于Map注入目标,如果键类型是String,Spring也会以同样的方式处理。例如,如果你有:
@Autowired
private Map<String, MovieCatalog> movies;

Spring会查找所有类型为MovieCatalog的bean,并将它们作为值添加到一个Map中,相应的键将是它们的bean名称。
正如先前所述,如果没有请求类型的bean可用,Spring会抛出NoSuchBeanDefinitionException异常。然而,有时你只想声明这些集合类型的bean。
@Bean
public List<Foo> fooList() {
    return Arrays.asList(new Foo());
}

并将它们注入

@Autowired
private List<Foo> foos;

在这个例子中,Spring 会因为上下文中没有 Foo 类型的 bean 而出现 NoSuchBeanDefinitionException 异常。但是你需要的不是一个 Foo 类型的 bean,而是一个 List<Foo> 类型的 bean。在 Spring 4.3 之前,你需要使用 @Resource 注解

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

这适用于构造函数、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忽略了任何对方法进行 @Resource@Autowired 注解的操作,因为它是一个 @Bean 方法,所以无法应用文档中描述的行为。但是,您可以使用Spring表达式语言(SpEL)按名称引用bean。在上面的示例中,您可以这样做。
@Bean
public Bar other(@Value("#{fooList}") List<Foo> foos) {
    new Bar(foos);
}

引用名为fooList的bean并注入它。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接