組合基於 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()
}

現在,在實例化 Context 時,不需要同時指定 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 類別在 Context 初始化期間很早就被處理,並且強制以此方式注入相依性可能會導致意外的早期初始化。盡可能採用基於參數的注入,如先前的範例所示。

避免在同一組態類別上的 @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 型別的所有宣告和使用,並快速向您顯示傳回該型別的 @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 定義的 Singleton 的啟動

如果您想要影響某些 Singleton Bean 的啟動建立順序,請考慮將其中一些宣告為 @Lazy,以便在首次存取時建立,而不是在啟動時建立。

@DependsOn 強制某些其他 Bean 先行初始化,確保指定的 Bean 在目前的 Bean 之前建立,超出後者的直接相依性所暗示的範圍。

背景初始化

從 6.2 開始,有一個背景初始化選項:@Bean(bootstrap=BACKGROUND) 允許挑選特定的 Bean 進行背景初始化,涵蓋每個此類 Bean 在 Context 啟動時的整個 Bean 建立步驟。

具有非延遲注入點的相依 Bean 會自動等待 Bean 實例完成。所有常規背景初始化都會在 Context 啟動結束時強制完成。只有另外標記為 @Lazy 的 Bean 才允許稍後完成(直到首次實際存取)。

背景初始化通常與相依 Bean 中的 @Lazy(或 ObjectProvider)注入點一起使用。否則,當需要盡早注入實際的背景初始化 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://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>

@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)
	}

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