跳到主要内容

组合基于 Java 的配置

DeepSeek V3 中英对照 Composing Java-based Configurations

Spring 的基于 Java 的配置功能允许你组合注解,这可以减少配置的复杂性。

使用 @Import 注解

正如在 Spring XML 文件中使用 <import/> 元素来帮助模块化配置一样,@Import 注解允许从另一个配置类中加载 @Bean 定义,如下例所示:

@Configuration
public class ConfigA {

@Bean
public A a() {
return new A();
}
}

@Configuration
@Import(ConfigA.class)
public class ConfigB {

@Bean
public B b() {
return new B();
}
}
java

现在,在实例化上下文时,不再需要同时指定 ConfigA.classConfigB.class,只需显式提供 ConfigB 即可,如下例所示:

public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class);

// now both beans A and B will be available...
A a = ctx.getBean(A.class);
B b = ctx.getBean(B.class);
}
java

这种方法简化了容器的实例化过程,因为只需要处理一个类,而不需要在构建过程中记住可能大量的 @Configuration 类。

提示

自 Spring Framework 4.2 起,@Import 还支持引用常规组件类,类似于 AnnotationConfigApplicationContext.register 方法。如果你想避免组件扫描,通过使用一些配置类作为入口点来显式定义所有组件,这将特别有用。

在导入的 @Bean 定义中注入依赖

前面的示例可以工作,但过于简单。在大多数实际场景中,bean 在配置类之间相互依赖。使用 XML 时,这不是问题,因为没有编译器的参与,你可以声明 ref="someBean" 并相信 Spring 在容器初始化期间会处理好。当使用 @Configuration 类时,Java 编译器对配置模型施加了限制,因为对其他 bean 的引用必须是有效的 Java 语法。

幸运的是,解决这个问题非常简单。正如我们之前讨论的@Bean 方法可以有任意数量的参数来描述 bean 的依赖关系。考虑以下更现实的场景,其中有多个 @Configuration 类,每个类都依赖于其他类中声明的 bean:

@Configuration
public class ServiceConfig {

@Bean
public TransferService transferService(AccountRepository accountRepository) {
return new TransferServiceImpl(accountRepository);
}
}

@Configuration
public class RepositoryConfig {

@Bean
public AccountRepository accountRepository(DataSource dataSource) {
return new JdbcAccountRepository(dataSource);
}
}

@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {

@Bean
public DataSource dataSource() {
// return new DataSource
}
}

public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
// everything wires up across configuration classes...
TransferService transferService = ctx.getBean(TransferService.class);
transferService.transfer(100.00, "A123", "C456");
}
java

还有另一种方法可以达到相同的结果。请记住,@Configuration 类最终只是容器中的另一个 bean:这意味着它们可以像任何其他 bean 一样利用 @Autowired@Value 注入以及其他功能。

注意

请确保你以这种方式注入的依赖项是最简单的那种。@Configuration 类在上下文初始化的早期阶段就被处理了,强制以这种方式注入依赖项可能会导致意外的早期初始化。尽可能使用基于参数的注入,如前例所示。

避免在同一个配置类的 @PostConstruct 方法中访问本地定义的 bean。这实际上会导致循环引用,因为非静态的 @Bean 方法在语义上需要一个完全初始化的配置类实例来调用。由于循环引用是不允许的(例如,在 Spring Boot 2.6+ 中),这可能会触发 BeanCurrentlyInCreationException

此外,通过 @Bean 定义 BeanPostProcessorBeanFactoryPostProcessor 时要特别小心。这些通常应声明为 static @Bean 方法,不会触发其包含配置类的实例化。否则,@Autowired@Value 可能无法在配置类本身上工作,因为它可能会在 AutowiredAnnotationBeanPostProcessor 之前作为 bean 实例创建。

以下示例展示了如何将一个 bean 自动装配到另一个 bean 中:

@Configuration
public class ServiceConfig {

@Autowired
private AccountRepository accountRepository;

@Bean
public TransferService transferService() {
return new TransferServiceImpl(accountRepository);
}
}

@Configuration
public class RepositoryConfig {

private final DataSource dataSource;

public RepositoryConfig(DataSource dataSource) {
this.dataSource = dataSource;
}

@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(dataSource);
}
}

@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {

@Bean
public DataSource dataSource() {
// return new DataSource
}
}

public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
// everything wires up across configuration classes...
TransferService transferService = ctx.getBean(TransferService.class);
transferService.transfer(100.00, "A123", "C456");
}
java
提示

@Configuration 类中使用构造函数注入仅在 Spring Framework 4.3 及更高版本中支持。还要注意,如果目标 bean 只定义了一个构造函数,则无需指定 @Autowired

完全限定导入的 Bean 以便于导航

在前面的场景中,使用 @Autowired 效果很好,并且提供了所需的模块化,但确定自动装配的 bean 定义到底在哪里声明仍然有些模糊。例如,作为一个开发者,查看 ServiceConfig 时,你如何确切知道 @Autowired AccountRepository bean 是在哪里声明的?这在代码中并不明确,但这可能完全没问题。记住,Spring Tools for Eclipse 提供了工具,可以渲染图表显示所有内容是如何连接的,这可能就是你所需要的。此外,你的 Java IDE 可以轻松找到 AccountRepository 类型的所有声明和使用,并快速显示返回该类型的 @Bean 方法的位置。

在无法接受这种歧义并且希望从 IDE 中直接从一个 @Configuration 类导航到另一个 @Configuration 类的情况下,可以考虑自动装配配置类本身。以下示例展示了如何做到这一点:

@Configuration
public class ServiceConfig {

@Autowired
private RepositoryConfig repositoryConfig;

@Bean
public TransferService transferService() {
// navigate 'through' the config class to the @Bean method!
return new TransferServiceImpl(repositoryConfig.accountRepository());
}
}
java

在前面的情况中,AccountRepository 的定义是完全显式的。然而,ServiceConfig 现在与 RepositoryConfig 紧密耦合。这就是权衡所在。这种紧密耦合可以通过使用基于接口或抽象类的 @Configuration 类来稍微缓解。考虑以下示例:

@Configuration
public class ServiceConfig {

@Autowired
private RepositoryConfig repositoryConfig;

@Bean
public TransferService transferService() {
return new TransferServiceImpl(repositoryConfig.accountRepository());
}
}

@Configuration
public interface RepositoryConfig {

@Bean
AccountRepository accountRepository();
}

@Configuration
public class DefaultRepositoryConfig implements RepositoryConfig {

@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(...);
}
}

@Configuration
@Import({ServiceConfig.class, DefaultRepositoryConfig.class}) // import the concrete config!
public class SystemTestConfig {

@Bean
public DataSource dataSource() {
// return DataSource
}

}

public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
TransferService transferService = ctx.getBean(TransferService.class);
transferService.transfer(100.00, "A123", "C456");
}
java

现在,ServiceConfig 与具体的 DefaultRepositoryConfig 之间的耦合度较低,并且内置的 IDE 工具仍然有用:你可以轻松获取 RepositoryConfig 实现的类型层次结构。通过这种方式,导航 @Configuration 类及其依赖项变得与通常的基于接口的代码导航过程无异。

影响 @Bean 定义的单例启动

如果你想影响某些单例 bean 的启动创建顺序,可以考虑将其中一些声明为 @Lazy,以便在首次访问时创建,而不是在启动时创建。

@DependsOn 强制某些其他 bean 首先初始化,确保在创建当前 bean 之前先创建指定的 bean,这超出了当前 bean 直接依赖所隐含的范围。

背景初始化

截至 6.2 版本,新增了一个后台初始化选项:@Bean(bootstrap=BACKGROUND) 允许将特定的 bean 标记为后台初始化,在上下文启动时,每个此类 bean 的整个创建步骤都将被覆盖。

具有非懒加载注入点的依赖 bean 会自动等待 bean 实例的完成。所有常规的后台初始化都会在上下文启动结束时强制完成。只有额外标记为 @Lazy 的 bean 才允许稍后完成(直到首次实际访问时)。

后台初始化通常与 @Lazy(或 ObjectProvider)注入点在依赖的 bean 中一起使用。否则,当需要提前注入实际后台初始化的 bean 实例时,主引导线程将会阻塞。

这种并发启动形式适用于单个 bean:如果这样的 bean 依赖于其他 bean,那么这些依赖的 bean 需要已经被初始化,要么通过简单地在前声明,要么通过 @DependsOn 在主引导线程中强制执行初始化,然后在后台触发受影响 bean 的初始化。

备注

必须声明一个类型为 ExecutorbootstrapExecutor bean,以便后台引导功能实际生效。否则,运行时将忽略后台标记。

引导执行器可以是一个仅用于启动目的的有界执行器,也可以是一个同时服务于其他目的的共享线程池。

条件性地包含 @Configuration 类或 @Bean 方法

通常,根据某些任意的系统状态有条件地启用或禁用整个 @Configuration 类甚至单个 @Bean 方法是非常有用的。一个常见的例子是使用 @Profile 注解,仅在 Spring Environment 中启用了特定配置文件时激活 bean(详情请参阅 Bean 定义配置文件)。

@Profile 注解实际上是通过使用一个更为灵活的注解 @Conditional 来实现的。@Conditional 注解指示了在注册 @Bean 之前应咨询的特定 org.springframework.context.annotation.Condition 实现。

Condition 接口的实现提供了一个 matches(…​) 方法,该方法返回 truefalse。例如,以下代码展示了用于 @Profile 的实际 Condition 实现:

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// Read the @Profile annotation attributes
MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
if (attrs != null) {
for (Object value : attrs.get("value")) {
if (context.getEnvironment().matchesProfiles((String[]) value)) {
return true;
}
}
return false;
}
return true;
}
java

有关更多详细信息,请参阅 @Conditional 的 Javadoc。

结合 Java 和 XML 配置

Spring 的 @Configuration 类支持并不旨在完全替代 Spring XML。一些功能,如 Spring XML 命名空间,仍然是配置容器的理想方式。在 XML 方便或必要的情况下,您有两种选择:要么以“以 XML 为中心”的方式实例化容器,例如使用 ClassPathXmlApplicationContext,要么以“以 Java 为中心”的方式实例化容器,使用 AnnotationConfigApplicationContext 并通过 @ImportResource 注解根据需要导入 XML。

以 XML 为中心的 @Configuration 类使用

在这种情况下,可能更倾向于从 XML 文件引导 Spring 容器,并以临时的方式包含 @Configuration 类。例如,在一个使用 Spring XML 的大型现有代码库中,按需创建 @Configuration 类并从现有的 XML 文件中包含它们会更为方便。在本节的后半部分,我们将介绍在这种“以 XML 为中心”的情况下使用 @Configuration 类的选项。

@Configuration 类声明为普通的 Spring <bean/> 元素

请记住,@Configuration 类最终是容器中的 bean 定义。在这一系列示例中,我们创建了一个名为 AppConfig@Configuration 类,并将其作为 <bean/> 定义包含在 system-test-config.xml 中。由于 <context:annotation-config/> 已启用,容器会识别 @Configuration 注解并正确处理在 AppConfig 中声明的 @Bean 方法。

以下示例展示了 Java 和 Kotlin 中的 AppConfig 配置类:

@Configuration
public class AppConfig {

@Autowired
private DataSource dataSource;

@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(dataSource);
}

@Bean
public TransferService transferService() {
return new TransferServiceImpl(accountRepository());
}
}
java

以下示例展示了一个示例 system-test-config.xml 文件的部分内容:

<beans>
<!-- enable processing of annotations such as @Autowired and @Configuration -->
<context:annotation-config/>

<context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>

<bean class="com.acme.AppConfig"/>

<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
</beans>
xml

以下示例展示了一个可能的 jdbc.properties 文件:

jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
jdbc.username=sa
jdbc.password=
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:/com/acme/system-test-config.xml");
TransferService transferService = ctx.getBean(TransferService.class);
// ...
}
java
备注

system-test-config.xml 文件中,AppConfig <bean/> 没有声明 id 属性。虽然这样做是可以接受的,但这是不必要的,因为没有其他 bean 会引用它,而且不太可能通过名称从容器中显式获取它。同样地,DataSource bean 仅通过类型自动装配,因此严格来说,显式的 bean id 并不是必需的。

使用 <context:component-scan /> 来扫描 @Configuration

因为 @Configuration 被元注解 @Component 所标注,所以使用 @Configuration 注解的类会自动成为组件扫描的候选对象。使用与前一个示例相同的场景,我们可以重新定义 system-test-config.xml 以利用组件扫描功能。请注意,在这种情况下,我们不需要显式声明 <context:annotation-config/>,因为 <context:component-scan/> 已经启用了相同的功能。

以下示例展示了修改后的 system-test-config.xml 文件:

<beans>
<!-- picks up and registers AppConfig as a bean definition -->
<context:component-scan base-package="com.acme"/>

<context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>

<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
</beans>
xml

@Configuration 类为中心的 XML 使用与 @ImportResource

在那些以 @Configuration 类作为配置容器的主要机制的应用中,可能仍然需要使用至少一些 XML。在这种场景下,你可以使用 @ImportResource 并仅定义所需的 XML。这样做可以实现一种“以 Java 为中心”的容器配置方式,并将 XML 的使用降至最低。以下示例(包括一个配置类、一个定义 bean 的 XML 文件、一个属性文件以及 main() 方法)展示了如何使用 @ImportResource 注解来实现“以 Java 为中心”的配置,仅在需要时使用 XML:

@Configuration
@ImportResource("classpath:/com/acme/properties-config.xml")
public class AppConfig {

@Value("${jdbc.url}")
private String url;

@Value("${jdbc.username}")
private String username;

@Value("${jdbc.password}")
private String password;

@Bean
public DataSource dataSource() {
return new DriverManagerDataSource(url, username, password);
}

@Bean
public AccountRepository accountRepository(DataSource dataSource) {
return new JdbcAccountRepository(dataSource);
}

@Bean
public TransferService transferService(AccountRepository accountRepository) {
return new TransferServiceImpl(accountRepository);
}

}
java
<beans>
<context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
</beans>
xml
jdbc.propertiesjdbc.url=jdbc:hsqldb:hsql://localhost/xdb
jdbc.username=sa
jdbc.password=
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
TransferService transferService = ctx.getBean(TransferService.class);
// ...
}
java