跳到主要内容

Bean 概述

ChatGPT-4o-mini 中英对照 Bean Overview

一个 Spring IoC 容器管理一个或多个 bean。这些 bean 是根据您提供给容器的配置元数据创建的(例如,以 XML <bean/> 定义的形式)。

在容器内部,这些 bean 定义被表示为 BeanDefinition 对象,这些对象包含(除了其他信息)以下元数据:

  • 一个包限定的类名:通常是被定义的 bean 的实际实现类。

  • Bean 行为配置元素,说明 bean 在容器中的行为(作用域、生命周期回调等)。

  • 其他 bean 的引用,这些引用是 bean 执行其工作的必要条件。这些引用也称为协作对象或依赖项。

  • 在新创建的对象中设置的其他配置选项 — 例如,连接池的大小限制或用于管理连接池的 bean 的连接数量。

这些元数据转换为一组属性,这些属性构成了每个 bean 定义。以下表格描述了这些属性:

表 1. Bean 定义

属性解释在…​
实例化 Beans
名称命名 Beans
范围Bean 范围
构造函数参数依赖注入
属性依赖注入
自动装配模式自动装配协作对象
延迟初始化模式延迟初始化的 Beans
初始化方法初始化回调
销毁方法销毁回调

除了包含有关如何创建特定 bean 的信息的 bean 定义外,ApplicationContext 实现还允许注册在容器外部(由用户)创建的现有对象。这是通过访问 ApplicationContext 的 BeanFactory 来完成的,方法是使用 getAutowireCapableBeanFactory() 方法,该方法返回 DefaultListableBeanFactory 实现。DefaultListableBeanFactory 通过 registerSingleton(..)registerBeanDefinition(..) 方法支持此注册。然而,典型的应用程序仅使用通过常规 bean 定义元数据定义的 beans。

备注

Bean 元数据和手动提供的单例实例需要尽早注册,以便容器在自动装配和其他反射步骤中能够正确处理它们。虽然在一定程度上支持覆盖现有的元数据和现有的单例实例,但在运行时注册新 bean(与对工厂的实时访问并发进行)并不被正式支持,可能会导致并发访问异常、bean 容器中的不一致状态,或者两者兼而有之。

重写 Beans

Bean 覆盖发生在使用已经分配的标识符注册 bean 时。虽然 bean 覆盖是可能的,但它使配置变得更难以阅读。

注意

Bean 覆盖将在未来的版本中被弃用。

要完全禁用 bean 覆盖,可以在 ApplicationContext 刷新之前将 allowBeanDefinitionOverriding 标志设置为 false。在这种设置中,如果使用了 bean 覆盖,将会抛出异常。

默认情况下,容器会以 INFO 级别记录每次尝试覆盖 bean 的日志,以便您可以相应地调整配置。虽然不推荐这样做,但您可以通过将 allowBeanDefinitionOverriding 标志设置为 true 来静默这些日志。

Java 配置

如果您使用 Java 配置,只要 @Bean 方法的返回类型与扫描到的具有相同组件名称的 bean 类匹配,对应的 @Bean 方法将始终默默覆盖该 bean 类。这意味着容器将调用 @Bean 工厂方法,而不是 bean 类上任何预先声明的构造函数。

备注

我们承认在测试场景中覆盖 beans 是方便的,并且从 Spring Framework 6.2 开始对此提供了明确支持。有关更多详细信息,请参阅 这一部分

命名 Bean

每个 bean 都有一个或多个标识符。这些标识符在托管 bean 的容器中必须是唯一的。一个 bean 通常只有一个标识符。然而,如果它需要多个标识符,额外的标识符可以被视为别名。

在基于 XML 的配置元数据中,您可以使用 id 属性、name 属性或两者来指定 bean 标识符。id 属性允许您指定一个确切的 id。通常,这些名称是字母数字的(例如 'myBean'、'someService' 等),但它们也可以包含特殊字符。如果您想为 bean 引入其他别名,您还可以在 name 属性中指定它们,用逗号(,)、分号(;)或空格分隔。尽管 id 属性被定义为 xsd:string 类型,但 bean id 的唯一性由容器强制执行,而不是由 XML 解析器。

您不需要为一个 bean 提供 nameid。如果您没有明确提供 nameid,容器会为该 bean 生成一个唯一的名称。然而,如果您想通过 ref 元素或服务定位器风格的查找来按名称引用该 bean,您必须提供一个名称。不提供名称的动机与使用 内嵌 bean自动装配协作对象 相关。

Bean 命名约定

约定是使用标准的 Java 命名约定来命名 bean 的实例字段。也就是说,bean 名称以小写字母开头,并从那里使用驼峰命名法。这样的名称示例包括 accountManageraccountServiceuserDaologinController 等等。

一致地命名 bean 使您的配置更易于阅读和理解。此外,如果您使用 Spring AOP,这在对一组通过名称相关的 bean 应用建议时也会有很大帮助。

备注

在类路径中进行组件扫描时,Spring 为未命名的组件生成 bean 名称,遵循之前描述的规则:基本上是将简单类名的首字母转换为小写。然而,在(不寻常的)特殊情况下,当字符数超过一个且第一个和第二个字符都是大写时,原始的大小写将被保留。这些规则与 java.beans.Introspector.decapitalize 定义的规则相同(Spring 在这里使用了这个规则)。

在 Bean 定义之外为 Bean 起别名

在 bean 定义中,您可以为 bean 提供多个名称,方法是使用 id 属性指定的最多一个名称和 name 属性中的任意数量的其他名称的组合。这些名称可以是同一 bean 的等效别名,并且在某些情况下非常有用,例如让应用程序中的每个组件通过使用特定于该组件的 bean 名称来引用一个公共依赖项。

指定所有实际定义 bean 的别名并不总是足够的。有时希望为在其他地方定义的 bean 引入一个别名。这在大型系统中是常见的情况,其中配置分散在每个子系统之间,每个子系统都有自己的一组对象定义。在基于 XML 的配置元数据中,您可以使用 <alias/> 元素来实现这一点。以下示例展示了如何做到这一点:

<alias name="fromName" alias="toName"/>
xml

在这种情况下,名为 fromName 的 bean(在同一个容器中)在使用此别名定义后,也可以被称为 toName

例如,子系统 A 的配置元数据可能通过名称 subsystemA-dataSource 引用一个数据源。子系统 B 的配置元数据可能通过名称 subsystemB-dataSource 引用一个数据源。当组合使用这两个子系统的主应用程序时,主应用程序通过名称 myApp-dataSource 引用数据源。为了使这三个名称都指向同一个对象,您可以将以下别名定义添加到配置元数据中:

<alias name="myApp-dataSource" alias="subsystemA-dataSource"/>
<alias name="myApp-dataSource" alias="subsystemB-dataSource"/>
xml

现在每个组件和主应用程序可以通过一个唯一的名称引用 dataSource,该名称保证不会与任何其他定义冲突(有效地创建了一个命名空间),但它们引用的是同一个 bean。

Java 配置

如果您使用 Java 配置,可以使用 @Bean 注解来提供别名。有关详细信息,请参见 使用 @Bean 注解

实例化 Beans

一个 bean 定义本质上是创建一个或多个对象的配方。当容器被请求时,它会查看一个命名 bean 的配方,并使用该 bean 定义封装的配置元数据来创建(或获取)一个实际对象。

如果您使用基于 XML 的配置元数据,您需要在 <bean/> 元素的 class 属性中指定要实例化的对象的类型(或类)。这个 class 属性(在内部,它是 BeanDefinition 实例上的一个 Class 属性)通常是必需的。(有关例外,请参见 使用实例工厂方法进行实例化Bean 定义继承。)您可以通过以下两种方式使用 Class 属性:

  • 通常,在容器直接通过反射调用构造函数创建 bean 的情况下,指定要构造的 bean 类,这在某种程度上等同于使用 new 操作符的 Java 代码。

  • 指定实际包含用于创建对象的 static 工厂方法的类,在容器调用类上的 static 工厂方法以创建 bean 的不太常见的情况下。调用 static 工厂方法返回的对象类型可以是相同的类,也可以是完全不同的类。

嵌套类名称

如果您想为嵌套类配置一个 bean 定义,可以使用嵌套类的二进制名称或源名称。

例如,如果您在 com.example 包中有一个名为 SomeThing 的类,并且这个 SomeThing 类有一个 static 嵌套类叫做 OtherThing,它们可以通过美元符号 ($) 或点 (.) 来分隔。因此,bean 定义中 class 属性的值可以是 com.example.SomeThing$OtherThingcom.example.SomeThing.OtherThing

使用构造函数进行实例化

当你通过构造函数方式创建一个 bean 时,所有普通类都可以被 Spring 使用并且与之兼容。也就是说,正在开发的类不需要实现任何特定的接口或以特定的方式编码。只需指定 bean 类即可。然而,根据你为该特定 bean 使用的 IoC 类型,你可能需要一个默认(空)构造函数。

Spring IoC 容器几乎可以管理您希望它管理的任何类。它并不局限于管理真正的 JavaBeans。大多数 Spring 用户更喜欢实际的 JavaBeans,这些 JavaBeans 只有一个默认的(无参数)构造函数,以及根据容器中的属性建模的适当的 setter 和 getter。您也可以在容器中拥有更奇特的非 Bean 风格的类。例如,如果您需要使用一个绝对不符合 JavaBean 规范的遗留连接池,Spring 也可以管理它。

使用基于 XML 的配置元数据,您可以如下指定您的 bean 类:

<bean id="exampleBean" class="examples.ExampleBean"/>

<bean name="anotherExample" class="examples.ExampleBeanTwo"/>
xml

有关向构造函数提供参数(如有必要)和在对象构造后设置对象实例属性的机制的详细信息,请参见 Injecting Dependencies

备注

在构造函数参数的情况下,容器可以在多个重载的构造函数中选择一个相应的构造函数。也就是说,为了避免歧义,建议尽可能保持构造函数签名的简单明了。

使用静态工厂方法实例化

当定义一个通过静态工厂方法创建的 bean 时,使用 class 属性来指定包含 static 工厂方法的类,并使用名为 factory-method 的属性来指定工厂方法的名称。您应该能够调用此方法(带有可选参数,如后面所述)并返回一个活动对象,该对象随后被视为通过构造函数创建的对象。这种 bean 定义的一种用途是在遗留代码中调用 static 工厂。

以下 bean 定义指定将通过调用工厂方法来创建 bean。该定义没有指定返回对象的类型(类),而是指定包含工厂方法的类。在这个例子中,createInstance() 方法必须是一个 static 方法。以下示例展示了如何指定一个工厂方法:

<bean id="clientService"
class="examples.ClientService"
factory-method="createInstance"/>
xml

以下示例展示了一个可以与前面的 bean 定义一起使用的类:

public class ClientService {
private static ClientService clientService = new ClientService();
private ClientService() {}

public static ClientService createInstance() {
return clientService;
}
}
java

有关向工厂方法提供(可选)参数的机制以及在对象从工厂返回后设置对象实例属性的详细信息,请参见 Dependencies and Configuration in Detail

备注

在工厂方法参数的情况下,容器可以在同名的多个重载方法中选择一个对应的方法。也就是说,为了避免歧义,建议尽可能保持工厂方法签名的简单明了。

提示

一个典型的问题案例是工厂方法重载,尤其是 Mockito 及其众多重载的 mock 方法。选择最具体的 mock 变体:

<bean id="clientService" class="org.mockito.Mockito" factory-method="mock">
<constructor-arg type="java.lang.Class" value="examples.ClientService"/>
<constructor-arg type="java.lang.String" value="clientService"/>
</bean>
xml

使用实例工厂方法进行实例化

类似于通过 静态工厂方法 实例化,使用实例工厂方法的实例化调用来自容器的现有 bean 的非静态方法来创建一个新 bean。要使用此机制,请将 class 属性留空,并在 factory-bean 属性中指定当前(或父级或祖先)容器中包含要调用以创建对象的实例方法的 bean 名称。使用 factory-method 属性设置工厂方法本身的名称。以下示例展示了如何配置这样的 bean:

<!-- the factory bean, which contains a method called createClientServiceInstance() -->
<bean id="serviceLocator" class="examples.DefaultServiceLocator">
<!-- inject any dependencies required by this locator bean -->
</bean>

<!-- the bean to be created via the factory bean -->
<bean id="clientService"
factory-bean="serviceLocator"
factory-method="createClientServiceInstance"/>
xml

以下示例显示了相应的类:

public class DefaultServiceLocator {

private static ClientService clientService = new ClientServiceImpl();

public ClientService createClientServiceInstance() {
return clientService;
}
}
java

一个工厂类也可以包含多个工厂方法,如下例所示:

<bean id="serviceLocator" class="examples.DefaultServiceLocator">
<!-- inject any dependencies required by this locator bean -->
</bean>

<bean id="clientService"
factory-bean="serviceLocator"
factory-method="createClientServiceInstance"/>

<bean id="accountService"
factory-bean="serviceLocator"
factory-method="createAccountServiceInstance"/>
xml

以下示例显示了相应的类:

public class DefaultServiceLocator {

private static ClientService clientService = new ClientServiceImpl();

private static AccountService accountService = new AccountServiceImpl();

public ClientService createClientServiceInstance() {
return clientService;
}

public AccountService createAccountServiceInstance() {
return accountService;
}
}
java

这种方法表明,工厂 bean 本身可以通过依赖注入 (DI) 进行管理和配置。请参见 Dependencies and Configuration in Detail

备注

在 Spring 文档中,“工厂 bean”指的是在 Spring 容器中配置的 bean,通过 实例静态 工厂方法创建对象。相比之下,FactoryBean(注意大小写)指的是一个 Spring 特定的 FactoryBean 实现类。

确定 Bean 的运行时类型

特定 bean 的运行时类型是非平凡的。bean 元数据定义中指定的类只是一个初始类引用,可能与声明的工厂方法结合,或者是一个 FactoryBean 类,这可能导致 bean 的不同运行时类型,或者在实例级工厂方法的情况下根本没有设置(此时通过指定的 factory-bean 名称解析)。此外,AOP 代理可能会用基于接口的代理包装 bean 实例,从而限制目标 bean 实际类型的暴露(仅限于其实现的接口)。

查找特定 bean 的实际运行时类型的推荐方法是对指定 bean 名称进行 BeanFactory.getType 调用。这考虑了上述所有情况,并返回 BeanFactory.getBean 调用对于相同 bean 名称将返回的对象类型。