使用 Spring DI


來看看如何使用 Spring DI 的功能,將〈在 DI 之前〉中相依注入的職責,委由 Spring 來完成。

為了要能使用 Spring DI,可以在 build.gradle 設定、管理 Spring 相關模組:

apply plugin: 'java-library'

repositories {
    jcenter()
}

dependencies {
    testImplementation 'junit:junit:4.12'

    compile 'com.h2database:h2:1.4.196'

    compile 'org.springframework:spring-context:5.1.2.RELEASE'
}

當然,物件與物件之間如何相依,必須告訴 Spring,它才有辦法將物件之間的關係聯繫起來。

有時候需要使用 XML 來設定這些資訊,無論是為了維護舊專案,或者是集中某些設定資訊之時,這就請參考我撰寫過的〈舊版 Spring〉文件,以及《Spring 2.0 技術手冊》。

Spring 目前支援標註(Annotation)的設定方式,標註的優點之一,是可以將與程式碼相關的設定,就放在程式碼之中,讓這些設定,也成為閱讀、理解程式碼時的幫手。

對於必須集中在某處的設定,例如整個應用程式都會使用的全域資訊,Spring 也善用標註,可將設定集中在一個 .java 之中,雖然副檔名為 .java,不過,應將之當成是一個設定檔來看待,Spring 稱這種設定方式為 JavaConfig,例如:

package cc.openhome;

import javax.sql.DataSource;

import org.h2.jdbcx.JdbcDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan
public class AppConfig {
    @Bean
    public DataSource getDataSource() {
        JdbcDataSource dataSource = new JdbcDataSource();
        dataSource.setURL("jdbc:h2:tcp://localhost/c:/workspace/SpringDI/gossip");
        dataSource.setUser("caterpillar");
        dataSource.setPassword("12345678");
        return dataSource;
    }
}

由於 JavaConfig 也是個 Java 類別,使用它作為設定檔的好處之一是,可以在其中撰寫 Java 程式碼,因此對於更複雜的設定,像是過去 XML 方式無法滿足的設定,就可以使用程式碼來組裝,以便滿足設定需求。在這邊,由於 H2 的 JdbcDataSource 原始碼並不在我們專案控制之內,因此使用程式碼的方式設定相關資訊。

為了令事情在一開始不要太過複雜,這個設定檔中,仍然是寫死了 JDBC URL 等資訊,之後會談到,如何將這些撰寫在 .properties。

由 Spring 管理的實例稱之為 Bean ,@Bean 告訴 Spring,getDataSource 傳回的實例會作為一個 Bean 元件,雖然程式碼中撰寫了 new,然而,不要以為之後應用程式要取得 Bean,每次都會呼叫一次 getDataSource 而產生一個新的 DataSource 實例。

是否產生多個 Bean 是可以設定的,而 Spring 會依設定來控制,這就是為何應該將 JavaConfig 看成是設定檔的原因;在沒有任何額外設定下,Spring 預設只會建立一個 Bean 實例。

至於其他的 Bean,像是 AccountDAOJdbcImplMessageDAOJdbcImplUserService 等實例,實際上也可以寫在 AppConfig 裏,然而由於它們的原始碼在我們的控制之中,更方便的作法是透過 Spring 來自動掃描與自動綁定。

為了能 Spring 能自動掃描 Bean 的存在,AppConfig 上標註了 @ComponentScan,如此 Spring 預設會自動掃描同一套件以及其子套件下,是否有 Bean 元件的存在。

接著可以來處理 AccountDAOJdbcImpl 的自動綁定:

package cc.openhome.model;

... 略

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class AccountDAOJdbcImpl implements AccountDAO {
    private DataSource dataSource;

    public AccountDAOJdbcImpl(@Autowired DataSource dataSource) {
        this.dataSource = dataSource;
    }

    ... 略
}

在這邊看到 DataSource 建構式上的 dataSource 參數旁標註了 @Autowired,當 Spring 在自身管理的 Bean 中發現了相同類型的實例,會自動設定給 dataSource,而 AccountDAOJdbcImpl 本身也會被 Spring 作為 Bean 管理,因此可以使用 @Component 來標註,表示這也是個 Bean 元件。

MessageDAOJdbcImpl 也做了類似的標註:

package cc.openhome.model;

... 略

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class MessageDAOJdbcImpl implements MessageDAO {
    private DataSource dataSource;

    public MessageDAOJdbcImpl(@Autowired DataSource dataSource) {
        this.dataSource = dataSource;
    }

    ... 略
}

接下來 UserService 也是類似,只不過自動綁定的對象包括了 AccountDAOMessageDAO 的實例:

package cc.openhome.model;

... 略

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class UserService {
    private final AccountDAO acctDAO;
    private final MessageDAO messageDAO;

    public UserService(@Autowired AccountDAO acctDAO, @Autowired MessageDAO messageDAO) {
        this.acctDAO = acctDAO;
        this.messageDAO = messageDAO;
    }

    ... 略
}

如上設定與標註之後,就可以透過 Spring 來取得 DataSourceAccountDAOJdbcImplMessageDAOJdbcImplUserService 實例了,然而,必須要有個物件來讀取 AppConfig 設定檔:

package cc.openhome;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import cc.openhome.model.UserService;

public class Main {
    public static void main(String[] ags) {
        ApplicationContext context = 
                new AnnotationConfigApplicationContext(cc.openhome.AppConfig.class);

        UserService userService = context.getBean(cc.openhome.model.UserService.class);

        userService.messages("caterpillar")
                   .forEach(message -> {
                       System.out.printf("%s\t%s%n",
                               message.getLocalDateTime(),
                               message.getBlabla());
                   });
    }
}

由於這邊使用標註類別來作為設定檔,因此透過 AnnotationConfigApplicationContext 來讀取,在建立 ApplicationContext 實例之後,可以透過 getBean 方法來取得想要的 Bean,至於這些實例之間的相依性,就是由 Spring 來幫忙搓合。

可以看到,你必須按照框架的規定,來完成相關的設定,如果你的應用程式沒有一定的複雜度,而使用框架換來的好處,沒有超過遵守框架規定而犧牲掉的自由,面對這些設定,大概只會覺得麻煩吧!

當然,設定確實還是麻煩的,實際上為了省去設定的繁瑣,不少框架都會有慣例優於設定(Convention over Configuration)的設計,也就是沒有設定時自動採用預設值,Spring 也有這方面的方案,像是 Spring Boot,這是之後會談到的課題。

這個範例的完成品,可以在 SpringDI 找到。