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 社群所有即將舉辦的活動。

檢視全部