1. 程式人生 > 實用技巧 >Java SPI 原始碼解析

Java SPI 原始碼解析

:程式碼環境基於 JDK 1.8

一、SPI 是什麼?

SPI(Service Provider Interface):是一個可以被第三方擴充套件或實現的 API,它可以用來實現框架擴充套件和可替換的模組,優勢是實現解耦。簡單來說就是推薦模組之間基於介面程式設計,模組之間不對實現類進行硬編碼。若在程式碼裡涉及具體的實現類就違反了可挺拔的原則。從而 java SPI 提供了這種服務發現機制:為某個介面尋找服務實現的機制。

二、SPI 與 API 的區別

  • API 直接為提供了功能,使用 API 就能完成任務。
  • API 和 SPI 都是相對的概念,差別只在語義上,API 直接被應用開發人員使用,SPI 被框架擴張人員使用。
  • API 大多數情況下,都是實現方來制定介面並完成對介面的不同實現,呼叫方僅僅依賴卻無權選擇不同實現。SPI 是呼叫方來制定介面,實現方來針對介面來實現不同的實現。呼叫方來選擇自己需要的實現方。

三、SPI 使用及示例

  1. 服務呼叫方通過 ServiceLoader.load 載入服務介面的實現類例項
  2. 服務提供方實現服務介面後, 在自己Jar包的 META-INF/services 目錄下新建一個介面名全名的檔案, 並將具體實現類全名寫入。

示例:

1. 建立介面

public interface Search {
    List<String> searchDoc(String keyword);
}

2. 建立 DatabaseSearch 實現類

public class DatabaseSearch implements Search {
    @Override
    public List<String> searchDoc(String keyword) {
        System.out.printf("資料庫搜尋:" + keyword);
        return null;
    }
}

3. 建立 FileSearch 實現類

public class FileSearch implements Search {
    @Override
    public List<String> searchDoc(String keyword) {
        System.out.println("檔案搜尋:" + keyword);
        return null;
    }
}

4. META-INF.services 中建立介面全限定名檔案:spi.learn.Search

spi.learn.FileSearch
spi.learn.DatabaseSearch

5. 測試類

public static void main(String[] args) {
    ServiceLoader<Search> s = ServiceLoader.load(Search.class);
    Iterator<Search> iterator = s.iterator();
    while (iterator.hasNext()) {
        Search search = iterator.next();
        search.searchDoc("spi");
    }
}

-----輸出:-----
檔案搜尋:spi
資料庫搜尋:spi

四、原始碼解讀

先來看下 ServiceLoader 類的全域性變數:

//spi 預設載入的路徑
private static final String PREFIX = "META-INF/services/";

// 表示正在被載入的類或介面
// The class or interface representing the service being loaded
private final Class<S> service;

// 用於定位、裝入和例項化提供程式的類載入器
// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;

// 許可權控制上下文
// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;

// 基於例項的順序快取類的實現例項,其中Key為實現類的全限定類名
// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

// 當前的"懶查詢"迭代器,ServiceLoader的核心
// The current lazy-lookup iterator
private LazyIterator lookupIterator;

ServiceLoader.load(Search.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) {
    // svc 為 null,拋 NullPointerException
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    // 若沒有指定載入器,預設使用系統載入器
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    // Java安全管理器  
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    reload();
}

public void reload() {
    // 清空例項化好的快取。
    providers.clear();
    // "懶查詢",ServiceLoader 的核心。
    lookupIterator = new LazyIterator(service, loader);
}

LazyIterator 為 ServiceLoader 的核心,來看看具體原始碼:

private class LazyIterator implements Iterator<S> {
    Class<S> service;
    ClassLoader loader;
    // 載入資源的URL集合
    Enumeration<URL> configs = null; 
    // 需載入的實現類的全限定類名的集合
    Iterator<String> pending = null;
    // 下一個需要載入的實現類的全限定類名
    String nextName = null;

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

    private boolean hasNextService() {
        //資源已存在,無需載入
        if (nextName != null) {
            return true;
        }
        // 資源為null,嘗試載入
        if (configs == null) {
            try {
                // 資源名稱,META-INF/services + 全限定名
                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 {
            // 反射構造 Class 例項
            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());
            // 例項完成,新增快取,Key:實現類全限定類名,Value:實現類例項
            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();
    }
}

LazyIterator 機制總結:LazyIterator 也實現了 Iterator介面的實現,Lazy特性體現在只有在使用 ServiceLoader 呼叫 iterator() 方法獲取 Iterator 介面匿名實現類後, 再呼叫 hasNext() 方法時,才會"懶判斷"或者"懶載入"下一個實現類的例項。呼叫的入口,也就是示例 main 方法中 while 那一步,我們再看下 iterator() 的 Iterator 匿名實現類原始碼:

public Iterator<S> iterator() {
    return new Iterator<S>() {
        Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();
        
        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();
        }
    };
}

呼叫鏈分析完,最後看下 hasNextService 中解析限定名檔案的 parse 方法,主要檢查檔案內容的字符合法性、快取過濾避免重複載入。

private Iterator<String> parse(Class<?> service, URL u) throws ServiceConfigurationError {
    InputStream in = null;
    BufferedReader r = null;
    // 存放 META-INF/services 下檔案中的實現類的全類名
    ArrayList<String> names = new ArrayList<>();
    try {
        in = u.openStream();
        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;   //-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);
        //判斷第一個 char 是否一個合法的 Java 起始識別符號
        if (!Character.isJavaIdentifierStart(cp))
            fail(service, u, lc, "Illegal provider-class name: " + ln);
        //判斷所有其他字串是否屬於合法的Java識別符號
        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;
}

五、總結

優點

  1. 使用 Java SPI 機制的優勢是實現解耦,使第三方服務模組的裝配控制的邏輯與呼叫者的業務程式碼分離,而不是耦合在一起。應用程式可以根據實際業務情況啟用框架擴充套件或替換框架元件。
  2. 相比使用提供介面 jar 包供第三方服務使用的方式,SPI 使得源框架不必關心介面的實現類的路徑,可以不使用硬編碼 import 匯入實現類。

缺點

  1. 雖然 ServiceLoader 使用了懶載入,但結果還是通過遍歷獲取,基本上可以說是全部例項化了一遍,所以說,這個懶載入機制在此場景下是浪費的。
  2. 由於是遍歷獲取,所以獲取實現類的方式不夠靈活。
  3. 多個併發多執行緒使用 ServiceLoader 類的例項是不安全的。

注:文章中難免有不足之處,歡迎評論、互動、指正。