JDK SPI 源碼解讀

本文首發於個人微信公衆號《andyqian》, 期待你的關注!

前言

在之前的文章《Seata 之 config 模塊源碼解讀》中提到了Microkernel + Plugin 的架構模式。如果對它還不熟悉的話,沒關係。我們可以簡單的將其理解爲一個接口的多個實現,與設計模式中的策略模式極爲相似。其中調用組裝這些 Plugin 的地方稱之爲 Microkernel,而接口的實現則可以理解爲 Plugin。從 JDK 1.6 版本開始,Java 在 JDK 中提供了一種名爲 SPI (Service Provider Interface) 的服務提供機制,可以理解成 Microkernel + Plugin 模式的 JDK 實現。

舉栗子

在解讀源碼之前,我們先來看看 JDK 中的 SPI 是如何使用的?通過以下例子進行演示:

接口

package io.andyqian.service;

public interface UserService {

    void printlnName();

    void printlnAge();
}

實現

package io.andyqian.service.impl;

import io.andyqian.service.UserService;

public class UserServiceImpl implements UserService {

    @Override
    public void printlnName() {
        System.out.println("andyqian");
    }

    @Override
    public void printlnAge() {
        System.out.println("18");
    }
}

配置:

  1. 在 resource 文件夾下創建子文件夾 META-INF.services,並新增名爲: io.andyqian.service.UserService 的文件,內容如下所示:
    io.andyqian.service.impl.UserServiceImpl

如下圖所示:

 

 

測試

public class UserServiceTest {

    @Test
    public void testLoaderClass(){
        ServiceLoader<UserService> serviceLoader =  ServiceLoader.load(UserService.class);
        for(UserService userService : serviceLoader) {
            userService.printlnAge();
            userService.printlnName();
        }
    }
}

運行結果

18
andyqian

通過上述代碼發現,我們並沒有手工實例化 UserServiceImpl 對象,但會執行其實現,甚是神奇,帶着這樣的疑問,我們繼續往下看。

源碼解析

在進行源碼分析之前,我們先來看看JDK中 SPI 中的一些規範。如下列表所示:

  1. 配置文件必須在指定文件夾 META-INF/services/ 內。
  2. 配置文件編碼格式必須爲:
    UTF-8
  3. 配置文件名必須是 類的全限定名
  4. 接口如果有多個實現,在配置文件中以 # 作爲分隔符。

緊接着,我們以 ServiceLoader 類的 load 方法作爲入口,分析源碼。代碼如下所示:

// 重載函數, 默認爲 當前線程的 ContextClassLoader
public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
}

//返回 ServiceLoader 實例
 public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
        return new ServiceLoader<>(service, loader);
}

ServiceLoader 構造函數,源碼如下所示:

// 校驗參數以及初始化 loader ,初始化 accessController。
 private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        reload();
}

其中 reload 方法,源碼如下:

// 構造 LazyIterator
  public void reload() {
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);
    }

這裏使用了自定義的 LazyIterator 類,其源碼如下所示:

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) {
            this.service = service;
            this.loader = loader;
        }

        // 判斷是否有下一個 Service,
        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;
                }
                // 通過config文件路徑加載服務
                pending = parse(service, configs.nextElement());
            }
            nextName = pending.next();
            return true;
        }

        // 獲取下一個service
        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 {
                // 目標類實例轉換爲響應的類,並將其加載到 providers 列表中
                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
        }

        // 實現 hasNext 方法,底層調用 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);
            }
        }

        // 實現 Iterator 類中的方法,底層調用 nextService()。
        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();
        }

    }

parse 方法源碼:

//其中service 表示需要加載的類對象, u 表示 META-INF.services 下文件 在系統中的全路徑。
 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();
            //這裏驗證配置文件中編碼爲 utf8.
            r = new BufferedReader(new InputStreamReader(in, "utf-8"));
            int lc = 1;
            // 循環解析配置文件中的每一行,當 lc >=0時繼續讀取,<0 時 則中斷
            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();
    }

parseLine 方法源碼如下:

private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
                          List<String> names)
        throws IOException, ServiceConfigurationError
    {
        // 讀取到空行,則返回 -1
        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) {
            if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
                fail(service, u, lc, "Illegal configuration-file syntax");
            int cp = ln.codePointAt(0);
            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);
            }
            // service 去重,如果不存在,則添加到服務列表中。
            if (!providers.containsKey(ln) && !names.contains(ln))
                names.add(ln);
        }
        return lc + 1;
    }

上面使用 Iterator 接口的實現類LazyIterator,將服務加載的這個動作延遲到使用服務時。這樣做的好處是:只有在真正使用時才加載,避免造成沒必要的加載。但未解決,未使用擴展加載的問題

通過上面的源碼,其實還是有疑問的,它們到底怎麼運行起來的呢?其中就不乏ServiceLoader 類 中 Iterable 接口的iterator 方法的功勞,代碼如下所示:

public Iterator<S> iterator() {

        //實例Iterator 對象
        return new Iterator<S>() {

           // 遍歷 providers 列表
            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();
            }

        };
    }

到這裏,已經比較清楚了。但還有一個比較重要的點,在Java中是不存在真正的for each 語法的。之所以我們可以使用,其實是藉助語法糖提供的。而上述的以下代碼:

public class UserServiceTest {
    @Test
    public void testLoaderClass(){
        ServiceLoader<UserService> serviceLoader =  ServiceLoader.load(UserService.class);
        for(UserService userService : serviceLoader) {
            userService.printlnAge();
            userService.printlnName();
        }
    }
}

對應可翻譯爲:

public class UserServiceTest {
    @Test
    public void testLoaderClass(){
        ServiceLoader<UserService> serviceLoader =  ServiceLoader.load(UserService.class);
        for(java.util.Iterator i$ = serviceLoader.iterator(); i$.hasNext();) {
            UserService userService = (UserService) i$.next();
            userService.printlnAge();
            userService.printlnName();
        }
    }
}

其中 serviceLoader.iterator()方法就是 ServiceLoader 類中實現的 iterator方法。至此,JDK中的SPI 的核心代碼就連起來了。

小結

在很多框架中,參考或增強了JDK 的SPI,使得其在最大程度的解耦,也給框架擴展提供了豐富的想象。其中就包括:JDBC,Dubbo,Seata 等等。下次我們一起來分析分析。


 

相關閱讀:

Java 代碼中幾類典型的 "壞味道"

Seata 之 config 模塊源碼解讀

Seata 分佈式事務框架

Dubbo 線程池源碼解析

 

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