Bean 概览 (Bean Overview)
Spring IoC 容器管理着一个或多个 Bean。这些 Bean 是根据你提供给容器的配置元数据(例如,以 XML <bean/> 定义的形式)创建的。
在容器内部,这些 Bean 定义被表示为 BeanDefinition 对象,其中包含(除其他信息外)以下元数据:
- 全限定类名:通常是所定义 Bean 的实际实现类。
- Bean 行为配置元素:说明 Bean 在容器中的行为方式(作用域、生命周期回调等)。
- 对其他 Bean 的引用:Bean 执行工作所需的依赖项。这些引用也称为协作对象(Collaborators)或依赖项(Dependencies)。
- 其他配置设置:例如,在管理连接池的 Bean 中,设置池的大小限制或使用的连接数。
这些元数据映射为组成每个 Bean 定义的一组属性。下表描述了这些属性:
| 属性 | 说明位置 |
|---|---|
| Class (类) | 实例化 Bean |
| Name (名称) | Bean 的命名 |
| Scope (作用域) | Bean 作用域 |
| Constructor arguments (构造函数参数) | 依赖注入 |
| Properties (属性) | 依赖注入 |
| Autowiring mode (自动装配模式) | 自动装配协作对象 |
| Lazy initialization mode (延迟初始化模式) | 延迟初始化的 Bean |
| Initialization method (初始化方法) | 初始化回调 |
| Destruction method (销毁方法) | 销毁回调 |
除了包含如何创建特定 Bean 信息的 Bean 定义之外,ApplicationContext 的实现还允许注册由用户在容器外部创建的现有对象。这是通过 getAutowireCapableBeanFactory() 方法访问 ApplicationContext 的 BeanFactory 来实现的,该方法返回 DefaultListableBeanFactory 实现。DefaultListableBeanFactory 通过 registerSingleton(..) 和 registerBeanDefinition(..) 方法支持此类注册。然而,典型的应用程序通常只使用通过常规 Bean 定义元数据定义的 Bean。
注意:Bean 元数据和手动提供的单例实例需要尽可能早地注册,以便容器在自动装配和其他内省步骤期间能够正确推断它们。虽然在某种程度上支持覆盖现有元数据和现有单例实例,但在运行时注册新 Bean(与对工厂的并发访问同时进行)并未得到官方支持,可能会导致并发访问异常、Bean 容器状态不一致,或两者兼而有之。
1. Bean 覆盖 (Overriding Beans)
当使用已经分配的标识符注册 Bean 时,就会发生 Bean 覆盖。虽然 Bean 覆盖是可能的,但它会使配置难以阅读。
警告:Bean 覆盖将在未来的版本中被弃用。
要完全禁用 Bean 覆盖,可以在刷新 ApplicationContext 之前将其 allowBeanDefinitionOverriding 标志设置为 false。在这种设置下,如果使用 Bean 覆盖,将抛出异常。
默认情况下,容器会以 INFO 级别记录每次覆盖 Bean 的尝试,以便你可以相应地调整配置。虽然不推荐,但你可以通过将 allowBeanDefinitionOverriding 标志设置为 true 来静默这些日志。
Java 配置
如果你使用 Java 配置,只要 @Bean 方法的返回类型与该 Bean 类匹配,相应的 @Bean 方法总是会静默覆盖具有相同组件名称的扫描 Bean 类。这仅仅意味着容器将调用 @Bean 工厂方法,而不是 Bean 类上任何预先声明的构造函数。
注意:我们承认在测试场景中覆盖 Bean 是很方便的,并且对此有明确的支持。请参考测试章节了解更多细节。
2. Bean 的命名 (Naming Beans)
每个 Bean 都有一个或多个标识符。这些标识符在托管该 Bean 的容器内必须是唯一的。一个 Bean 通常只有一个标识符。但是,如果它需要多个标识符,则额外的标识符可以被视为别名(Aliases)。
在基于 XML 的配置元数据中,你可以使用 id 属性、name 属性或两者来指定 Bean 的标识符。id 属性允许你指定确切的一个 id。按照惯例,这些名称是字母数字('myBean'、'someService' 等),但它们也可以包含特殊字符。如果你想为 Bean 引入其他别名,也可以在 name 属性中指定,用逗号 (,)、分号 (;) 或空格分隔。尽管 id 属性被定义为 xsd:string 类型,但 Bean id 的唯一性是由容器强制执行的,而不是由 XML 解析器强制执行的。
你不必为 Bean 提供 name 或 id。如果你没有显式提供 name 或 id,容器会为该 Bean 生成一个唯一的名称。但是,如果你想通过 ref 元素或服务定位器风格的查找按名称引用该 Bean,则必须提供名称。不提供名称的情况通常与使用内部 Bean和自动装配协作对象有关。
Bean 命名约定
惯例是在命名 Bean 时使用实例字段名称的标准 Java 约定。也就是说,Bean 名称以小写字母开头,之后采用驼峰式命名。此类名称的示例包括 accountManager、accountService、userDao、loginController 等。
一致地命名 Bean 使你的配置更易于阅读和理解。此外,如果你使用 Spring AOP,在为一组按名称关联的 Bean 应用通知(Advice)时,这会很有帮助。
注意:通过类路径中的组件扫描,Spring 会按照前面描述的规则为未命名的组件生成 Bean 名称:基本上是获取简单的类名并将其首字母转为小写。但是,在(不常见的)特殊情况下,如果第一个和第二个字符都是大写,则保留原始大小写。这与
java.beans.Introspector.decapitalize定义的规则相同(Spring 在此处使用了该规则)。
在 Bean 定义之外为 Bean 定义别名
在 Bean 定义本身中,你可以通过组合使用 id 属性指定的一个名称和 name 属性中任意数量的其他名称,为 Bean 提供多个名称。这些名称可以是同一个 Bean 的等效别名,在某些情况下很有用,例如让应用程序中的每个组件通过使用该组件特有的 Bean 名称来引用公共依赖项。
然而,在实际定义 Bean 的地方指定所有别名并不总是足够的。有时希望为在别处定义的 Bean 引入别名。这在大型系统中很常见,其中配置分散在每个子系统中,每个子系统都有自己的一组对象定义。在基于 XML 的配置元数据中,你可以使用 <alias/> 元素来完成此操作:
<alias name="fromName" alias="toName"/>在这种情况下,名为 fromName 的 Bean(在同一个容器中)在使用此别名定义后,也可以被称为 toName。
例如,子系统 A 的配置元数据可能会以 subsystemA-dataSource 的名称引用 DataSource。子系统 B 的配置元数据可能会以 subsystemB-dataSource 的名称引用 DataSource。当组合使用这两个子系统的住主应用程序时,主应用程序以 myApp-dataSource 的名称引用 DataSource。为了让这三个名称都指向同一个对象,你可以将以下别名定义添加到配置元数据中:
<alias name="myApp-dataSource" alias="subsystemA-dataSource"/>
<alias name="myApp-dataSource" alias="subsystemB-dataSource"/>现在,每个组件和主应用程序都可以通过一个唯一的且保证不会与任何其他定义冲突的名称(有效地创建了一个命名空间)来引用 dataSource,并且它们引用的是同一个 Bean。
Java 配置
如果你使用 Java 配置,@Bean 注解可以用来提供别名。详见使用 @Bean 注解。
3. 实例化 Bean (Instantiating Beans)
Bean 定义本质上是创建一个或多个对象的配方。当被询问时,容器会查看命名 Bean 的配方,并使用该 Bean 定义封装的配置元数据来创建(或获取)实际的对象。
如果你使用基于 XML 的配置元数据,你可以在 <bean/> 元素的 class 属性中指定要实例化的对象类型(或类)。这个 class 属性(在内部是 BeanDefinition 实例上的 Class 属性)通常是强制性的。(有关例外情况,请参阅通过实例工厂方法实例化和 Bean 定义继承。)
你可以通过以下两种方式之一使用 Class 属性:
- 通常,在容器本身通过反射调用构造函数直接创建 Bean 的情况下,指定要构造的 Bean 类,这在某种程度上等同于使用
new运算符的 Java 代码。 - 在容器调用类上的
static工厂方法来创建 Bean 的较不常见情况下,指定包含被调用的static工厂方法的实际类。从调用static工厂方法返回的对象类型可能是同一个类,也可能是完全不同的另一个类。
内部类名称
如果你想为嵌套类(Nested class)配置 Bean 定义,可以使用嵌套类的二进制名称(Binary name)或源名称(Source name)。
例如,如果在 com.example 包中有一个名为 SomeThing 的类,并且这个 SomeThing 类有一个名为 OtherThing 的 static 嵌套类,它们可以用美元符号 ($) 或点 (.) 分隔。因此,Bean 定义中 class 属性的值应该是 com.example.SomeThing$OtherThing 或 com.example.SomeThing.OtherThing。
使用构造函数实例化
当你采用构造函数方法创建 Bean 时,所有普通类都可以被 Spring 使用并与之兼容。也就是说,正在开发的类不需要实现任何特定的接口或以特定的方式编写代码。只需指定 Bean 类就足够了。但是,根据你为该特定 Bean 使用哪种类型的 IoC,你可能需要一个默认(空)构造函数。
Spring IoC 容器几乎可以管理你希望它管理的任何类。它不限于管理真正的 JavaBean。大多数 Spring 用户更喜欢实际的 JavaBean,其仅具有默认(无参数)构造函数,并具有根据容器中属性建模的适当 setter 和 getter。你也可以在容器中拥有更奇特的非 Bean 风格的类。例如,如果你需要使用一个绝对不符合 JavaBean 规范的遗留连接池,Spring 也可以管理它。
在基于 XML 的配置元数据中,你可以按如下方式指定 Bean 类:
<bean id="exampleBean" class="examples.ExampleBean"/>
<bean name="anotherExample" class="examples.ExampleBeanTwo"/>有关向构造函数提供参数(如果需要)以及在对象构造完成后设置对象实例属性的机制的详细信息,请参阅注入依赖项。
注意:在构造函数参数的情况下,容器可以在多个重载的构造函数中选择一个对应的构造函数。话虽如此,为了避免歧义,建议让你的构造函数签名尽可能简单。
使用静态工厂方法实例化
定义使用静态工厂方法创建的 Bean 时,请使用 class 属性指定包含 static 工厂方法的类,并使用名为 factory-method 的属性指定工厂方法本身的名称。你应该能够调用此方法(带有可选参数,稍后描述)并返回一个活动对象,该对象随后被视为是通过构造函数创建的。此类 Bean 定义的一种用途是在遗留代码中调用 static 工厂。
以下 Bean 定义指定将通过调用工厂方法来创建 Bean。该定义不指定返回对象的类型(类),而是指定包含工厂方法的类。在此示例中,createInstance() 方法必须是 static 方法。以下示例展示了如何指定工厂方法:
<bean id="clientService"
class="examples.ClientService"
factory-method="createInstance"/>以下示例展示了可与上述 Bean 定义配合使用的类:
public class ClientService {
private static ClientService clientService = new ClientService();
private ClientService() {}
public static ClientService createInstance() {
return clientService;
}
}class ClientService private constructor() {
companion object {
private val clientService = ClientService()
@JvmStatic
fun createInstance() = clientService
}
}有关向工厂方法提供(可选)参数以及在从工厂返回后设置对象实例属性的机制的详细信息,请参阅依赖项和配置详解。
注意:在工厂方法参数的情况下,容器可以在多个同名的重载方法中选择一个对应的方法。话虽如此,为了避免歧义,建议让你的工厂方法签名尽可能简单。
技巧
工厂方法重载的一个典型问题案例是 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>通过实例工厂方法实例化
与通过静态工厂方法实例化类似,通过实例工厂方法实例化会调用容器中现有 Bean 的非静态方法来创建新 Bean。要使用此机制,请将 class 属性留空,并在 factory-bean 属性中指定当前(或父级或祖先)容器中包含要调用以创建对象的实例方法的 Bean 名称。使用 factory-method 属性设置工厂方法本身的名称。以下示例展示了如何配置此类 Bean:
<!-- 工厂 bean,包含名为 createClientServiceInstance() 的方法 -->
<bean id="serviceLocator" class="examples.DefaultServiceLocator">
<!-- 注入此 locator bean 所需的任何依赖项 -->
</bean>
<!-- 通过工厂 bean 创建的 bean -->
<bean id="clientService"
factory-bean="serviceLocator"
factory-method="createClientServiceInstance"/>以下示例展示了对应的类:
public class DefaultServiceLocator {
private static ClientService clientService = new ClientServiceImpl();
public ClientService createClientServiceInstance() {
return clientService;
}
}class DefaultServiceLocator {
companion object {
private val clientService = ClientServiceImpl()
}
fun createClientServiceInstance(): ClientService {
return clientService
}
}一个工厂类也可以包含多个工厂方法,如下例所示:
<bean id="serviceLocator" class="examples.DefaultServiceLocator">
<!-- 注入此 locator bean 所需的任何依赖项 -->
</bean>
<bean id="clientService"
factory-bean="serviceLocator"
factory-method="createClientServiceInstance"/>
<bean id="accountService"
factory-bean="serviceLocator"
factory-method="createAccountServiceInstance"/>对应的类:
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;
}
}class DefaultServiceLocator {
companion object {
private val clientService = ClientServiceImpl()
private val accountService = AccountServiceImpl()
}
fun createClientServiceInstance(): ClientService {
return clientService
}
fun createAccountServiceInstance(): AccountService {
return accountService
}
}这种方法表明,工厂 Bean 本身也可以通过依赖注入 (DI) 进行管理和配置。详见依赖项和配置详解。
注意:在 Spring 文档中,“工厂 Bean (factory bean)”是指在 Spring 容器中配置的、通过实例或静态工厂方法创建对象的 Bean。相比之下,
FactoryBean(注意大写)是指 Spring 特有的FactoryBean实现类。
4. 确定 Bean 的运行时类型
确定特定 Bean 的运行时类型并非易事。Bean 元数据定义中指定的类只是一个初始类引用,可能与声明的工厂方法结合使用,或者是一个 FactoryBean 类,这可能导致 Bean 的运行时类型不同,或者在实例级工厂方法的情况下根本不设置(在这种情况下,通过指定的 factory-bean 名称解析)。此外,AOP 代理可能会用基于接口的代理包装 Bean 实例,从而限制了目标 Bean 实际类型的暴露(仅由于其实现的接口)。
找出特定 Bean 实际运行时类型的推荐方法是针对指定的 Bean 名称调用 BeanFactory.getType。这考虑了上述所有情况,并返回 BeanFactory.getBean 调用针对相同 Bean 名称将返回的对象类型。
补充教学 —— 走进 Bean 的内部世界
1. BeanDefinition:Bean 的“基因图谱” 在 Spring 内部,你写的 XML 或注解最终都会变成一个 BeanDefinition 对象。
- 它存了类的名字、是否是单例、依赖谁。
- 理解点:Spring 容器在启动时,第一步是读取配置并创建这些“图谱”,第二步才是根据图谱去实例化真正的对象。
2. 命名约定的力量 虽然你可以给 Bean 起任何名字,但强烈建议遵循 类名首字母小写 的规范(如 userService)。
- 为什么?因为当你使用
@Autowired进行按名称匹配时,Spring 默认寻找的就是这个名字。遵循规范可以减少很多不必要的配置。
3. id vs name:到底用哪个?
id:每个 Bean 只能有一个,通常是唯一的标识符。name:可以起多个,支持逗号或分号分隔,适合起“别名”。- 别名 (Alias) 的妙用:在大型项目中,如果你引用了第三方库的一个 Bean,但你想用一个符合自己项目语境的名字来称呼它,别名就派上用场了。
4. 实例化三板斧:Constructor vs Static Factory vs Instance Factory
- Constructor (最常用):直接
new,最符合 Java 直觉。 - Static Factory:不需要创建工厂对象就能拿产品。常用于集成一些遗留代码的工具类。
- Instance Factory:需要先有一个工厂 Bean,再从工厂 Bean 里“生”出产品。这在需要复杂的初始化逻辑或依赖注入时非常有用。
5. 区分 factory-bean 与 FactoryBean (敲黑板!) 这是面试常考题,也是最容易混淆的概念:
factory-bean属性:是配置在 XML 里的一个属性,指通过另一个 Bean 的方法来创建当前 Bean。FactoryBean接口:是一个 Spring 定义的接口。如果一个 Bean 实现了这个接口,那么getBean()拿到的不是这个 Bean 本身,而是它里面生成的对象。它是 Spring 提供的一种高度灵活的扩展点。