Skip to content

使用 @Bean 注解 (Using the @Bean Annotation)

@Bean 是一个方法级注解,是 XML <bean/> 元素的直接模拟。该注解支持 <bean/> 提供的一些属性,例如:

你可以在标注了 @Configuration@Component 的类中使用 @Bean 注解。

声明一个 Bean (Declaring a Bean)

要声明一个 Bean,你可以使用 @Bean 注解一个方法。你使用此方法在 ApplicationContext 中注册一个 Bean 定义,其类型为方法的返回值。默认情况下,Bean 的名称与方法名称相同。以下示例显示了 @Bean 方法声明:

java
@Configuration
public class AppConfig {

	@Bean
	public TransferServiceImpl transferService() {
		return new TransferServiceImpl();
	}
}
kotlin
@Configuration
class AppConfig {

	@Bean
	fun transferService() = TransferServiceImpl()
}

上述配置完全等同于以下 Spring XML:

xml
<beans>
	<bean id="transferService" class="com.acme.TransferServiceImpl"/>
</beans>

这两者都在 ApplicationContext 中提供了一个名为 transferService 的 Bean,并绑定到一个类型为 TransferServiceImpl 的对象实例。

你也可以使用 默认方法 (Default Methods) 来定义 Bean。这允许通过实现带有默认方法中 Bean 定义的接口来组合 Bean 配置。

java
public interface BaseConfig {

	@Bean
	default TransferServiceImpl transferService() {
		return new TransferServiceImpl();
	}
}

@Configuration
public class AppConfig implements BaseConfig {

}

你还可以声明你的 @Bean 方法具有 接口(或基类) 作为返回类型,如下例所示:

java
@Configuration
public class AppConfig {

	@Bean
	public TransferService transferService() {
		return new TransferServiceImpl();
	}
}
kotlin
@Configuration
class AppConfig {

	@Bean
	fun transferService(): TransferService {
		return TransferServiceImpl()
	}
}

然而,这会将高级类型预测的可见性限制在指定的接口类型(TransferService)。只有在受影响的单例 Bean 实例化之后,容器才能知道完整类型(TransferServiceImpl)。非延迟单例 Bean 根据它们的声明顺序进行实例化,因此根据另一个组件何时尝试通过非声明类型进行匹配(例如 @Autowired TransferServiceImpl,它仅在 transferService Bean 实例化后才解析),你可能会看到不同的类型匹配结果。

提示

如果你始终通过声明的服务接口引用你的类型,那么你的 @Bean 返回类型可以安全地遵循该设计决策。然而,对于实现多个接口的组件或可能通过其实现类型引用的组件,声明尽可能具体的返回类型(至少与引用你的 Bean 的注入点所需的一样具体)会更安全。

Bean 依赖 (Bean Dependencies)

标注了 @Bean 的方法可以具有任意数量的参数,这些参数描述了构建该 Bean 所需的依赖项。例如,如果我们的 TransferService 需要一个 AccountRepository,我们可以通过方法参数来实现该依赖,如下例所示:

java
@Configuration
public class AppConfig {

	@Bean
	public TransferService transferService(AccountRepository accountRepository) {
		return new TransferServiceImpl(accountRepository);
	}
}
kotlin
@Configuration
class AppConfig {

	@Bean
	fun transferService(accountRepository: AccountRepository): TransferService {
		return TransferServiceImpl(accountRepository)
	}
}

解析机制与基于构造函数的依赖注入几乎完全相同。

接收生命周期回调 (Receiving Lifecycle Callbacks)

任何使用 @Bean 注解定义的类都支持常规的生命周期回调,并且可以使用 JSR-250 的 @PostConstruct@PreDestroy 注解。

常规的 Spring 生命周期回调也得到完全支持。如果一个 Bean 实现了 InitializingBeanDisposableBeanLifecycle,容器会调用它们各自的方法。

标准的一组 *Aware 接口(例如 BeanFactoryAwareBeanNameAwareMessageSourceAwareApplicationContextAware 等)也得到完全支持。

@Bean 注解支持指定任意的初始化和销毁回调方法,就像 Spring XML 中 bean 元素的 init-methoddestroy-method 属性一样,如下例所示:

java
public class BeanOne {

	public void init() {
		// 初始化逻辑
	}
}

public class BeanTwo {

	public void cleanup() {
		// 销毁逻辑
	}
}

@Configuration
public class AppConfig {

	@Bean(initMethod = "init")
	public BeanOne beanOne() {
		return new BeanOne();
	}

	@Bean(destroyMethod = "cleanup")
	public BeanTwo beanTwo() {
		return new BeanTwo();
	}
}
kotlin
class BeanOne {

	fun init() {
		// 初始化逻辑
	}
}

class BeanTwo {

	fun cleanup() {
		// 销毁逻辑
	}
}

@Configuration
class AppConfig {

	@Bean(initMethod = "init")
	fun beanOne() = BeanOne()

	@Bean(destroyMethod = "cleanup")
	fun beanTwo() = BeanTwo()
}

::: note 注意 默认情况下,使用 Java 配置定义的具有公共 closeshutdown 方法的 Bean 会自动加入销毁回调。如果你有一个公共的 closeshutdown 方法,并且你不希望在容器关闭时调用它,你可以将 @Bean(destroyMethod = "") 添加到你的 Bean 定义中,以禁用默认的 (inferred) 模式。

对于你通过 JNDI 获取的资源,你可能希望默认这样做,因为其生命周期是在应用程序之外管理的。特别是对于 DataSource,务必始终这样做,因为众所周知它在 Jakarta EE 应用服务器上会出现问题。

以下示例显示了如何防止为 DataSource 进行自动销毁回调:

java
@Bean(destroyMethod = "")
public DataSource dataSource() throws NamingException {
	return (DataSource) jndiTemplate.lookup("MyDS");
}
kotlin
@Bean(destroyMethod = "")
fun dataSource(): DataSource {
	return jndiTemplate.lookup("MyDS") as DataSource
}

此外,对于 @Bean 方法,你通常会使用编程式 JNDI 查找,要么使用 Spring 的 JndiTemplateJndiLocatorDelegate 助手,要么直接使用 JNDI InitialContext,而不是使用 JndiObjectFactoryBean 变体(这会迫使你将返回类型声明为 FactoryBean 类型而不是实际的目标类型,从而使其更难在打算引用此处提供的资源的其它 @Bean 方法中用于交叉引用调用)。 :::

在上面提示之前的示例中的 BeanOne 的情况下,在构造期间直接调用 init() 方法也是同样有效的:

java
@Configuration
public class AppConfig {

	@Bean
	public BeanOne beanOne() {
		BeanOne beanOne = new BeanOne();
		beanOne.init();
		return beanOne;
	}

	// ...
}
kotlin
@Configuration
class AppConfig {

	@Bean
	fun beanOne() = BeanOne().apply {
		init()
	}

	// ...
}

提示

当你直接在 Java 中工作时,你可以对你的对象执行任何你喜欢的操作,而不必总是依赖容器生命周期。

指定 Bean 作用域 (Specifying Bean Scope)

Spring 包含了 @Scope 注解,以便你可以指定 Bean 的作用域。

使用 @Scope 注解

你可以指定使用 @Bean 注解定义的 Bean 应该具有特定的作用域。你可以使用 Bean 作用域部分中指定的任何标准作用域。

默认作用域是 singleton,但你可以使用 @Scope 注解覆盖它,如下例所示:

java
@Configuration
public class MyConfiguration {

	@Bean
	@Scope("prototype")
	public Encryptor encryptor() {
		// ...
	}
}
kotlin
@Configuration
class MyConfiguration {

	@Bean
	@Scope("prototype")
	fun encryptor(): Encryptor {
		// ...
	}
}

@Scopescoped-proxy

Spring 提供了通过 作用域代理 (scoped proxies) 处理作用域依赖的便捷方式。在使用 XML 配置时,创建此类代理的最简单方法是 <aop:scoped-proxy/> 元素。在 Java 中使用 @Scope 注解配置你的 Bean 提供了等效的支持,即 proxyMode 属性。默认值为 ScopedProxyMode.DEFAULT,这通常表示除非在组件扫描指令级别配置了不同的默认值,否则不应创建作用域代理。你可以指定 ScopedProxyMode.TARGET_CLASSScopedProxyMode.INTERFACESScopedProxyMode.NO

如果你将 XML 参考文档中的作用域代理示例移植到我们使用 Java 的 @Bean,它类似于以下内容:

java
// 一个暴露为代理的 HTTP Session 作用域 Bean
@Bean
@SessionScope
public UserPreferences userPreferences() {
	return new UserPreferences();
}

@Bean
public Service userService() {
	UserService service = new SimpleUserService();
	// 指向代理 userPreferences Bean 的引用
	service.setUserPreferences(userPreferences());
	return service;
}
kotlin
// 一个暴露为代理的 HTTP Session 作用域 Bean
@Bean
@SessionScope
fun userPreferences() = UserPreferences()

@Bean
fun userService(): Service {
	return SimpleUserService().apply {
		// 指向代理 userPreferences Bean 的引用
		setUserPreferences(userPreferences())
	}
}

自定义 Bean 命名 (Customizing Bean Naming)

默认情况下,配置类使用 @Bean 方法的名称作为生成的 Bean 的名称。但是,可以使用 name 属性覆盖此功能,如下例所示:

java
@Configuration
public class AppConfig {

	@Bean("myThing")
	public Thing thing() {
		return new Thing();
	}
}
kotlin
@Configuration
class AppConfig {

	@Bean("myThing")
	fun thing() = Thing()
}

Bean 别名 (Bean Aliasing)

正如在命名 Bean 中所讨论的,有时希望为单个 Bean 提供多个名称,也称为 Bean 别名。@Bean 注解的 name 属性接受一个字符串数组。以下示例显示了如何为一个 Bean 设置多个别名:

java
@Configuration
public class AppConfig {

	@Bean({"dataSource", "subsystemA-dataSource", "subsystemB-dataSource"})
	public DataSource dataSource() {
		// 实例化、配置并返回 DataSource Bean...
	}
}
kotlin
@Configuration
class AppConfig {

	@Bean("dataSource", "subsystemA-dataSource", "subsystemB-dataSource")
	fun dataSource(): DataSource {
		// 实例化、配置并返回 DataSource Bean...
	}
}

Bean 描述 (Bean Description)

有时,提供更详细的 Bean 文本描述会很有帮助。当为了监控目的(可能通过 JMX)暴露 Bean 时,这特别有用。

要向 @Bean 添加描述,你可以使用 @Description 注解,如下例所示:

java
@Configuration
public class AppConfig {

	@Bean
	@Description("Provides a basic example of a bean")
	public Thing thing() {
		return new Thing();
	}
}
kotlin
@Configuration
class AppConfig {

	@Bean
	@Description("Provides a basic example of a bean")
	fun thing() = Thing()
}

补充教学 —— @Bean 注解的高级玩儿法与避坑指南

@Bean 看似简单,但在实际项目(特别是 Spring Boot)中有很多容易忽略的细节。

1. 返回值类型的陷阱:接口还是实现类? 文档里提到了“高级类型预测”。简单来说:

  • 写法 Apublic MyService myService() { return new MyServiceImpl(); }
  • 写法 Bpublic MyServiceImpl myService() { return new MyServiceImpl(); }建议:除非你有极强的理由只暴露接口,否则请优先使用写法 B(具体实现类)原因:Spring 在初始化容器时,需要预测每个 Bean 的类型。如果是写法 A,Spring 只能看到 MyService 接口。如果你此时有另一个 Bean 用 @Autowired MyServiceImpl 注入,Spring 在 myService 真正创建出来之前是无法确定它是否匹配的。如果是写法 B,Spring 瞬间就能确定类型。

2. destroyMethod = "" 的神级用途 这是 Spring 源码中一个非常“聪明”但也容易误事的设计:推断式销毁(Inferred Destruction)。 Spring 会自动寻找 Bean 中名为 closeshutdown 的方法作为销毁回调。

  • 坑点:如果你从外部(如 JNDI)拿到了一个 DataSource,这个连接池的生命周期是由应用服务器管理的。如果 Spring 容器关闭时擅自调用了它的 close(),会导致整台服务器的其它应用也挂掉。
  • 对策:给这种 Bean 加上 @Bean(destroyMethod = ""),明明白白告诉 Spring:“别乱动,它的命不归你管”。

3. 方法参数注入 vs 直接调用方法@Configuration 类里,这两种方式都能实现依赖注入:

  • 方式 Apublic BeanB beanB(BeanA a) { return new BeanB(a); }
  • 方式 Bpublic BeanB beanB() { return new BeanB(beanA()); }建议优先使用方式 A(参数注入)。它更符合解耦的原则,且在 Lite 模式下也能正常工作。

4. 结合 @Profile 实现动态配置@Bean 经常与 @Profile 配合使用,实现环境切换:

java
@Bean
@Profile("dev")
public DataSource devDataSource() { ... }

@Bean
@Profile("prod")
public DataSource prodDataSource() { ... }

这比在 XML 里写嵌套的 <beans profile="..."> 要优雅得多。

5. 默认方法与“配置继承” 文档提到的接口 default 方法是一个非常现代的技巧。你可以把一些通用的基础设施(如 RedisTemplateObjectMapper)定义在接口里,然后不同的 @Configuration 类通过实现这些接口来“继承”这些配置。这在大型微服务架构中减少重复代码非常有效。

Based on Spring Framework.