对于最新稳定版本,请使用 Spring Framework 7.0.6spring-doc.cadn.net.cn

基于 Java 的配置组合

Spring 的基于 Java 的配置功能允许你组合注解,从而降低配置的复杂性。spring-doc.cadn.net.cn

使用@Import注解

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

@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,如下例所示:spring-doc.cadn.net.cn

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-doc.cadn.net.cn

从 Spring Framework 4.2 起,@Import 注解也支持引用普通的组件类,这与 AnnotationConfigApplicationContext.register 方法类似。 如果你希望通过少量配置类作为入口点来显式定义所有组件,从而避免使用组件扫描,这一特性将特别有用。

在导入的组件上注入依赖@Bean定义

前面的示例可以正常工作,但过于简单。在大多数实际场景中,Bean 在不同的配置类之间会相互依赖。使用 XML 时,这并不是一个问题,因为不涉及编译器,你可以声明 ref="someBean",并信任 Spring 在容器初始化期间自行解析。而使用 @Configuration 类时,Java 编译器会对配置模型施加约束,即对其他 Bean 的引用必须符合有效的 Java 语法。spring-doc.cadn.net.cn

幸运的是,解决这个问题很简单。正如我们之前所讨论的,一个@Bean方法可以拥有任意数量的参数,用于描述该 bean 的依赖项。请考虑以下更贴近实际的场景:多个@Configuration类相互依赖,每个类都依赖于其他类中声明的 bean:spring-doc.cadn.net.cn

@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 注入以及其他特性。spring-doc.cadn.net.cn

请确保以这种方式注入的依赖项仅限于最简单的类型。@Configuration类在上下文初始化过程中处理得非常早,强制通过这种方式注入依赖可能会导致意外的提前初始化。只要可能,请尽量采用基于参数的注入方式,如前面示例所示。spring-doc.cadn.net.cn

避免在同一个配置类的 @PostConstruct 方法中访问本地定义的 Bean。这实际上会导致循环引用,因为非静态的 @Bean 方法在语义上需要一个已完全初始化的配置类实例才能被调用。当循环引用被禁止时(例如在 Spring Boot 2.6 及更高版本中),这可能会触发 BeanCurrentlyInCreationException 异常。spring-doc.cadn.net.cn

此外,要特别小心通过 @Bean 定义的 BeanPostProcessorBeanFactoryPostProcessor。这些通常应声明为 static @Bean 方法,以避免触发其所在配置类的实例化。否则,@Autowired@Value 可能无法在该配置类本身上正常工作,因为有可能在 AutowiredAnnotationBeanPostProcessor 之前就将其创建为 Bean 实例。spring-doc.cadn.net.cn

以下示例展示了如何将一个 bean 自动装配到另一个 bean:spring-doc.cadn.net.cn

@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
为便于导航,对导入的 Bean 进行完整限定

在前述场景中,使用 @Autowired 能够很好地工作并提供所需的模块化,但要确切判断自动装配的 bean 定义是在哪里声明的,仍然有些模糊。例如,作为一名正在查看 ServiceConfig 的开发人员,你如何确切知道 @Autowired AccountRepository 这个 bean 是在哪里声明的呢?这一点在代码中并未显式体现,而这可能完全可以接受。请记住,Spring Tools for Eclipse 提供了可视化工具,可以生成图表来展示所有组件是如何连接的,这或许已经足够满足你的需求。此外,你的 Java IDE 也能轻松找到 AccountRepository 类型的所有声明和使用位置,并快速向你展示返回该类型的 @Bean 方法所在的位置。spring-doc.cadn.net.cn

在无法接受这种歧义性、并且希望直接从 IDE 中实现从一个 @Configuration 类导航到另一个类的情况下,请考虑自动装配配置类本身。以下示例展示了如何实现这一点:spring-doc.cadn.net.cn

@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 类在一定程度上得到缓解。请考虑以下示例:spring-doc.cadn.net.cn

@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 类及其依赖关系就与通常浏览基于接口的代码的过程没有区别了。spring-doc.cadn.net.cn

如果你想影响某些 Bean 的启动创建顺序,可以考虑将其中一些声明为 @Lazy(在首次访问时创建,而不是在启动时创建),或者使用 @DependsOn 注解指定依赖于其他特定的 Bean(确保在当前 Bean 创建之前,先创建这些指定的其他 Bean,这超出了当前 Bean 直接依赖关系所隐含的顺序)。

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

根据某些任意的系统状态,有条件地启用或禁用整个 @Configuration 类, 甚至单个的 @Bean 方法,通常是很有用的。一个常见的例子是使用 @Profile 注解, 仅在 Spring Environment 中启用了特定配置文件时才激活相应的 Bean(详见 Bean 定义配置文件)。spring-doc.cadn.net.cn

@Profile 注解实际上是通过使用一个更灵活的注解实现的,该注解称为 @Conditional@Conditional 注解指示在注册 @Bean 之前应咨询特定的 org.springframework.context.annotation.Condition 实现。spring-doc.cadn.net.cn

Condition 接口的实现提供了一个 matches(…​) 方法,该方法返回 truefalse。例如,以下代码清单展示了 Condition 注解实际使用的 @Profile 实现:spring-doc.cadn.net.cn

@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 以获取更多详情。spring-doc.cadn.net.cn

结合 Java 和 XML 配置

Spring 的 @Configuration 类支持并非旨在完全替代 Spring XML。某些功能(例如 Spring XML 命名空间)仍然是配置容器的理想方式。在 XML 使用起来更方便或必不可少的情况下,您可以选择以下两种方式之一:要么采用“以 XML 为中心”的方式,例如使用 ClassPathXmlApplicationContext 来实例化容器;要么采用“以 Java 为中心”的方式,使用 AnnotationConfigApplicationContext 并结合 @ImportResource 注解按需导入 XML。spring-doc.cadn.net.cn

以 XML 为中心的使用@Configuration

或许更倾向于通过 XML 来引导 Spring 容器,并以临时的方式包含 @Configuration 类。例如,在一个已大量使用 Spring XML 的现有代码库中,按需创建 @Configuration 类并从现有的 XML 文件中引入它们会更加简便。本节稍后将介绍在这种“以 XML 为中心”的场景中使用 @Configuration 类的选项。spring-doc.cadn.net.cn

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

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

以下示例展示了一个普通的 Java 配置类:spring-doc.cadn.net.cn

@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 文件的一部分:spring-doc.cadn.net.cn

<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 文件:spring-doc.cadn.net.cn

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);
	// ...
}
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/> 已经启用了相同的功能。spring-doc.cadn.net.cn

以下示例展示了修改后的 system-test-config.xml 文件:spring-doc.cadn.net.cn

<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以类为中心的 XML 使用方式,适用于@ImportResource

在以 @Configuration 类作为容器配置主要机制的应用程序中,通常仍有必要使用至少少量的 XML。在这些场景下,你可以使用 @ImportResource 注解,并仅定义所需的最少 XML 内容。这样做可以实现一种“以 Java 为中心”的容器配置方式,并将 XML 的使用降至最低限度。以下示例(包含一个配置类、一个定义 bean 的 XML 文件、一个属性文件以及 main 类)展示了如何使用 @ImportResource 注解来实现按需使用 XML 的“以 Java 为中心”的配置:spring-doc.cadn.net.cn

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