取得領先優勢
VMware 提供培訓和認證,以加速您的進度。
了解更多注意:此文章已於 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 的另一種替代方案,但沒有註解對您的應用程式碼的侵入性。