來看看如何使用 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,像是 AccountDAOJdbcImpl
、MessageDAOJdbcImpl
、UserService
等實例,實際上也可以寫在 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
也是類似,只不過自動綁定的對象包括了 AccountDAO
與 MessageDAO
的實例:
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 來取得 DataSource
、AccountDAOJdbcImpl
、MessageDAOJdbcImpl
或 UserService
實例了,然而,必須要有個物件來讀取 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 找到。