组合基于Java的配置

Spring的基于Java的配置功能允许您组合注解,从而减少配置的复杂性。

使用@Import注解

就像在Spring XML文件中使用<import/>元素来帮助模块化配置一样,@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);

	// 现在两个bean A 和 B 都将可用...
	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)

	// 现在两个bean A 和 B 都将可用...
	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() {
		// 返回一个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 {

	@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 {
		// 返回一个DataSource
	}
}


fun main() {
	val ctx = AnnotationConfigApplicationContext(SystemTestConfig::class.java)
	// 所有配置类之间都会自动连接...
	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() {
		// 返回一个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
	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 {
		// 返回一个DataSource
	}
}

fun main() {
	val ctx = AnnotationConfigApplicationContext(SystemTestConfig::class.java)
	// 所有配置类之间都会自动连接...
	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类型的所有声明和用法,并快速显示返回该类型的@Bean方法的位置。

在不接受这种模糊性的情况下,并且希望能够从IDE内部直接导航到另一个@Configuration类时,请考虑自动装配配置类本身。以下示例显示了如何实现:

  • Java

  • Kotlin

@Configuration
public class ServiceConfig {

	@Autowired
	private RepositoryConfig repositoryConfig;

	@Bean
	public TransferService transferService() {
		// 通过配置类导航到@Bean方法!
		return new TransferServiceImpl(repositoryConfig.accountRepository());
	}
}
@Configuration
class ServiceConfig {

	@Autowired
	private lateinit var repositoryConfig: RepositoryConfig

	@Bean
	fun transferService(): TransferService {
		// 通过配置类导航到@Bean方法!
		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})  // 导入具体的配置!
public class SystemTestConfig {

	@Bean
	public DataSource dataSource() {
		// 返回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)  // 导入具体的配置!
class SystemTestConfig {

	@Bean
	fun dataSource(): DataSource {
		// 返回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的启动创建顺序,请考虑将其中一些声明为@Lazy(在首次访问时创建而不是在启动时创建)或者作为@DependsOn某些其他bean(确保在当前bean之前创建特定的其他bean,超出后者的直接依赖关系所暗示的范围)。

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

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

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

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

  • Java

  • Kotlin

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
	// 读取@Profile注解属性
	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 {
	// 读取@Profile注解属性
	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。

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类,并将其包含在system-test-config.xml中作为<bean/>定义。因为<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>
	<!-- 启用诸如@Autowired和@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>
	<!-- 捕获并注册AppConfig作为bean定义 -->
	<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为中心”的配置:

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