组合基于 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开始,@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
注入以及其他功能。
确保您以这种方式注入的依赖项仅限于最简单的类型。 避免在同一配置类的 此外,在通过 |
以下示例显示了一个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")
}
@Configuration 类中的构造函数注入仅从Spring Framework 4.3开始受支持。另请注意,如果目标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() {
// 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,则这些Bean需要已经初始化,要么简单地通过更早声明,要么通过@DependsOn
,它强制在触发受影响Bean的后台初始化之前在主引导线程中初始化。
必须声明类型为 引导执行器可能只是一个用于启动目的的有界执行器,或者是一个也用于其他目的的共享线程池。 |
有条件地包含@Configuration
类或@Bean
方法
根据某些任意系统状态有条件地启用或禁用完整的@Configuration
类甚至单个@Bean
方法通常很有用。一个常见的示例是使用@Profile
注解仅在Spring Environment
中启用了特定配置文件时激活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
类支持并非旨在完全替代 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://127.0.0.1/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>
使用 @ImportResource
以 @Configuration
类为中心的 XML 用法
在 @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://127.0.0.1/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>()
// ...
}