组合基于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.class和ConfigB.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 定义的 BeanPostProcessor 和 BeanFactoryPostProcessor,要特别小心。这些通常应该声明为静态的 @Bean 方法,以免触发其包含的配置类的实例化。否则,@Autowired 和 @Value 可能无法在配置类本身上生效,因为有可能在 AutowiredAnnotationBeanPostProcessor 被调用之前就创建了配置类实例。
以下示例展示了如何将一个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")
}
如果目标bean只定义了一个构造函数,则无需指定@Autowired。
为了便于导航而对导入的bean进行完全限定
在前面的场景中,使用@Autowired能够很好地发挥作用,并提供了所需的模块化特性,但确切地确定自动注入的bean定义在哪里被声明仍然有些模糊。例如,作为开发者,当你查看ServiceConfig时,如何才能准确知道@Autowired AccountRepository bean是在哪里被声明的呢?代码中并没有明确指出这一点,不过这可能也没什么大问题。需要注意的是,Spring Tools IDE提供了相应的工具,可以生成图表来显示所有的依赖关系,这可能正是你所需要的。此外,你的Java IDE也能够轻松找到所有AccountRepository类型的声明和使用地方,并快速显示出返回该类型的@Bean方法的位置。
在无法接受这种模糊性的情况下,如果你希望从IDE内部直接从一个@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类及其依赖关系就与通常导航基于接口的代码的过程没有区别了。
影响由@Bean定义的单例(Singletons)的启动
如果你想影响某些单例bean的创建顺序,可以考虑将其中一些声明为@Lazy,这样它们会在首次被访问时才进行创建,而不是在启动时创建。
@DependsOn 强制某些其他bean首先被初始化,确保在当前bean创建之前,这些指定的bean已经被创建完毕,这超出了后者直接依赖关系所要求的内容。
背景初始化
从6.2版本开始,有一个背景初始化选项:@Bean(bootstrap=BACKGROUND)允许单独指定特定bean进行背景初始化,这将覆盖上下文启动时每个此类bean的整个创建过程。
具有非延迟注入点的依赖Bean会自动等待Bean实例完成初始化。所有常规的后台初始化操作都会在上下文启动结束时被强制完成。只有额外标记为@Lazy的Bean才被允许在之后完成初始化(直到首次实际访问时)。
背景初始化通常与依赖bean中的@Lazy(或ObjectProvider)注入点一起使用。否则,当需要提前注入实际已进行背景初始化的bean实例时,主启动线程将会被阻塞。
这种并发启动的形式适用于单个Bean:如果这样的Bean依赖于其他Bean,那么这些其他Bean需要已经被初始化,要么是通过更早的声明来实现的,要么是通过@DependsOn注解来实现的。该注解会强制在触发受影响Bean的后台初始化之前,在主启动线程中完成这些Bean的初始化过程。
为了使后台引导(background bootstrapping)真正生效,必须声明一个类型为Executor的bootstrapExecutor bean。否则,在运行时后台标记(background markers)会被忽略。
该引导执行器(bootstrap executor)可以仅用于启动目的而是一个有界执行器(bounded executor),也可以是一个同时用于其他用途的共享线程池(shared thread pool)。
条件性地包含 @Configuration 类或 @Bean 方法
根据某种任意的系统状态,有时很有必要有条件地启用或禁用整个@Configuration类,甚至是个别的@Bean方法。一个常见的例子是使用@Profile注解,只有在Spring的Environment中启用了特定的配置文件(profile)时,才激活相应的Bean(详情请参见Bean定义配置文件)。
@Profile 注解实际上是通过使用一个更加灵活的注解 @Conditional 来实现的。@Conditional 注解指定了在注册 @Bean 之前应该先检查的特定 org.springframework.context.annotation.Condition 实现类。
Condition接口的实现提供了一个matches(…)方法,该方法返回true或false。例如,以下代码展示了用于@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类支持并不旨在100%完全替代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配置类:
- 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 类中心使用 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);
}
@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)
}
}
<beans>
<context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
</beans>
jdbc.propertiesjdbc.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>()
// ...
}