在 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

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


@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

到目前為止,您已經看到,如果沒有候選 bean,除非 @Autowired 的 'required' 引數設定為 false,否則自動裝配將會失敗。鑑於自動裝配遵循“按型別”語義,如果超過一個 bean,無論 required 引數的值如何,都會導致失敗。例如,在將 @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

這可以透過切換到“按名稱”語義來解決——透過 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 配置的部落格文章——它提供了另一種替代 XML 的方式,但不會像註解那樣侵入您的應用程式程式碼。

獲取 Spring 時事通訊

透過 Spring 時事通訊保持聯絡

訂閱

取得先機

VMware 提供培訓和認證,助您快速提升。

瞭解更多

獲得支援

Tanzu Spring 透過一個簡單的訂閱即可為 OpenJDK™、Spring 和 Apache Tomcat® 提供支援和二進位制檔案。

瞭解更多

即將舉辦的活動

檢視 Spring 社群所有即將舉辦的活動。

檢視所有