组合基于 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.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 开始, |
注入导入的 @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 要特别小心。这些通常应声明为 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")
}
|
|
在前面的场景中,使用 @Autowired 效果很好并提供了所需的模块化,但确定自动装配的 bean 定义确切声明在哪里仍然有些模糊。例如,作为查看 ServiceConfig 的开发人员,您如何知道 @Autowired AccountRepository bean 确切声明在哪里?它在代码中不明确,这可能很好。请记住,https://spring.io/tools[Spring Tools for Eclipse] 提供了可以呈现显示所有连接方式的图表的工具,这可能就是您所需要的一切。此外,您的 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 定义的单例的启动
如果您想影响某些单例 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,则它们需要已经初始化,这可以通过简单地提前声明或通过 @DependsOn 来实现,@DependsOn 强制在主引导线程中初始化受影响的 bean 之前触发后台初始化。
|
必须声明类型为 |
有条件地包含 @Configuration 类或 @Bean 方法
根据某些任意系统状态,有条件地启用或禁用完整的 @Configuration 类甚至单个 @Bean 方法通常很有用。一个常见的示例是使用 @Profile 注解仅在 Spring Environment 中启用了特定配置文件时激活 bean(有关详细信息,请参阅 Bean 定义配置文件)。
@Profile 注解实际上是通过使用一个更灵活的注解 @Conditional 实现的。@Conditional 注解指示在注册 @Bean 之前应咨询特定的 org.springframework.context.annotation.Condition 实现。
Condition 接口的实现提供了一个返回 true 或 false 的 matches(…) 方法。例如,以下清单显示了用于 @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 方便或必要的情况下,您有两种选择:通过使用例如 ClassPathXmlApplicationContext 以“XML 为中心”的方式实例化容器,或者通过使用 AnnotationConfigApplicationContext 和 @ImportResource 注解以“Java 为中心”的方式实例化容器,以根据需要导入 XML。
@Configuration 类的 XML 中心使用
可能更倾向于从 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="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>()
// ...
}
|
在 |
<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 注解实现“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);
}
@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.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>()
// ...
}