Spring 2.1 中自定义注解配置和组件检测

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

注意:此帖子已于 2007 年 5 月 31 日更新,以反映 2.1-M2 正式版本的现状

两周前,我在 博客 中介绍了 Spring 2.1 的新基于注解的依赖注入功能,并提到我将在“本周晚些时候”提供更多信息。事实证明,这有点乐观,但好消息是此功能在此期间已经发展了很多。因此,要按照这里的示例操作,您需要下载 2.1-M2 正式版本(或者,如果您是阅读此更新条目最早的人之一,并且 M2 尚未发布,您应该至少获取夜间构建版本 #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);
	}
}

只要存根版本仍然不包含 @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 注解,但我希望展示各种过滤选项 - 不仅包括注解,还包括可分配类型甚至正则表达式。当然,主要目标是禁用 MessageRepository 的 JDBC 版本以支持存根版本,根据我的结果,这正是发生的事情


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 将导致自动装配失败。鉴于自动装配遵循“按类型”语义,无论“required”参数的值如何,多个 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

这可以通过切换到“按名称”语义来解决 - 通过 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”注解中(例如 @Repository("messageRepository"))显式提供 bean 名称要简单得多。如果您能够利用在整个应用程序中一致使用的命名约定,则提供您自己的策略可能很有用(此特定示例有点牵强,但希望证明该策略非常灵活,以便您可以遵循您自己的命名约定)。

到目前为止,所有 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 注解添加一个 include 过滤器(它不再包含在默认过滤器中)


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 社区中所有即将举行的活动。

查看全部