组合基于 Java 的配置

Spring 基于 Java 的配置特性允许您组合注解,这可以降低配置的复杂性。

使用 @Import 注解

正如 <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 方法。如果您想通过使用几个配置类作为入口点来显式定义所有组件,从而避免组件扫描,这会特别有用。

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

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

幸运的是,解决这个问题很简单。正如我们已经讨论过的,一个 @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:这意味着它们可以像任何其他 Bean 一样利用 @Autowired@Value 注入以及其他特性。

确保通过这种方式注入的依赖关系仅是最简单的类型。@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")
}
从 Spring Framework 4.3 起,@Configuration 类中才支持构造器注入。另请注意,如果目标 Bean 只定义了一个构造器,则无需指定 @Autowired

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

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

在这种模糊性不可接受,并且希望在 IDE 中实现从一个 @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 类及其依赖关系就与导航基于接口的代码的常规过程没有区别了。

影响 @Bean 定义的单例的启动

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

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

后台初始化

从 6.2 起,有一个后台初始化选项:@Bean(bootstrap=BACKGROUND) 允许将特定的 Bean 单独拎出来进行后台初始化,涵盖了每个此类 Bean 在上下文启动时的整个 Bean 创建步骤。

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

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

这种形式的并发启动适用于单个 Bean:如果这样的 Bean 依赖于其他 Bean,则这些 Bean 需要已经被初始化,这可以通过简单地早于它声明,或者通过 @DependsOn 来实现,后者强制在主引导线程中进行初始化,然后再触发受影响 Bean 的后台初始化。

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

引导执行器可以是一个仅用于启动目的的有限执行器,或者是一个也用于其他目的的共享线程池。

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

通常,基于任意系统状态有条件地启用或禁用完整的 @Configuration 类甚至单个 @Bean 方法会很有用。一个常见的例子是使用 @Profile 注解,仅当 Spring Environment 中启用了特定的 Profile 时才激活 Bean(详情请参阅Bean 定义 Profile)。

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

Condition 接口的实现提供了一个 matches(…​) 方法,该方法返回 truefalse。例如,以下列表显示了用于 @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().matchesProfiles((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.matchesProfiles(*value as Array<String>)) {
				return true
			}
		}
		return false
	}
	return true
}

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

组合 Java 和 XML 配置

Spring 的 @Configuration 类支持并不旨在完全取代 Spring XML。一些特性,例如 Spring XML 命名空间,仍然是配置容器的理想方式。在 XML 方便或必要的情况下,您可以选择:或者使用例如 ClassPathXmlApplicationContext 以“XML 为中心”的方式实例化容器,或者使用 AnnotationConfigApplicationContext@ImportResource 注解以“Java 为中心”的方式按需导入 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 配置类

  • Java

  • Kotlin

@Configuration
public class AppConfig {

	@Autowired
	private DataSource dataSource;

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

	@Bean
	public TransferService transferService() {
		return new TransferServiceImpl(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

使用 <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>

以 @Configuration 类为中心,结合 @ImportResource 使用 XML

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

  • 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);
	}

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

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

}
@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)
	}

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

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

}
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>()
	// ...
}