java.util包裏面的類,另外一個在jdbc裏面應用的很多。從表面上看起來他們之間似乎沒有多少的聯繫。實際上DriverManager對ServiceLoader的使用可以達到一種巧妙的效果。在這裏我想探討一下DriverManager使用到的一種設計思路以及對我們後續解決類似問題的指導。
ServiceLoader
ServiceLoader是jdk6裏面引進的一個特性。在第一次碰到的時候還是有點不太理解。從官方的文檔來說,它主要是用來裝載一系列的service provider。而且ServiceLoader可以通過service provider的配置文件來裝載指定的service provider。
相信看完前面的這一段描述,依然還是會一頭霧水。那麼我們就結合一個具體的示例來講講吧。假定我們要定義一個接口MessageService,它的定義如下:
- package com.test.service;
- public interface MessageService {
- String getMessage();
- }
RawMessage:
- package com.test.raw;
- import com.test.service.MessageService;
- public class RawMessage implements MessageService {
- public String getMessage() {
- return "Raw message";
- }
- }
- package com.test.format;
- import com.test.service.MessageService;
- public class FormattedMessage implements MessageService {
- public String getMessage() {
- return "Formatted message";
- }
- }
針對前面這個接口來說,我們這裏的兩個實現可以說是兩個service provider。因爲按照前面這個接口的規範,我們有兩個具體的不同實現。他們也許對應着不同的情形。
OK,這裏我們理解了service provider的意思。那麼有了這些個接口和service provider,他們有什麼用呢?他們和Service Loader有什麼關係呢?我們繼續往下討論。
既然前面提到Service Loader是用來裝載這些service provider的,那就是說我們可以使用它來將這些具體實現的類引入進來使用。我們在原來代碼的目錄下面創建一個META-INF/services的目錄,並創建一個com.test.service.MessageService的文件。這個文件名比較有意思,它是和我們前面定義的MessageService類的全名一樣的。然後我們在這個文件裏保存如下的信息:
- com.test.raw.RawMessage
- com.test.format.FormattedMessage
整個項目的結構以及相關配置文件的佈局如下圖所示:
這裏我們注意到,META-INF文件夾是放在src這個代碼目錄下。前面這部分的配置文件到底是做什麼用的呢?他們爲什麼要命名成和類的全名一樣?我們看一下使用他們的示例代碼再來討論。
我們在原來的項目中增加如下的代碼:
- package com.test.messageconsumer;
- import java.util.ServiceLoader;
- import com.test.service.MessageService;
- public class MessageConsumer {
- public static void main(String[] args) {
- ServiceLoader<MessageService> serviceLoader =
- ServiceLoader.load(MessageService.class);
- for(MessageService service : serviceLoader) {
- System.out.println(service.getMessage());
- }
- }
- }
這部分代碼使用我們前面定義的類,它通過ServiceeLoader的load方法來裝載MessageService類型的對象。我們在一個循環裏去調用裝載的service裏的getMessage方法。如果運行這個程序,我們會發現一個如下的結果:
- Raw message
- Formatted message
現在如果我們從結果來反推那些代碼的作用,至少可以看到一點,我們這裏的ServiceLoader起作用了。它這個load方法雖然是load MessageService類型的class,實際上把MessageService的所有子類型都裝載了進來並可以使用。那麼java是怎麼知道我們有這麼兩個子類呢?再想想我們前面定義的META-INF下面的配置文件,我們就可以猜出來了。沒錯,java通過查找META-INF目錄下對應MessageService全名的文件,然後把文件裏所有的項讀取出來然後嘗試去裝載它們。我們可以嘗試一下去驗證他們,比如說我們把com.test.service.MessageService文件裏後面的com.test.format.FormattedMessage這一行給刪除了再去運行程序,我們會發現只有如下的輸出了:
- Raw message
可見,java也不是通過施了什麼魔法就找到我們定義的這些類,它是通過讀取這個配置文件才知道的。我們現在回過頭來看看前面的代碼。我們定義了一個接口和它的兩個實現,然後設置好配置文件後,一個使用他們的程序可以通過讀取配置文件來初始化具體的實現。在使用他們的代碼裏,我們直接操作的是MessageService這個接口,並不是具體的實現。從這一點來說,很符合我們面向對象中針對接口和抽象編程的原則。而且這個時候如果對依賴注入比較熟悉的人似乎也嗅到了這麼一絲的味道。確實,這裏可以說是一種依賴注入的一個簡單實現,我們通過配置文件可以提供一些特定的類給使用程序。當然,這裏針對ServiceLoader還有一個特定的限制,就是我們提供的這些具體實現的類必須提供無參數的構造函數,否則ServiceLoader就會報錯。
ServiceLoader使用的思考
目前來說,我們已經看到了ServiceLoader的一種簡單使用場景。在前面這個示例中,我們對接口的實現是在同一個工程裏面,如果我們需要使用他們的時候,完全沒必要通過ServiceLoader再裝載具體實現進來,我們完全可以通過一個ArrayList再將他們的具體實現一個個加進去就可以了。那麼,什麼時候用ServiceLoader比較合適呢?既然在同一個工程裏我們jvm可以直接裝載他們的實現,那麼很可能就是我們要裝載的實現不在同一個工程裏,可能是需要我們動態添加的,這個時候,他們的引入不是編譯時候加進來的,是在運行的時候加入的,我們不能像使用普通引入的靜態類庫那樣來使用他們。所以這就是ServiceLoader的優點所在了。比如說我們有一組接口,有的實現是我們本地的,我們可以在使用代碼裏直接引入進來。而有的卻是第三方實現的,他們可能會在運行的時候加入進來。那麼我們事先是不清楚的,也就不可能定死了他們的實現是哪個具體的類名。在前面ServiceLoader的使用裏,我不用把你具體的實現引用到代碼裏,而只是在配置文件裏指定就可以了。這一點也是我們後面要討論的DriverManager的一個重要的核心思想。
DriverManager的應用和設計思路
DriverManager是jdbc裏管理和註冊不同數據庫driver的工具類。從它設計的初衷來看,和我們前面討論的場景有相似之處。首先一個,針對一個數據庫 可能會存在着不同的數據庫驅動實現。我們在使用特定的驅動實現時不希望修改現有的代碼才能達到目的,而希望通過一個簡單的配置就可以達到效果。
比如說,我們現在有一個數據庫的驅動A,我們希望在程序裏使用它而不修改代碼。一種理想的選擇就是我們將驅動A的信息加入到一個配置文件中,程序通過讀取配置文件信息將A加載進來。而以後如果我們希望改用另外一個驅動B的時候,我們只需要將配置文件裏的信息修改成驅動B的。我們肯定不希望在代碼裏寫什麼registerDriver(new A());之類的代碼。尤其在有的情況下我們根本沒有使用這些驅動的源代碼。
DriverManager的設計
下圖是DriverManager相關的類圖:
在實際的應用中,我們每個具體的驅動都需要實現這裏的Driver接口。比如說mysql jdbc的驅動,它的方法聲明如下:
- public class Driver extends NonRegisteringDriver implements java.sql.Driver
從使用的角度來說,我們一般是通過如下的語句來實現裝載的:
- Class.forName(“com.mysql.jdbc.Driver”);
看到這一步的時候,大家估計還是會有點疑惑,這裏只是用的Class.forName方法,似乎和DriverManager沒關係啊。沒錯,這一步從表面上看沒什麼關係,實際上我們來看他們具體的行爲。Class.forName主要是做了什麼呢?它主要是要求JVM查找並裝載指定的類。這樣我們的類com.mysql.jdbc.Driver就被裝載進來了。而且在類被裝載進JVM的時候,它的靜態方法就會被執行。我們來看com.mysql.jdbc.Driver的實現代碼。在它的實現裏有這麼一段代碼:
- static {
- try {
- java.sql.DriverManager.registerDriver(new Driver());
- } catch (SQLException E) {
- throw new RuntimeException("Can't register driver!");
- }
- }
很明顯,這裏使用了DriverManager並將該類給註冊上去了。所以,對於任何實現前面Driver接口的類,只要在他們被裝載進JVM的時候註冊DriverManager就可以實現被後續程序使用。在實際項目中,如果我們需要有更大的裝載靈活性,可以把Class.forName裏的參數放到配置文件裏,需要裝載其他Driver的時候,改一下文件再重啓一下程序就可以了。
我們前面討論的這種場景主要針對某一個驅動的加載。在某些情況下,我們可能會這樣想,如果我們一個項目裏要同時用到多個數據庫呢?他們每個數據庫甚至用到的驅動都不同。那麼我們該怎麼辦呢?很顯然,我們可以考慮將這些所有可用的驅動先加載進來,然後再根據不同的需要來選擇。DriverManager裏面就有這麼一個方法可以事先加載已有的一些驅動。我們來看看DriverManager的一部分代碼:
- static {
- loadInitialDrivers();
- println("JDBC DriverManager initialized");
- }
這部分代碼是在DriverManager被加載進JVM執行的。而loadInitialDrivers()方法裏有一個關鍵的實現部分如下:
- AccessController.doPrivileged(new PrivilegedAction<Void>() {
- public Void run() {
- ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
- Iterator driversIterator = loadedDrivers.iterator();
- try{
- while(driversIterator.hasNext()) {
- println(" Loading done by the java.util.ServiceLoader : "+driversIterator.next());
- }
- } catch(Throwable t) {
- // Do nothing
- }
- return null;
- }
- });
在這部分代碼裏,我們看到了一個熟悉的身影,沒錯,ServiceLoader。這部分代碼將寫到配置文件裏的Driver都加載了進來。我們從前面ServiceLoader部分的介紹瞭解到,ServiceLoader.load()方法是將配置文件裏指定的所有類都裝載進來並調用他們的構造函數。而從前面com.mysql.jdbc.Driver的代碼裏我們就已經看到它們在被加載的時候又會調用DriverManager的registerDriver方法。這樣將所有配置的jdbc driver註冊和裝載的過程就完成了。
在DriverManager裏面,有一個ArrayList,就專門用來保存註冊好的Driver。它的定義如下:
- private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<DriverInfo>();
這裏使用了CopyOnWriteArrayList是因爲考慮到在多線程使用的環境下爲了保證線程安全。immutable的設計思路,你懂的。
現在,我們針對DriverManager的初始化和裝載過程做一個梳理。首先,DriverManager在一開始的時候會嘗試裝載配置文件裏設定的Driver以及系統環境屬性裏設定的Driver。作爲那些被加載的Driver實現,他們本身在被裝載時會在執行的static代碼段裏通過調用DriverManager.registerDriver()來把自身註冊到DriverManager的registeredDrivers列表中。這樣後面就可以通過得到的Driver來取得連接了。這種方式是JDBC4最新使用的方式。在之前使用的時候可以採用registerDriver和Class.forName的方法。爲了保證不和特定的具體Driver耦合,採用Class.forName的方式會更好一點。不過現在連Class.forName的方式都省了。這樣不管是顯式還是隱式的加載驅動都保證這些驅動被保存到了DriverManager裏的registeredDrivers列表裏。
我們再來看看對Driver的選擇,這裏用的Class.forName,它通過傳入的String類型參數來確定具體的對象類型。雖然這裏返回的只是一個Class對象。我們後面可以通過它的createInstance方法來創建具體的對象。這種手法也是很多工廠方法模式裏慣用的手段。在這種方法裏,我們通過創建好一系列的對象信息,然後根據傳入的參數類型來創建具體的對象。但是返回的還是一個抽象類型。這樣就完美的實現了和具體實現的解耦以及對象創建過程的封裝。
現在看來DriverManager實現所採用的思想和工廠模式有很多近似的地方。在我們一些具體問題的設計思路上也有可借鑑之處。下面針對項目中一個具體的問題來進行探討。
一個具體問題的分析
這是一個項目中碰到的具體的問題,問題的整體結構如下:
我們的RequestProcessor從消息隊列裏邊接收消息。然後根據發送過來的消息來選擇不同的具體processor,比如說如果消息指定的是list server,那麼我們就需要用listServerRequestProcessor。這樣,根據不同的請求消息,我們就可以提供不同的processor。從這個步驟來說,我們可能就會考慮到一個典型的工廠模式應用。從問題具體的案例來說,我們消息隊列發送過來的消息類型總共有20多個。如果根據這些消息類型去創建不同的processor對象的話,總共也就大概20多種。
一種我們典型的做法就是將選擇processor對象的代碼放到一個方法裏,然後用一堆的if, else判斷條件的語句,像如下所示:
- public RequestProcessor getProcessor(String messageType) {
- if(messageType == "createInstance") {
- return new CreateInstanceProcessor();
- } else if(messageType == "deleteInstance") {
- return new DeleteInstanceProcessor();
- } ...
- //...
- }
這種做法不是不行,以後每次我們添加或者刪除新類型的processor的時候還是很麻煩,需要來修改這邊的代碼。那麼能不能不需要添加這麼一堆的判斷語句來實現選擇對象的效果呢?這個時候,我們可以考慮借鑑DriverManager的思想了。
首先一個我們可以將這些具體實現的request processor聲明放到配置文件裏,因爲他們都繼承自RequestProcessor,我們可以採用ServiceLoader把他們加載進來。下一個問題就是,我們該怎麼來選擇特定的request processor呢?我們可以學DriverManager裏面getDriver的手法。在那裏每個具體的Driver實現都提供了一個acceptsURL方法,這個方法判斷是否符合該Driver的期望。比如說我給的url是com.mysql.jdbc.Driver,那麼則只有mysql jdbc的Driver才能返回爲true。我們在這裏可以依葫蘆畫瓢的定義一個acceptMessage(String messageType)的方法。每個不同的實現根據不同的參數值來返回對象。那麼我們前面那個方法就可以用如下的方式實現了。
- public RequestProcessor getProcessor(String messageType) {
- for(ServiceLoader service : serviceList) {
- if(service.acceptMessage(messageType)) {
- return service;
- }
- }
- return null;
- }
我們這裏的serviceList定義可以放在類的靜態代碼中:
- ServiceLoader<MessageService> serviceList =
- ServiceLoader.load(RequestProcessor.class);
這是一種選擇對象的方式,當然,我們也可以把所有對象放到一個map裏面,然後根據傳入的參數去選擇。
總結
ServiceLoader是一個可以實現動態加載具體實現類的機制,通過它可以實現和具體實現代碼的解耦。也可以實現類似於IOC的效果。在jdbc的DriverManager裏,就使用了ServiceLoader的特性。對它這種特性的靈活運用可以帶來很多代碼上實現的簡化。它也可以作爲一種工廠方法實現的參考。