组合基于 Java 的配置 (Composing Java-based Configurations)
Spring 基于 Java 的配置功能允许你组合注解,这可以降低配置的复杂性。
使用 @Import 注解 (Using the @Import Annotation)
正如在 Spring XML 文件中使用 <import/> 元素来辅助配置模块化一样,@Import 注解允许从另一个配置类加载 @Bean 定义,如下例所示:
@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,如下例所示:
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 还支持引用普通的组件(Component)类,这类似于 AnnotationConfigApplicationContext.register 方法。如果你想通过使用几个配置类作为入口点来显式定义所有组件,从而避免组件扫描,这将非常有用。
注入对导入的 @Bean 定义的依赖
前面的示例虽然有效,但过于简单。在大多数实际场景中,Bean 之间跨配置类存在相互依赖。在使用 XML 时,这不是问题,因为不涉及编译器,你可以直接声明 ref="someBean" 并相信 Spring 会在容器初始化期间解决它。而使用 @Configuration 类时,Java 编译器会对配置模型施加约束,即对其它 Bean 的引用必须符合 Java 语法。
幸运的是,解决这个问题很简单。正如我们已经讨论过的,一个 @Bean 方法可以具有任意数量的参数,这些参数描述了 Bean 的依赖关系。考虑以下更现实的场景,其中有多个 @Configuration 类,每个类都依赖于在其它类中声明的 Bean:
@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 定义的 BeanPostProcessor 和 BeanFactoryPostProcessor。这些通常应声明为 static @Bean 方法,以免触发其所属配置类的实例化。否则,@Autowired 和 @Value 可能无法在配置类本身上工作,因为配置类实例创建的时间可能早于 AutowiredAnnotationBeanPostProcessor。
以下示例展示了一个 Bean 如何自动装配到另一个 Bean:
@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")
}提示
请注意,如果目标 Bean 只定义了一个构造函数,则不需要指定 @Autowired。
全限定导入的 Bean 以方便导航
在上述场景中,使用 @Autowired 工作良好并提供了所需的模块化,但在准确确定自动装配的 Bean 定义是在哪里声明时仍存在一定的模糊性。例如,作为查看 ServiceConfig 的开发人员,你如何确切知道 @Autowired AccountRepository Bean 是在哪里声明的?代码中并未显式指出,这也许没问题。Spring Tools IDE 支持提供的工具可以渲染显示所有内容连接情况的图表。此外,你的 Java IDE 可以轻松找到 AccountRepository 类型的所有声明和用法,并快速向你显示返回该类型的 @Bean 方法的位置。
如果这种模糊性不可接受,并且你希望在 IDE 中从一个 @Configuration 类直接导航到另一个类,请考虑自动装配配置类本身。以下示例展示了如何实现:
@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 类来在一定程度上缓解。考虑以下示例:
@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 定义的单例的启动 (Influencing the Startup of @Bean-defined Singletons)
如果你想影响某些单例 Bean 的启动创建顺序,可以考虑将其中一些声明为 @Lazy,以便在首次访问时创建,而不是在启动时创建。
@DependsOn 强制先初始化某些其它 Bean,确保指定的 Bean 在当前 Bean 之前创建,这超出了后者直接依赖所暗示的范围。
后台初始化 (Background Initialization)
从 6.2 版本开始,有一个后台初始化选项:@Bean(bootstrap=BACKGROUND) 允许分出特定的 Bean 进行后台初始化,涵盖了上下文启动时每个此类 Bean 的整个创建步骤。
具有非延迟注入点(non-lazy injection points)的依赖 Bean 会自动等待该 Bean 实例完成。所有常规后台初始化都强制在上下文启动结束时完成。只有另外标记为 @Lazy 的 Bean 才被允许稍后完成(直到第一次实际访问)。
后台初始化报通常与依赖 Bean 中的 @Lazy(或 ObjectProvider)注入点配合使用。否则,当实际需要尽早注入后台初始化的 Bean 实例时,主引导线程将会阻塞。
这种形式的并发启动适用于单个 Bean:如果此类 Bean 依赖于其它的 Bean,则它们需要已经初始化,要么只是通过更早声明,要么通过 @DependsOn 强制在触发受影响 Bean 的后台初始化之前,在主引导线程中完成初始化。
注意
必须声明一个类型为 Executor 的 bootstrapExecutor Bean,后台引导才能真正激活。否则,后台标记将在运行时被忽略。
引导执行器(bootstrap executor)可以是一个仅用于启动目的的有界执行器,也可以是一个也用于其它目的的共享线程池。
有条件地包含 @Configuration 类或 @Bean 方法
根据某些任意系统状态有条件地启用或禁用完整的 @Configuration 类甚至单个 @Bean 方法通常很有用。一个常见的例子是使用 @Profile 注解,仅当 Spring Environment 中启用了特定配置文件时才激活 Bean(详见 Bean 定义配置文件)。
@Profile 注解实际上是使用一个名为 @Conditional 的更灵活配置来实现的。@Conditional 注解指明了在注册 @Bean 之前应咨询的特定 org.springframework.context.annotation.Condition 实现。
Condition 接口的实现提供了一个返回 true 或 false 的 matches(…) 方法。例如,以下列表显示了用于 @Profile 的实际 Condition 实现:
@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().matchesProfiles((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.matchesProfiles(*value as Array<String>)) {
return true
}
}
return false
}
return true
}详见 @Conditional Javadoc。
结合使用 Java 和 XML 配置 (Combining Java and XML Configuration)
Spring 对 @Configuration 类的支持并不旨在完全取代 Spring XML。某些设施(如 Spring XML 命名空间)仍然是配置容器的理想方式。在 XML 方便或必要的情况下,你有一个选择:要么以“以 XML 为中心”的方式实例化容器(例如使用 ClassPathXmlApplicationContext),要么以“以 Java 为中心”的方式实例化容器(使用 AnnotationConfigApplicationContext 和 @ImportResource 注解根据需要导入 XML)。
以 XML 为中心的 @Configuration 类用法 (XML-centric Use of @Configuration Classes)
从 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 配置类:
@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>
<!-- 启用对 @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=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 保持在最低限度。
以下示例显示了如何使用 @ImportResource 注解来实现根据需要使用 XML 的“以 Java 为中心”配置:
@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=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>()
// ...
}补充教学 —— 多种配置方式的“混搭”艺术
在一个成熟的企业级 Spring 应用中,配置往往不是单一的,而是多种方式的组合。理解它们的优先级和适用场景是进阶的关键。
1. @Import 的深层威力@Import 不仅仅能导入 @Configuration 类。
- 普通类:如果你导入一个普通类,Spring 会把它当成一个简单的
@Component注册进去。 ImportSelector和ImportBeanDefinitionRegistrar:这是 Spring Boot 自动配置(Auto-configuration)的基石。通过它们,你可以根据特定的逻辑动态地注入 Bean。
2. 为什么推荐“以 Java 为中心”的混合配置?
- 代码即配置:Java 配置可以让你享受类型安全、重构支持和 IDE 导航。
- 保留 XML 的长处:XML 仍然是处理复杂命名空间(如 AOP 配置、事务管理配置)的利器。通过
@ImportResource,你可以把这些沉重的、很少改动的 XML 隐藏在 Java 类背后,让主干架构清晰可见。
3. Spring 6.2 引入的 BACKGROUND 引导:性能利器 这是文档中提到的一个非常现代的特性。
- 背景:随着微服务越来越重,Bean 的初始化顺序成为了系统启动的瓶颈。
- 作用:标记为
BACKGROUND的 Bean 会在独立的线程中并发初始化。 - 配合技巧:一定要配合
@Lazy注入点。如果主线程在后台 Bean 还没造好时就急着要用,主线程还是会被阻塞。这个特性适合那些体积庞大(如大型客户端连接池、模型加载)但启动初期不是强依赖的单例 Bean。
4. 注入配置类本身 vs 注入 Bean 依赖
- 参数注入(推荐):
public Service service(Repository repo) { ... }这种方式是最清晰的。 - 注入 Config 类:
@Autowired private RepoConfig repoConfig;这种方式在大型项目中通过repoConfig.repo()显式调用,有助于在 IDE 中追踪依赖来源,“哪里不爽点哪里”,特别适合于排查多个同类型 Bean 的冲突问题。
5. 优先级陷阱 如果 XML 中定义了一个 Bean,Java 配置中也定义了一个同名的 @Bean,谁会赢? 结论:在 Spring 的默认行为中,后加载的定义会覆盖前面的(除非显式关闭了 allowBeanDefinitionOverriding)。在混合配置中,通常是 Java 配置中 @ImportResource 导入的 XML 具有最后发言权(具体取决于加载顺序)。在大型项目中,建议通过明确的命名规范避开这种覆盖风险。