SPI簡要分析

一、背景介紹

  在看spring源碼和dubbo源碼的時候,發現兩者都用採用了JDK中spi的技術,發現都有大作用,所以就來分析下JDK中的SPI的使用方式及源碼實現。

二、什麼是SPI

  SPI的全稱是 Service Provider Interface。 一種從特定路徑下,將實現了某些特定接口的類加載到內存中的方式(爲什麼會如此說,請看後面分析)。提供了另外一種方式加載實現類,也降低了代碼的耦合程度,提升了代碼的可擴展性。

  實現SPI的地方主要有以下3處。主要的類和方法分別是:

  • JDK
    •   java.util.ServiceLoader#load
  • Spring
    •   org.springframework.core.io.support.SpringFactoriesLoader#loadFactories
  • Dubbo
    •   org.apache.dubbo.common.extension.ExtensionLoader#

 

三、舉例說明JDK SPI的使用方式

 1. 自定義實現類, 實現數據庫驅動 Driver.class

package com.fattyca1.driver;

import java.sql.*;
import java.util.Properties;
import java.util.logging.Logger;

/**
 * <br>自定義數據庫操作</br>
 *
 * @author fattyca1
 */
public abstract class CustomziedDriver implements Driver {

    public Connection connect(String url, Properties info) throws SQLException {
        return null;
    }

    public boolean acceptsURL(String url) throws SQLException {
        return false;
    }

    public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException {
        return new DriverPropertyInfo[0];
    }

    public int getMajorVersion() {
        return 0;
    }

    public int getMinorVersion() {
        return 0;
    }

    public boolean jdbcCompliant() {
        return false;
    }

    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
        return null;
    }

    // 自定義方法
    protected abstract void outDbName();
}
package com.fattyca1.driver;

/**
 * <br>自定義實現數據庫驅動</br>
 *
 * @author fattyca1
 */
public class Fattyca1Driver extends CustomziedDriver {

    @Override
    public void outDbName() {
        System.out.println("fattyca1 db driver init ... ");
    }
}

 2. 在resources下創建META-INF/services文件夾,並在目錄中創建文件。文件名稱爲實現接口名稱,文件中內容爲接口實現類。

  

 

 3. 建立一個main程序來測試結果

import com.fattyca1.driver.Fattyca1Driver;

import java.sql.Driver;
import java.util.ServiceLoader;

/**
 * <br>spi 測試客戶端</br>
 *
 * @author fattyca1
 */
public class SPIClient {

    public static void main(String[] args) {

        ServiceLoader<Driver> drivers = ServiceLoader.load(Driver.class);

        for (Driver driver : drivers) {
            if(driver instanceof Fattyca1Driver) {
                ((Fattyca1Driver) driver).outDbName();
            }
        }


    }
}

 4. 實驗結果。如下圖:

  

 

以上方式就是JDK中的SPI的實現方式,通過此方法,我們實現了自定義的Driver,可以使用自己定義的方式連接數據庫。

 

四、JDK中ServiceLoader的源碼分析

  通過看上面例子, 大概瞭解到如果使用JDK中的SPI的實現方式,但是我們還不知道JDK是如何操作的。 接下來,我們就分析分析JDK的實現方式。

1. 從構造函數入口,發現其構造方法是私有,無法被外部初始化,所以我們直接從提供的靜態方法入手

  // ServiceLoader的構造函數
   private ServiceLoader(Class<S> svc, ClassLoader cl) {
        ...
   }

2. 我們從ServiceLoader#load方法開始,一步步點進去,發現其最後調用的私有構造方法,構造中的核心方法是reload()。

    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        ...
        reload(); // 主要實現方法
    }

3. 查看reload()方法的具體實現,發現並無多餘代碼,主要是清除了cacheMap, 實例化了lookupIterator,。

    public void reload() {
        providers.clear(); // 清除緩存中的對象
        lookupIterator = new LazyIterator(service, loader); // 初始化迭代器,此迭代器被調用時,纔會加載類。
    }

4.分析LazyIterator. 從名字可以看出,這是懶加載的類(命名清晰的好處)。此代碼實現起來比較簡單。 通過給定的class名稱, 讀取資源文件, 然後加載文件中的實現類。通過反射,生成實現類,放入cacheMap中。

private class LazyIterator implements Iterator<S>
    {

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

        private LazyIterator(Class<S> service, ClassLoader loader) {
            ...
        }

        private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
                }
            }
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                pending = parse(service, configs.nextElement());
            }
            nextName = pending.next();
            return true;
        }

        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
        }

        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);
            }
        }

        public S next() {
            if (acc == null) {
                return nextService();
            } else {
                PrivilegedAction<S> action = new PrivilegedAction<S>() {
                    public S run() { return nextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }

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

    }

 

五、SPI使用場景

  1.   jdk中數據庫驅動的加載
  2.       spring中各種組件的插拔
  3.       dubbo中自定義rpc協議,序列化方式,過濾器等 

六、總結

  至此,我們分析完JDK源碼中的SPI的實現(代碼實現簡單,沒有很仔細),發現實現簡單,功能d強大,大家是否學習到了呢?  我們在自己實現代碼的時候,可以多考慮學習此方式,也可以給代碼松耦合,提升自己的代碼質量。spring和dubbo中的源碼和JDK中的源碼都十分相似,實現起來大同小異,大家有時間可以自己比較比較。  

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章