取得先机
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
为了让事情更有趣一些,我将添加 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 的方式,但不会像注解那样侵入您的应用程序代码。