Spring 2.1 中的注解驱动依赖注入

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

Spring 2.0 引入了注解支持和注解感知的配置选项,使用 Java 5(或更高版本)进行开发的 Spring 用户可以利用这些选项。

@Transactional 用于标记和配置事务定义
@Aspect (AspectJ) 用于定义切面,以及 @Pointcut 定义和通知(@Before、@After、@Around)
@Repository 用于指示一个类作为存储库(亦称为 Data Access Object 或 DAO)在运行
@Required 用于强制要求带注解的 Bean 属性必须提供一个值

借助 Spring 2.1,注解驱动配置这一主题得到了显著扩展,并将随着我们向 RC1 版本迈进而继续发展。实际上,现在可以通过注解来驱动 Spring 的依赖注入。此外,Spring 可以发现需要在应用程序上下文中配置的 Bean。

这篇博客文章将作为教程式介绍,分 10 个易于理解的步骤讲解基本特性。本周稍后我将跟进讲解一些更高级的特性和定制选项。如果您对替代配置选项感兴趣,还应该看看 Spring Java Configuration 项目和 这篇博客

本教程至少需要 Java 5,建议使用 Java 6(否则在步骤 1 结束时有一个单独的要求)。

步骤 1

获取 spring-framework-2.1-m1-with-dependencies.zip。解压存档后,您将在 'dist' 目录中找到 spring.jar 和 spring-mock.jar。将它们添加到您的 CLASSPATH,以及以下文件(所示路径相对于解压后的 2.1-m1 存档的 'lib' 目录)

  • asm/asm-2.2.3.jar
  • asm/asm-commons-2.2.3.jar
  • aspectj/aspectjweaver.jar
  • hsqldb/hsqldb.jar
  • jakarta-commons/commons-logging.jar
  • log4j/log4j-1.2.14.jar
(注意:如果您未运行在 Java 6 上,您还需要添加 j2ee/common-annotations.jar)

步骤 2

为示例提供接口和类。我尽量保持其简单,但又能演示主要功能。我将所有代码和配置都包含在一个名为 "blog" 的包中。我建议遵循同样的指南,以便示例能按原样运行;否则,请务必进行必要的修改。首先,GreetingService 接口

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

然后,一个简单的实现


public class GreetingServiceImpl implements GreetingService {
    private MessageRepository messageRepository;

    public void setMessageRepository(MessageRepository messageRepository) {
        this.messageRepository = messageRepository;
    }

    public String greet(String name) {
        Locale locale = Locale.getDefault();
        String message = messageRepository.getMessage(locale.getDisplayLanguage());
        return message + " " + name;
    }
}

由于该服务依赖于一个 MessageRepository,接下来定义该接口


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

目前,一个桩实现 (stub implementation)


public class StubMessageRepository implements MessageRepository {
    Map<String,String> messages = new HashMap<String,String>();

    public void initialize() {
        messages.put("English", "Welcome");
        messages.put("Deutsch", "Willkommen");
    }

    public String getMessage(String language) {
        return messages.get(language);
    }
}

步骤 3

定义 Spring 应用程序上下文的 Bean。请注意,我包含了一个新的 'context' 命名空间(注意:此处也包含了 'aop' 命名空间,它将在最后一步使用)


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       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/aop
           http://www.springframework.org/schema/aop/spring-aop-2.0.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context-2.1.xsd">
      
    <bean class="blog.GreetingServiceImpl"/>

    <bean class="blog.StubMessageRepository"/>

</beans>

显然,这个配置看起来有点稀疏。您可能已经猜到,'context' 命名空间很快就会发挥作用。

步骤 4

提供一个利用 Spring 基础支持类的简单测试用例

public class GreetingServiceImplTests extends AbstractDependencyInjectionSpringContextTests {
    private GreetingService greetingService;

    public void setGreetingService(GreetingService greetingService) {
        this.greetingService = greetingService;
    }

    @Override
    protected String[] getConfigLocations() {
        return new String[] { "/blog/applicationContext.xml" };
    }

    public void testEnglishWelcome() {
        Locale.setDefault(Locale.ENGLISH);
        String name = "Spring Community";
        String greeting = greetingService.greet(name);
        assertEquals("Welcome " + name, greeting);
    }

    public void testGermanWelcome() {
        Locale.setDefault(Locale.GERMAN);
        String name = "Spring Community";
        String greeting = greetingService.greet(name);
        assertEquals("Willkommen " + name, greeting);
    }
}

尝试运行测试,并注意它们因 NullPointerException 而失败。这是预料之中的,因为 GreetingServiceImpl 还没有被提供一个 MessageRepository。在接下来的两步中,您将分别添加注解来驱动依赖注入和初始化。

步骤 5

在 GreetingServiceImpl 的 setter 方法上提供 @Autowired 注解,例如

@Autowired
public void setMessageRepository(MessageRepository messageRepository) {
    this.messageRepository = messageRepository;
}

然后,将 'annotation-config' 元素(来自新的 'context' 命名空间)添加到您的配置中


<beans ... >
    
    <context:annotation-config/>

    <bean class="blog.GreetingServiceImpl"/>

    <bean class="blog.StubMessageRepository"/>

</beans> 

重新运行测试。它们仍然会失败,但仔细看会发现是新的问题。断言失败了,因为返回的消息是 null。这意味着 'messageRepository' 属性已经在 greeting 服务上设置了!现在,StubMessageRepository 只需要被初始化。

步骤 6

Spring 为初始化回调提供了几个选项:Spring 的 InitializingBean 接口或 XML 中的 'init-method' 声明。自 Spring 2.1 起,支持 JSR-250 注解 - 提供了另一种选择:@PostConstruct(@PreDestroy 注解可用于销毁回调,您很快就会看到)。在 StubMessageRepository 中,将注解添加到 initialize 方法中

@PostConstruct
public void initialize() {
    messages.put("English", "Welcome");
    messages.put("Deutsch", "Willkommen");
}

重新运行测试。这次应该能通过了!

步骤 7

@Autowired 注解也可用于基于构造函数的注入。如果您想尝试该选项,从 GreetingServiceImpl 中删除 setter 方法,并添加这个构造函数(然后重新运行测试)


@Autowired
public GreetingServiceImpl(MessageRepository messageRepository) {
    this.messageRepository = messageRepository;
}

如果愿意,您甚至可以使用字段注入。删除构造函数,直接将注解添加到字段上,然后重新运行测试。代码应如下所示


@Autowired
private MessageRepository messageRepository;

步骤 8

添加一个基于 JDBC 的 MessageRepository 存储库实现


public class JdbcMessageRepository implements MessageRepository {

    private SimpleJdbcTemplate jdbcTemplate;

    @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);
    }

}

请注意,除了用于初始化的 @PostConstruct,这里还使用了 @PreDestroy 来标记一个在销毁时调用的方法。这种实现有一个不清楚的地方:SimpleJdbcTemplate 将如何提供?一个选项是为模板提供一个 Bean 定义。另一个选项是某种方式向模板的构造函数提供一个 DataSource 实现。添加以下(带注解的)方法


@Autowired
public void createTemplate(DataSource dataSource) {
    this.jdbcTemplate = new SimpleJdbcTemplate(dataSource);
}

这展示了依赖注入与任意方法(而非传统的 'setter')协同工作。这将在下一步的课程中进行测试。

步骤 9

在 Spring 2.1 中,甚至可以*发现*“候选”Bean,而无需像上面那样在 XML 中显式提供。默认情况下会识别某些注解。这包括 @Repository 注解以及新的 @Component 注解。将这两个注解分别添加到 JdbcMessageRepository 和 GreetingServiceImpl 中


@Repository
public class JdbcMessageRepository implements MessageRepository { ... }

@Component
public class GreetingServiceImpl implements GreetingService { ... }

然后修改 XML 文件,删除现有的显式 Bean 定义,只需添加一个 component-scan 标签


<beans ... >
    <context:component-scan base-package="blog"/>
</beans>

然后,只需添加 DataSource Bean 定义和用于配置属性占位符的新标签


<beans ... >
    <context:component-scan base-package="blog"/>

    <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>

... 以及 jdbc.properties 文件本身


jdbc.driver=org.hsqldb.jdbcDriver
jdbc.url=jdbc:hsqldb:mem:blog
jdbc.username=sa
jdbc.password=

重新运行测试,您应该会看到绿色条,即使只在 XML 中定义了数据源。

步骤 10

最后,添加一个切面(@Aspect 注解默认也会自动检测)


@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);
    }
}

要激活自动代理生成,只需将以下标签添加到 xml 中


<aop:aspectj-autoproxy/>

重新运行测试,您应该会看到日志消息!

注意:扫描和配置过程可以在没有任何 XML 的情况下启动,并且可以定制(例如,检测您自己的注解和/或类型)。我将在下一篇文章中讨论这些特性及更多内容。

同时,我希望这篇文章能很好地达到其目的 - 提供这些新的 Spring 2.1 特性的实操经验。一如既往,我们期待社区的反馈,所以请随意留下评论!

获取 Spring 新闻通讯

订阅 Spring 新闻通讯,保持联系

订阅

领先一步

VMware 提供培训和认证,助力您飞速发展。

了解更多

获取支持

Tanzu Spring 通过一项简单的订阅,为 OpenJDK™、Spring 和 Apache Tomcat® 提供支持和二进制文件。

了解更多

即将举办的活动

查看 Spring 社区所有即将举办的活动。

查看全部