在 Spring 2.1 中自訂註解配置和元件偵測

工程 | Mark Fisher | 2007 年 5 月 29 日 | ...

注意:此文章已於 2007 年 5 月 31 日更新,以反映 2.1-M2 正式版的狀態

兩週前,我在部落格上發表了關於 Spring 2.1 的新註解驅動依賴注入功能,我提到我會在「本週稍晚」分享更多資訊。 結果證明有點樂觀,但好消息是,這段時間以來,該功能已經有了很大的發展。 因此,若要跟著此處的範例進行操作,您需要下載 2.1-M2 正式版 (或者,如果您是第一個閱讀此更新條目的人,且 M2 尚未可用,您應該至少抓取 nightly build #115,您可以從這裡下載)。

我想演示的第一件事是如何建立不使用任何 XML 的應用程式環境。 對於那些使用過 Spring 的 BeanDefinitionReader 實作的人來說,這看起來會非常熟悉。 不過,在建立環境之前,我們需要在類別路徑上有一些「候選」bean。 繼續我之前部落格中的範例,我有以下兩個介面


public interface GreetingService {
	String greet(String name);
}

public interface MessageRepository {
	String getMessage(String language);
}

...以及這些相應的實作


@Component
public class GreetingServiceImpl implements GreetingService {

	@Autowired
	private MessageRepository messageRepository;
	
	public String greet(String name) {
		Locale locale = Locale.getDefault();
		if (messageRepository == null) {
			return "Sorry, no messages";
		}
		String message = messageRepository.getMessage(locale.getDisplayLanguage());
		return message + " " + name;
	}
}

@Repository
public class StubMessageRepository implements MessageRepository {

	Map<String,String> messages = new HashMap<String,String>();
	
	@PostConstruct
	public void initialize() {
		messages.put("English", "Welcome");
		messages.put("Deutsch", "Willkommen");
	}
	
	public String getMessage(String language) {
		return messages.get(language);
	}
}

現在正如我所承諾的...組裝這個無可否認的微不足道的「應用程式」,而無需任何 XML


Locale.setDefault(Locale.GERMAN);
GenericApplicationContext context = new GenericApplicationContext();
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context);
scanner.scan("blog"); // the parameter is 'basePackage'
context.refresh();
GreetingService greetingService = (GreetingService) context.getBean("greetingServiceImpl");
String message = greetingService.greet("Standalone Beans");
System.out.println(message);

結果


Willkommen Standalone Beans

從本質上講,這與使用來自新「context」命名空間的 component-scan XML 元素時的行為完全相同 (如我之前的部落格中所演示的)。 但是,我想著重於一些較新的功能以及自訂選項。 首先,我將從 StubMessageRepository 中移除 @Repository 註解,然後重新執行測試,這會產生以下例外情況


org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'greetingServiceImpl': Autowiring of fields failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire field: private blog.MessageRepository blog.GreetingServiceImpl.messageRepository; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No unique bean of type [blog.MessageRepository] is defined: expected single bean but found 0

顯然,預設情況下,@Autowired 註解表示必要的依賴關係,但可以透過新增具有「false」值的「required」參數輕鬆切換,例如


@Component
public class GreetingServiceImpl implements GreetingService {

	@Autowired(required=false)
	private MessageRepository messageRepository;
	...

修改後的結果


Sorry, no messages

為了讓事情更有趣一點,我將新增 JDBC 版本的 MessageRepository (也來自之前的文章)


@Repository
public class JdbcMessageRepository implements MessageRepository {

	private SimpleJdbcTemplate jdbcTemplate;

	@Autowired
	public void createTemplate(DataSource dataSource) {
		this.jdbcTemplate = new SimpleJdbcTemplate(dataSource);
	}
	
	@PostConstruct
	public void setUpDatabase() {
		jdbcTemplate.update("create table messages (language varchar(20), message varchar(100))");
		jdbcTemplate.update("insert into messages (language, message) values ('English', 'Welcome')");
		jdbcTemplate.update("insert into messages (language, message) values ('Deutsch', 'Willkommen')");
	}
	
	@PreDestroy
	public void tearDownDatabase() {
		jdbcTemplate.update("drop table messages");
	}
	
	public String getMessage(String language) {
		return jdbcTemplate.queryForObject("select message from messages where language = ?", String.class, language);
	}
}

只要 stub 版本包含 @Repository 註解,重新執行測試現在將產生以下例外情況


org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'greetingServiceImpl': Autowiring of fields failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire field: private blog.MessageRepository blog.GreetingServiceImpl.messageRepository; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jdbcMessageRepository': Autowiring of methods failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire method: public void blog.JdbcMessageRepository.createTemplate(javax.sql.DataSource); nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No unique bean of type [javax.sql.DataSource] is defined: expected single bean but found 0

顯然,由於環境中沒有 DataSource 可用,因此導致了一系列的自動裝配失敗。 但是,作為測試驅動開發的堅定信徒,我希望在設定基礎架構之前單元測試我的實作。 幸運的是,掃描器具有相當的可自訂性,我可以提供篩選器,例如


Locale.setDefault(Locale.GERMAN);
GenericApplicationContext context = new GenericApplicationContext();

boolean useDefaultFilters = false;

ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context, useDefaultFilters);
scanner.addExcludeFilter(new AssignableTypeFilter(JdbcMessageRepository.class));
scanner.addIncludeFilter(new AnnotationTypeFilter(Component.class));
scanner.addIncludeFilter(new RegexPatternTypeFilter(Pattern.compile("blog\\.Stub.*")));
scanner.scan("blog");

context.refresh();
GreetingService greetingService = 
             (GreetingService) context.getBean("greetingServiceImpl");
String message = greetingService.greet("Standalone Beans");
System.out.println(message);

正如您所看到的,我停用了「defaultFilters」並明確新增了我自己的。 在這種情況下,這並非完全必要,因為預設值包含 @Component 和 @Repository 註解,但我想展示各種篩選選項 - 不僅包括註解,還包括可分配的類型,甚至是正則表示式。 當然,主要目標是停用 JDBC 版本的 MessageRepository,而支持 stub,而根據我的結果,這正是發生的情況


Willkommen Standalone Beans

假設我現在準備好整合 JDBC 版本,我可能需要包含一些用於 DataSource 的 XML 配置,例如


<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context-2.1.xsd">
      
    <context:property-placeholder location="classpath:blog/jdbc.properties"/>
    
    <bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    	<property name="driverClassName" value="${jdbc.driver}"/>
    	<property name="url" value="${jdbc.url}"/>
    	<property name="username" value="${jdbc.username}"/>
    	<property name="password" value="${jdbc.password}"/>
    </bean>
	
</beans>

然後,我可以將掃描與 XmlBeanDefinitionReader 結合使用 (請注意,我已恢復為僅預設篩選器)


Locale.setDefault(Locale.GERMAN);
GenericApplicationContext context = new GenericApplicationContext();

ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context);
scanner.scan("blog");

BeanDefinitionReader reader = new XmlBeanDefinitionReader(context);
reader.loadBeanDefinitions("classpath:/blog/dataSource.xml");

context.refresh();
GreetingService greetingService = (GreetingService) context.getBean("greetingServiceImpl");
String message = greetingService.greet("Hybrid Beans");
System.out.println(message);

環境包含掃描的 bean 以及 XML 中定義的 bean,結果是


Willkommen Hybrid Beans

到目前為止,您已經看到,除非 @Autowired 的「required」參數設定為 false,否則 0 個候選 bean 將導致自動裝配失敗。 鑑於自動裝配遵循「by-type」語義,無論 required 參數的值如何,超過 1 個 bean 都會導致失敗。 例如,在將 @Repository 註解新增回 StubMessageRepository 並重新執行先前的範例後,我收到以下例外情況


org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'greetingServiceImpl': Autowiring of fields failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire field: private blog.MessageRepository blog.GreetingServiceImpl.messageRepository; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No unique bean of type [blog.MessageRepository] is defined: expected single bean but found 2

可以透過切換到「by-name」語義來解決此問題 - 透過 Spring 2.1 對 JSR-250 @Resource 註解的支援來完成


@Component
public class GreetingServiceImpl implements GreetingService {

	@Resource(name="jdbcMessageRepository")
	private MessageRepository messageRepository;
	...

您可能在先前的範例中注意到,bean 名稱 (如 @Resource 註解中所指定的) 預設為去大寫的非限定類別名稱。 若要覆寫此行為,可以新增您自己的 BeanNameGenerator 策略實作,例如


private static class MyBeanNameGenerator implements BeanNameGenerator {

	public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
		String fqn = definition.getBeanClassName();
		return Introspector.decapitalize(fqn.replace("blog.", "").replace("Jdbc", ""));
	}
}

然後將此策略提供給掃描器會覆寫預設值


ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context);
scanner.setBeanNameGenerator(new MyBeanNameGenerator());
scanner.scan("blog");

因此,可以相應地修改 @Resource 註解中指定的名稱


@Resource(name="messageRepository")
private MessageRepository messageRepository;

注意:當依賴容器進行自動裝配時,預設命名策略通常已足夠 (即,它在「幕後」工作)。 因此,命名策略僅應在您將在其他地方按名稱引用 bean 的情況下才考慮。 即使那樣,對於隔離的情況,在「stereotype」註解中明確提供 bean 名稱要簡單得多 (例如 @Repository("messageRepository"))。 如果您可以利用在整個應用程式中一致使用的命名慣例,則提供您自己的策略會很有用 (此特定範例有點牽強,但希望表明該策略非常寬容,以便您可以遵循自己的命名慣例)。

到目前為止,所有 bean 都已配置為預設的「singleton」範圍,但範圍解析是掃描器的另一個可自訂策略。 預設值將在每個元件上尋找 @Scope 註解。 例如,若要將 GreetingServiceImpl 配置為「prototype」,只需新增以下內容


@Scope("prototype")
@Component
public class GreetingServiceImpl implements GreetingService { .. }

雖然預設的註解方法非常簡單,但範圍幾乎總是特定於部署的考量因素。 因此,它通常不屬於類別層級或原始碼中。 基於這些原因,以下策略介面可用,並且可以像先前範例中的 BeanNameGenerator 一樣在掃描器上指定


public interface ScopeMetadataResolver {
	ScopeMetadata resolveScopeMetadata(BeanDefinition definition);
}

請注意,名稱產生和範圍解析策略也可以在基於 XML 的配置中提供,例如


<context:component-scan base-package="blog"
                        name-generator="blog.MyBeanNameGenerator"
                        scope-resolver="blog.MyScopeMetadataResolver"/>

同樣,可以將自訂篩選器新增為子元素


<context:component-scan base-package="blog" use-default-filters="false">
    <context:include-filter type="annotation" expression="org.springframework.stereotype.Component"/>
    <context:include-filter type="regex" expression="blog\.Stub.*"/>
    <context:exclude-filter type="assignable" expression="blog.JdbcMessageRepository"/>
</context:component-scan>

我知道這個條目已經涵蓋了很多內容,但還有最後一個主題我想涵蓋。 在之前的文章中,我包含了一個具有 <aop:aspectj-autoproxy/> 元素的切面。 現在我想演示如何使用我們的獨立版本新增自動代理行為。 首先,切面本身 (與上次相同)


@Aspect
public class ServiceInvocationLogger {

	private int invocationCount;
	
	@Pointcut("execution(* blog.*Service+.*(..))")
	public void serviceInvocation() {}
	
	@Before("serviceInvocation()")
	public void log() {
		invocationCount++;
		System.out.println("service invocation #" + invocationCount);
	}
}

接下來,我需要為 @Aspect 註解新增一個包含篩選器 (它不再包含在預設篩選器中)


scanner.addIncludeFilter(new AnnotationTypeFilter(Aspect.class));
scanner.scan("blog");

最後,我需要註冊基於 AspectJ 註解的自動代理建立器 (在對環境呼叫 refresh() 之前)


AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(context);
context.refresh();

結果


service invocation #1
Willkommen Hybrid Beans

希望此條目和前一篇為 Spring 2.1 的這些新功能提供了足夠的介紹。 如果您願意,您現在應該對如何將元件掃描和註解配置與少量「傳統」Spring XML 配置結合使用有相當的了解。 此外,透過提供您自己的篩選器、名稱產生器和範圍解析器,您可以自訂配置過程。 正式版 2.1-M2 在參考文件中包含更詳細的資訊。

請繼續關注這個 Interface21 團隊部落格,以了解更多新功能,因為我們將繼續從目前的里程碑階段邁向 Spring 2.1 的 RC1 版本,如果您對註解驅動的配置不特別感興趣,那麼您可能需要關注 Costin Leau 即將發布的關於 Spring Java Configuration 的部落格 - 它提供了 XML 的另一種替代方案,但沒有註解對您的應用程式碼的侵入性。

取得 Spring 電子報

與 Spring 電子報保持聯繫

訂閱

取得領先優勢

VMware 提供培訓和認證,以加速您的進度。

了解更多

取得支援

Tanzu Spring 在一個簡單的訂閱中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位檔。

了解更多

即將到來的活動

查看 Spring 社群中所有即將到來的活動。

查看全部