組合基於 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.classConfigB.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 定義 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() {
		// 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")
}
從 Spring Framework 4.3 起,@Configuration 類中才支援構造器注入。另請注意,如果目標 Bean 只定義了一個構造器,則無需指定 @Autowired

完全限定匯入的 Bean 以便於導航

在前面的場景中,使用 @Autowired 效果很好,並提供了所需的模組化,但確定自動裝配的 Bean 定義到底是在哪裡宣告的仍然有些模糊。例如,作為一個檢視 ServiceConfig 的開發者,你如何確切知道 @Autowired AccountRepository Bean 是在哪裡宣告的?程式碼中沒有顯式說明,但這可能完全沒問題。請記住,Spring Tools for Eclipse 提供了可以渲染圖表來展示所有內容如何連線的工具,這可能就是你所需要的一切。此外,你的 Java IDE 可以輕鬆找到 AccountRepository 型別的 J所有宣告和使用,並快速向你展示返回該型別的 @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 的後臺初始化。

必須宣告一個型別為 ExecutorbootstrapExecutor Bean,以便後臺引導實際生效。否則,後臺標記在執行時將被忽略。

引導執行器可以是一個僅用於啟動目的的有限執行器,或者是一個也用於其他目的的共享執行緒池。

有條件地包含 @Configuration 類或 @Bean 方法

通常,基於任意系統狀態有條件地啟用或停用完整的 @Configuration 類甚至單個 @Bean 方法會很有用。一個常見的例子是使用 @Profile 註解,僅當 Spring Environment 中啟用了特定的 Profile 時才啟用 Bean(詳情請參閱Bean 定義 Profile)。

@Profile 註解實際上是透過使用一個更靈活的註解來實現的,該註解稱為 @Conditional@Conditional 註解指明瞭在註冊 @Bean 之前應該參考的特定 org.springframework.context.annotation.Condition 實現。

Condition 介面的實現提供了一個 matches(…​) 方法,該方法返回 truefalse。例如,以下列表顯示了用於 @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 方便或必要的情況下,您可以選擇:或者使用例如 ClassPathXmlApplicationContext 以“XML 為中心”的方式例項化容器,或者使用 AnnotationConfigApplicationContext@ImportResource 註解以“Java 為中心”的方式按需匯入 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:///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 類為中心,結合 @ImportResource 使用 XML

在將 @Configuration 類作為配置容器的主要機制的應用中,可能仍然需要使用至少一部分 XML。在這種情況下,你可以使用 @ImportResource 註解,並只定義你需要的那部分 XML。這樣做可以實現一種“Java 中心”(Java-centric)的方式來配置容器,並將 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)
	}

}
properties-config.xml
<beans>
	<context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
</beans>
jdbc.properties
jdbc.url=jdbc:hsqldb:hsql:///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>()
	// ...
}