1. 程式人生 > >曾經滄海難為水,除卻巫山不是雲

曾經滄海難為水,除卻巫山不是雲

淺談SPI機制

前言

這段時間在研究一個開源框架,發現其中有一些以SPI命名的包,經過搜尋、整理以及思考之後,將學習的筆記、心得整理出來,供日後複習使用。

SPI

SPI全稱是Service Provider Interface,翻譯過來是服務提供者介面,這個翻譯其實不那麼形象,理解起來也不是很好理解,至少不那麼見名知意。

其實SPI是一種機制,一種類似於服務發現的機制,什麼叫做服務發現呢,就是能夠根據情況發現已有服務的機制,好像說了跟沒說一樣,對吧,下面我們逐個來理解。

首先是服務,英文叫做Service,服務可以理解為就是某一種或者某幾種功能,比如日常生活中的醫生,提供看病的服務;家政公司,提供家政服務;房產中介公司,提供,這樣子的話,關於服務,應該是理清楚了。

接下來是服務的發現,英文是Service Discovery,理解了服務,那麼服務的發現就應該很好理解了,用大白話講就是具有某種能力,可以發現某些服務,比如生活中的房產中介公司(服務發現),他們就能夠發現很多的擁有空閒房子並且願意出租的人(服務)。

SPI機制的作用就是服務發現,也就是說,我們有一些服務,然後通過SPI機制,就能讓這些服務被需要的人所使用,而我們這些服務被發現的過程就是SPI的任務了。

說到這裡,可能你還是不太理解SPI是什麼,接下來我們通過具體的例子分析來理解SPI。

在JDBC4.0之前,我們使用JDBC去連線資料庫的時候,通常會經過如下的步驟

  1. 將對應資料庫的驅動加到類路徑中
  2. 通過Class.forName()註冊所要使用的驅動,如Class.forName(com.mysql.jdbc.Driver)
  3. 使用驅動管理器DriverManager來獲取連線
  4. 後面的內容我們不關心了。

這種方式有個缺點,載入驅動是由使用者來操作的,這樣就很容易出現載入錯驅動或者更換驅動的時候,忘記更改載入的類了。

在JDBC4.0,現在我們使用的時候,上面的第二步就不需要了,並且能夠正常使用,這個就是SPI的功勞了。

接下來我們先來看下為什麼不需要第二步。

熟悉反射的同學應該知道,第二步其實就是將對應的驅動類載入到虛擬機器中,也就是說,現在我們沒有手動載入,那麼對應的驅動類是如何載入到虛擬機器中的呢,我們通過DriverManger

的原始碼的瞭解SPI是如何實現這個功能的。

DriverManager.java

在DriverManager中,有一段靜態程式碼(靜態程式碼在類被載入的時候就會執行)

static {
    // 在這裡載入對應的驅動類
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

接下來我們來具體看下其內容

loadInitialDrivers()

private static void loadInitialDrivers() {
    String drivers;
    try {
        // 先獲取系統變數
        drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
                return System.getProperty("jdbc.drivers");
            }
        });
    } catch (Exception ex) {
        drivers = null;
    }

    // SPI機制載入驅動類
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
            
            //  通過ServiceLoader.load進行查詢,我們的重點也是這裡,後面分析
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            // 獲取迭代器,也請注意這裡
            Iterator<Driver> driversIterator = loadedDrivers.iterator();

            try{
                // 遍歷迭代器
                // 這裡需要這麼做,是因為ServiceLoader預設是延遲載入
                // 只是找到對應的class,但是不載入
                // 所以這裡在呼叫next的時候,其實就是例項化了對應的物件了
                // 請注意這裡 --------------------------------------------------------------------  1
                while(driversIterator.hasNext()) {
                    // 真正例項化的邏輯,詳見後面分析
                    driversIterator.next();
                }
            } catch(Throwable t) {
            // Do nothing
            }
            return null;
        }
    });

    println("DriverManager.initialize: jdbc.drivers = " + drivers);

    if (drivers == null || drivers.equals("")) {
        return;
    }
    // 同時載入系統變數中找到的驅動類
    String[] driversList = drivers.split(":");
    println("number of Drivers:" + driversList.length);
    for (String aDriver : driversList) {
        try {
            println("DriverManager.Initialize: loading " + aDriver);
            // 由於是系統變數,所以使用系統類載入器,而不是應用類載入器
            Class.forName(aDriver, true,
                    ClassLoader.getSystemClassLoader());
        } catch (Exception ex) {
            println("DriverManager.Initialize: load failed: " + ex);
        }
    }
}

從上面的程式碼中並沒有找到對應的操作邏輯,唯一的一個突破點就是ServiceLoader.load(Driver.class)方法,該方法其實就是SPI的核心啦

接下來我們來分析這個類的程式碼(程式碼可能有點長哦,要有心理準備)

ServiceLoader.java


public final class ServiceLoader<S>
    implements Iterable<S>
{
    /**
    *  由於是呼叫ServiceLoader.load(Driver.class)方法,所以我們先從該方法分析
    */ 
    public static <S> ServiceLoader<S> load(Class<S> service) {
        // 獲取當前的上下文執行緒
        // 預設情況下是應用類載入器,具體的內容稍後分析
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        // 呼叫帶載入器的載入方法
        return ServiceLoader.load(service, cl);
    }

    /**
    *  帶類載入器的載入方法
    */
    public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader)
    {
        // 只是返回一哥ServiceLoader物件,呼叫自己的建構函式嘛
        return new ServiceLoader<>(service, loader);
    }

    /**
    *  私有建構函式
    */
    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        // 目標載入類不能為null
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        // 獲取類載入器,如果cl是null,則使用系統類載入器
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        // 呼叫reload方法
        reload();
    }

    // 用於快取載入的服務提供者
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // 真正查詢邏輯的實現
    private LazyIterator lookupIterator;

    /**
    *  reload方法
    */
    public void reload() {
        // 先清空內容
        providers.clear();
        // 初始化lookupIterator
        lookupIterator = new LazyIterator(service, loader);
    }
}

LazyIterator.class

LazyIterator是ServiceLoader的私有內部類

private class LazyIterator
        implements Iterator<S>
{

    Class<S> service;
    ClassLoader loader;

    /**
    *  私有建構函式,用於初始化引數
    */
    private LazyIterator(Class<S> service, ClassLoader loader) {
        this.service = service;
        this.loader = loader;
    }
}

到了上面的內容,其實ServiceLoader.load()方法就結束了,並沒有實際上去查詢具體的實現類,那麼什麼時候才去查詢以及載入呢,還記得上面的Iterator<Driver> driversIterator = loadedDrivers.iterator();這一行程式碼嗎,這一行程式碼用於獲取一個迭代器,這裡同樣也沒有進行載入,但是,其後面還有遍歷迭代器的程式碼,上面標註為1的部分。

迭代器以及遍歷迭代器的過程如下所示

ServiceLoader.java

public Iterator<S> iterator() {
    return new Iterator<S>() {

        // 注意這裡的providers,這裡就是上面提到的用於快取
        // 已經載入的服務提供者的容器。
        Iterator<Map.Entry<String,S>> knownProviders
            = providers.entrySet().iterator();

        // 底層其實委託給了providers
        public boolean hasNext() {
            if (knownProviders.hasNext())
                return true;
            // 如果沒有快取,則查詢及載入
            return lookupIterator.hasNext();
        }

        // 同上
        public S next() {
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            return lookupIterator.next();
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }
    };
}

上面已經分析過了,ServiceLoader.load()方法執行到LazyIterator的初始化之後就結束了,真正地查詢直到呼叫lookupIterator.hasNext()才開始。

LazyIterator.java

// 希望你還記得他
private class LazyIterator
        implements Iterator<S>
{

    Class<S> service;
    ClassLoader loader;
    Enumeration<URL> configs = null;
    Iterator<String> pending = null;
    String nextName = null;

    //檢查 AccessControlContext,這個我們不關係
    // 關鍵的核心是都呼叫了hasNextService()方法
    public boolean hasNext() {
        if (acc == null) {
            return hasNextService();
        } else {
            PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                public Boolean run() { return hasNextService(); }
            };
            return AccessController.doPrivileged(action, acc);
        }
    }

    private boolean hasNextService() {
        // 第一次載入
        if (nextName != null) {
            return true;
        }
        // 第一次載入
        if (configs == null) {
            try {
                // 注意這裡,獲取了的完整名稱
                // PREFIX定義在ServiceLoader中
                // private static final String PREFIX = "META-INF/services/"
                // 這裡可以看到,完整的類名稱就是 META-INF/services/CLASS_FULL_NAME
                // 比如這裡的 Driver.class,完整的路徑就是
                //                  META-INF/services/java.sql.Driver,注意這個只是檔名,不是具體的類哈
                String fullName = PREFIX + service.getName();
                // 如果類載入器為null,則使用系統類載入器進行載入
                // 類載入會載入指定路徑下的所有類
                if (loader == null)
                    configs = ClassLoader.getSystemResources(fullName);
                else // 使用傳入的類載入器進行載入,其實就是應用類載入器
                    configs = loader.getResources(fullName);
            } catch (IOException x) {
                fail(service, "Error locating configuration files", x);
            }
        }
        // 如果pending為null或者沒有內容,則進行載入,一次只加載一個檔案的一行
        while ((pending == null) || !pending.hasNext()) {
            if (!configs.hasMoreElements()) {
                return false;
            }
            // 解析讀取到的每個檔案,高潮來了
            pending = parse(service, configs.nextElement());
        }
        nextName = pending.next();
        return true;
    }

    /**
    *  解析讀取到的每個檔案
    */
    private Iterator<String> parse(Class<?> service, URL u)
        throws ServiceConfigurationError
    {
        InputStream in = null;
        BufferedReader r = null;
        ArrayList<String> names = new ArrayList<>();
        try {
            in = u.openStream();
            // utf-8編碼
            r = new BufferedReader(new InputStreamReader(in, "utf-8"));
            int lc = 1;
            // 一行一行地讀取資料
            while ((lc = parseLine(service, u, r, lc, names)) >= 0);
        } catch (IOException x) {
            fail(service, "Error reading configuration file", x);
        } finally {
            try {
                if (r != null) r.close();
                if (in != null) in.close();
            } catch (IOException y) {
                fail(service, "Error closing configuration file", y);
            }
        }
        // 返回迭代器
        return names.iterator();
    }

    // 解析一行行的資料
    private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
                          List<String> names)
        throws IOException, ServiceConfigurationError
    {
        String ln = r.readLine();
        if (ln == null) {
            return -1;
        }
        // 查詢是否存在#
        // 如果存在,則剪取#前面的內容
        // 目的是防止讀取到#及後面的內容
        int ci = ln.indexOf('#');
        if (ci >= 0) ln = ln.substring(0, ci);
        ln = ln.trim();
        int n = ln.length();
        if (n != 0) {
            // 不能包含空格及製表符\t
            if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
                fail(service, u, lc, "Illegal configuration-file syntax");
            int cp = ln.codePointAt(0);
            // 檢查第一個字元是否是Java語法規範的單詞
            if (!Character.isJavaIdentifierStart(cp))
                fail(service, u, lc, "Illegal provider-class name: " + ln);
            // 檢查每個字元
            for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
                cp = ln.codePointAt(i);
                if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
                    fail(service, u, lc, "Illegal provider-class name: " + ln);
            }
            // 如果快取中沒有,並且當前列表中也沒有,則加入列表。
            if (!providers.containsKey(ln) && !names.contains(ln))
                names.add(ln);
        }
        return lc + 1;
    }

    /**
    *  上面解析完檔案之後,就開始載入檔案的內容了
    */
    private S nextService() {
        if (!hasNextService())
            throw new NoSuchElementException();
        String cn = nextName;
        nextName = null;
        Class<?> c = null;
        try {
            // 這一行就很熟悉啦
            c = Class.forName(cn, false, loader);
        } catch (ClassNotFoundException x) {
            fail(service,
                    "Provider " + cn + " not found");
        }
        if (!service.isAssignableFrom(c)) {
            fail(service,
                    "Provider " + cn  + " not a subtype");
        }
        try {
            // 例項化並且將其轉化為對應的介面或者父類
            S p = service.cast(c.newInstance());
            // 將其放入快取中
            providers.put(cn, p);
            // 返回當前例項
            return p;
        } catch (Throwable x) {
            fail(service,
                    "Provider " + cn + " could not be instantiated",
                    x);
        }
        throw new Error();          // This cannot happen
    }
}

到此,解析的步驟就完成了,在一開始的DriverManager中,我們也看到了在DriveirManager中一直在呼叫next方法,也就是持續地載入找到的所有的Driver的實現類了,比如MySQL的驅動類,Oracle的驅動類啦。

這個例子有點長,但我們收穫還是很多,我們知道了JDBC4不用手動載入驅動類的實現原理,其實就是通過ServiceLoader去查詢當前類載入器能訪問到的目錄下的WEB-INF/services/FULL_CLASS_NAME檔案中的所有內容,而這些內容由一定的規範,如下

  • 每行只能寫一個全類名
  • #作為註釋
  • 只能使用utf-8及其相容的編碼
  • 每個實現類必須提供一個無參建構函式,因為是直接使用class.newInstance()來建立例項的嘛

由此我們也明白了SPI機制的工作原理,那麼這個東西有什麼用呢,其實JDBC就是個最好的例子啦,這樣使用者就不需要知道到底是要載入哪個實現類,一方面是簡化了操作,另一方面避免了操作的錯誤,當然,這種一般是用於寫框架之類的用途,用於向框架使用者提供更加便利的操作,比如上面的引導我看到SPI的例子,其實是來自一個RPC框架,通過SPI機制,讓我們可以直接編寫自定義的序列化方式,然後由框架來負責載入即可。

SPI實戰小案例

上面學習完了SPI的例子,也學習完了JDBC是如何實現的,接下來我們來通過一個小案例,來動手實踐一下SPI是如何工作的。

新建一個介面,內容隨便啦

HelloServie.java

public interface HelloService {
    void sayHello();
}

然後編寫其實現類

HelloServiceImpl.java

public class HelloServiceImpl implements HelloService {
    @Override
    public void sayHello() {
        System.out.println("hello world");
    }
}

關鍵點來了,既然是學習SPI,那麼我們肯定不是手動new一個實現類啦,而是通過SPI的機制來載入,如果認真地看完上面的分析,那麼下面的內容應該很容易看懂啦,如果沒看懂,再回去看一下啦。

  1. 在實現類所在專案(這裡是同個專案哈)的類路徑下,如果是maven專案,則是在resources目錄下

    1. 建立目錄META-INF/services
    2. 建立檔案cn.xuhuanfeng.spi.HelloService(介面的全限定名哈)
  2. 內容是實現類的類名:cn.xuhuanfeng.spi.impl.HelloServiceImpl(注意這裡我們直接放在同個專案,不是同個專案也可以的!!!)

  3. 自定義一