Composing Java-based Configurations

Spring 基于 Java 的配置功能让您可以组合注解,这可以降低配置的复杂性。

Using the @Import Annotation

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

  • Java

  • Kotlin

@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();
	}
}
@Configuration
class ConfigA {

	@Bean
	fun a() = A()
}

@Configuration
@Import(ConfigA::class)
class ConfigB {

	@Bean
	fun b() = B()
}

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

  • Java

  • Kotlin

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);
}
import org.springframework.beans.factory.getBean

fun main() {
	val ctx = AnnotationConfigApplicationContext(ConfigB::class.java)

	// now both beans A and B will be available...
	val a = ctx.getBean<A>()
	val b = ctx.getBean<B>()
}

这种方法简化了容器实例化,因为只需处理一个类,而不是在构建期间要求记住大量的 @Configuration 类。

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

Injecting Dependencies on Imported @Bean Definitions

前面的示例可用,但过于简单。在大多数实际场景中,bean 依赖于跨配置类的其他 bean。在使用 XML 时,这不是问题,因为它不涉及编译器,并且可以声明 ref="someBean",并相信 Spring 会在容器初始化期间解决它。在使用 @Configuration 类时,Java 编译器对配置模型施加了约束,因为对其他 bean 的引用必须是有效的 Java 语法。

幸运的是,解决这个问题很简单。如 we already discussed@Bean 方法可以具有任意数量的参数来描述 bean 依赖项。考虑以下更现实的场景,其中包含多个 @Configuration 类,每个类都依赖于在其他类中声明的 bean:

  • Java

  • Kotlin

@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");
}
import org.springframework.beans.factory.getBean

@Configuration
class ServiceConfig {

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

@Configuration
class RepositoryConfig {

	@Bean
	fun accountRepository(dataSource: DataSource): AccountRepository {
		return JdbcAccountRepository(dataSource)
	}
}

@Configuration
@Import(ServiceConfig::class, RepositoryConfig::class)
class SystemTestConfig {

	@Bean
	fun dataSource(): DataSource {
		// return new DataSource
	}
}


fun main() {
	val ctx = AnnotationConfigApplicationContext(SystemTestConfig::class.java)
	// everything wires up across configuration classes...
	val transferService = ctx.getBean<TransferService>()
	transferService.transfer(100.00, "A123", "C456")
}

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

确保以这种方式注入的依赖项是最简单的类型。@Configuration 类在上下文的初始化期间很早就被处理,强制一个依赖项以这种方式被注入可能会导致意外的早期初始化。在可能的情况下,使用基于参数的注入,如前面的示例。 避免在同一个配置类的 @PostConstruct 方法中访问本地定义的 bean。这实际上会导致循环引用,因为非静态 @Bean 方法在语义上要求调用一个完全初始化的配置类实例。在禁止循环引用(例如,在 Spring Boot 2.6+ 中)时,这可能会触发 BeanCurrentlyInCreationException。 此外,要特别小心通过 @Bean 定义的 BeanPostProcessorBeanFactoryPostProcessor。这些通常应声明为 static @Bean 方法,而不是触发其中一个配置类的实例化。否则, @Autowired@Value 可能无法在配置类本身上工作,因为有可能比 AutowiredAnnotationBeanPostProcessor 更早地创建它作为 Bean 实例。

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

  • Java

  • Kotlin

@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");
}
import org.springframework.beans.factory.getBean

@Configuration
class ServiceConfig {

	@Autowired
	lateinit var accountRepository: AccountRepository

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

@Configuration
class RepositoryConfig(private val dataSource: DataSource) {

	@Bean
	fun accountRepository(): AccountRepository {
		return JdbcAccountRepository(dataSource)
	}
}

@Configuration
@Import(ServiceConfig::class, RepositoryConfig::class)
class SystemTestConfig {

	@Bean
	fun dataSource(): DataSource {
		// return new DataSource
	}
}

fun main() {
	val ctx = AnnotationConfigApplicationContext(SystemTestConfig::class.java)
	// everything wires up across configuration classes...
	val transferService = ctx.getBean<TransferService>()
	transferService.transfer(100.00, "A123", "C456")
}

@Configuration 类中的构造函数注入从 Spring Framework 4.3 起才受支持。另外请注意,如果目标 bean 仅定义了一个构造函数,则不需要指定 @Autowired

[id="beans-java-injecting-imported-beans-fq"]Fully-qualifying imported beans for ease of navigation

在上述场景中,使用 @Autowired 效果很好,并提供了所需的模块化,但确定自动装配 Bean 定义的确切声明位置仍然有些含糊。例如,作为查看 ServiceConfig 的开发人员,您如何确切知道 @Autowired AccountRepository Bean 的声明位置?这在代码中并没有明确说明,这可能很好。请记住, Spring Tools for Eclipse 提供了可以呈现图表以显示如何装配所有内容的工具,这可能就是您所需要的。此外,您的 Java IDE 可以轻松找到 AccountRepository 类型的全部声明和用法,并快速向您展示返回该类型的 @Bean 方法的位置。 在无法接受此模棱两可的情况下,并且希望从一个 @Configuration 类直接导航到另一个 @Configuration 类,请考虑自动装配配置类本身。以下示例展示了如何执行此操作:

  • Java

  • Kotlin

@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());
	}
}
@Configuration
class ServiceConfig {

	@Autowired
	private lateinit var repositoryConfig: RepositoryConfig

	@Bean
	fun transferService(): TransferService {
		// navigate 'through' the config class to the @Bean method!
		return TransferServiceImpl(repositoryConfig.accountRepository())
	}
}

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

  • Java

  • Kotlin

@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");
}
import org.springframework.beans.factory.getBean

@Configuration
class ServiceConfig {

	@Autowired
	private lateinit var repositoryConfig: RepositoryConfig

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

@Configuration
interface RepositoryConfig {

	@Bean
	fun accountRepository(): AccountRepository
}

@Configuration
class DefaultRepositoryConfig : RepositoryConfig {

	@Bean
	fun accountRepository(): AccountRepository {
		return JdbcAccountRepository(...)
	}
}

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

	@Bean
	fun dataSource(): DataSource {
		// return DataSource
	}

}

fun main() {
	val ctx = AnnotationConfigApplicationContext(SystemTestConfig::class.java)
	val transferService = ctx.getBean<TransferService>()
	transferService.transfer(100.00, "A123", "C456")
}

现在 ServiceConfig 对于具体的 DefaultRepositoryConfig 是松散耦合的,并且内置的 IDE 工具仍然有用:您可以轻松地获得 RepositoryConfig 实现的类型层次结构。这样,导航 @Configuration 类及其依赖项与通常导航基于接口的代码的过程没有区别。

Influencing the Startup of @Bean-defined Singletons

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

@DependsOn 强制某些其他 bean 首先初始化,从而确保在当前 bean 之前创建指定的 bean,超越后者的直接依赖项所暗示的内容。

Background Initialization

从 6.2 开始,有一个后台初始化选项:@Bean(bootstrap=BACKGROUND) 允许为后台初始化选择特定 bean,涵盖上下文中启动的每个此类 bean 的整个 bean 创建步骤。

具有非惰性注入点的依赖 bean 会自动等待 bean 实例完成。将在上下文启动结束时强制完成所有常规后台初始化。只有另外标记为 @Lazy 的 bean 可以稍后完成(直到首次实际访问)。

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

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

如果想要真正启动后台引导,就必须声明一个类型为 ExecutorbootstrapExecutor bean。否则,将在运行时忽略后台标记。 引导执行器可以仅仅用于启动目的的受限执行器,也可以是为其他目的提供服务的共享线程池。

Conditionally Include @Configuration Classes or @Bean Methods

通常很方便根据某些任意的系统状态对一个完整的 @Configuration 类甚至对 @Bean 方法有条件地启用或禁用它们。一个常见的示例是使用 @Profile 注释,在仅在 Spring Environment 中启用了特定配置文件时激活 bean(有关详细信息,请参见 Bean Definition Profiles)。

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

Condition 接口的实现提供了一个返回 truefalsematches(…​) 方法。例如,以下清单显示了用于 @Profile 的实际 Condition 实现:

  • Java

  • Kotlin

@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().acceptsProfiles(((String[]) value))) {
				return true;
			}
		}
		return false;
	}
	return true;
}
override fun matches(context: ConditionContext, metadata: AnnotatedTypeMetadata): Boolean {
	// Read the @Profile annotation attributes
	val attrs = metadata.getAllAnnotationAttributes(Profile::class.java.name)
	if (attrs != null) {
		for (value in attrs["value"]!!) {
			if (context.environment.acceptsProfiles(Profiles.of(*value as Array<String>))) {
				return true
			}
		}
		return false
	}
	return true
}

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

Combining Java and XML Configuration

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

XML-centric Use of @Configuration Classes

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

[id="beans-java-combining-xml-centric-declare-as-bean"]Declaring @Configuration classes as plain Spring <bean/> elements

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

  • Java

  • Kotlin

@Configuration
public class AppConfig {

	@Autowired
	private DataSource dataSource;

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

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

	@Autowired
	private lateinit var dataSource: DataSource

	@Bean
	fun accountRepository(): AccountRepository {
		return JdbcAccountRepository(dataSource)
	}

	@Bean
	fun transferService() = TransferService(accountRepository())
}

以下示例显示了示例 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>

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

jdbc.url=jdbc:hsqldb:hsql://localhost/xdb jdbc.username=sa jdbc.password=

  • Java

  • Kotlin

public static void main(String[] args) {
	ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:/com/acme/system-test-config.xml");
	TransferService transferService = ctx.getBean(TransferService.class);
	// ...
}
fun main() {
	val ctx = ClassPathXmlApplicationContext("classpath:/com/acme/system-test-config.xml")
	val transferService = ctx.getBean<TransferService>()
	// ...
}

system-test-config.xml 文件中,AppConfig <bean/> 没有声明 id 元素。虽然可以这样做,但没有必要,因为没有任何其他 bean 引用它,并且不可能通过名称从容器明确获取它。类似地,DataSource bean 总是仅按类型自动装配,因此一个明确的 bean id 不是严格必需的。

[id="beans-java-combining-xml-centric-component-scan"] Using <context:component-scan/> to pick up @Configuration classes

由于 @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>

@Configuration Class-centric Use of XML with @ImportResource

@Configuration 类为配置容器的主要机制的应用程序中,仍然可能需要至少使用一些 XML。在这些场景中,您可以使用 @ImportResource,并且仅定义所需数量的 XML。这样做实现了对容器进行“以 Java 为中心”的配置的方法,并将 XML 保持在最低限度。以下示例(包括配置类、定义 bean 的 XML 文件、属性文件和 main 类)展示了如何使用 @ImportResource 注解来实现所需的“以 Java 为中心”的 XML 配置:

  • Java

  • Kotlin

@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);
	}
}
@Configuration
@ImportResource("classpath:/com/acme/properties-config.xml")
class AppConfig {

	@Value("\${jdbc.url}")
	private lateinit var url: String

	@Value("\${jdbc.username}")
	private lateinit var username: String

	@Value("\${jdbc.password}")
	private lateinit var password: String

	@Bean
	fun dataSource(): DataSource {
		return DriverManagerDataSource(url, username, password)
	}
}
properties-config.xml
<beans>
	<context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
</beans>

jdbc.properties jdbc.url=jdbc:hsqldb:hsql://localhost/xdb jdbc.username=sa jdbc.password=

  • Java

  • Kotlin

public static void main(String[] args) {
	ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
	TransferService transferService = ctx.getBean(TransferService.class);
	// ...
}
import org.springframework.beans.factory.getBean

fun main() {
	val ctx = AnnotationConfigApplicationContext(AppConfig::class.java)
	val transferService = ctx.getBean<TransferService>()
	// ...
}