1. 程式人生 > >淺析JDK中ServiceLoader的原始碼

淺析JDK中ServiceLoader的原始碼

前提

緊接著上一篇《通過原始碼淺析JDK中的資源載入》,ServiceLoader是SPI(Service Provider Interface)中的服務類載入的核心類,也就是,這篇文章先介紹ServiceLoader的使用方式,再分析它的原始碼。

ServiceLoader的使用

這裡先列舉一個經典的例子,MySQL的Java驅動就是通過ServiceLoader載入的,先引入mysql-connector-java的依賴:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.47</version>
</dependency>

檢視這個依賴的原始碼包下的META-INF目錄,可見:

r-s-l-1

我們接著檢視java.lang.DriverManager,靜態程式碼塊裡面有:

static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

其中,可以檢視loadInitialDrivers()有如下的程式碼片段:

r-s-l-2

java.lang.DriverManager是啟動類載入器載入的基礎類,但是它可以載入rt.jar包之外的類,上篇文章提到,這裡打破了雙親委派模型,原因是:ServiceLoader中使用了執行緒上下文類載入器去載入類。這裡JDBC載入的過程就是典型的SPI的使用,總結規律如下:

  • 1、需要定義一個介面。
  • 2、介面提供商需要實現第1步中的介面。
  • 3、介面提供商在META-INF/services目錄下建立一個文字檔案,檔名是第1步中定義的介面的全限定類名,文字內容是介面的實現類的全限定類名,每個不同的實現佔獨立的一行。
  • 4、使用ServiceLoader載入介面類,獲取介面的實現的例項迭代器。

舉個簡單的例項,先定義一個介面和兩個實現:

public interface Say {

  void say();
}

public class SayBye implements Say {

    @Override
    public void say() {
        System.out.println("Bye!");
    }
}

public class SayHello implements Say {

    @Override
    public void say() {
        System.out.println("Hello!");
    }
}

接著在專案的META-INF/services中新增檔案如下:

r-s-l-3

最後通過main函式驗證:

r-s-l-4

基於SPI或者說ServiceLoader載入介面實現這種方式也可以廣泛使用在相對基礎的元件中,因為這是一個成熟的規範。

ServiceLoader原始碼分析

上面通過一個經典例子和一個例項介紹了ServiceLoader的使用方式,接著我們深入分析ServiceLoader的原始碼。我們先看ServiceLoader的類簽名和屬性定義:

public final class ServiceLoader<S> implements Iterable<S>{
    //需要載入的資源的路徑的目錄,固定是ClassPath下的META-INF/services/
    private static final String PREFIX = "META-INF/services/";
    // ServiceLoader需要正在需要載入的類或者介面
    // The class or interface representing the service being loaded
    private final Class<S> service;
    // ServiceLoader進行類載入的時候使用的類載入器引用
    // 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實現了Iterable介面,這一點提示了等下我們在分析它原始碼的時候,需要重點分析iterator()方法的實現。ServiceLoader依賴於類載入器例項進行類載入,它的核心屬性LazyIterator是就是用來實現iterator()方法的,下文再重點分析。接著,我們分析ServiceLoader的建構函式:

public void reload() {
    //清空快取
    providers.clear();
    //構造LazyIterator例項
    lookupIterator = new LazyIterator(service, loader);
}

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

ServiceLoader只有一個私有的建構函式,也就是它不能通過建構函式例項化,但是要例項化ServiceLoader必須依賴於它的靜態方法呼叫私有構造去完成例項化操作,而例項化過程主要做了幾步:

  • 1、判斷傳入的介面或者類的Class例項不能為null,否則會丟擲異常。
  • 2、如果傳入的ClassLoader例項為null,則使用應用類載入器(Application ClassLoader)。
  • 3、例項化訪問控制上下文。
  • 4、呼叫例項方法reload(),清空目標載入類的實現類例項的快取並且構造LazyIterator例項。

注意一點是例項方法reload()的修飾符是public,也就是可以主動呼叫去清空目標載入類的實現類例項的快取和重新構造LazyIterator例項。接著看ServiceLoader提供的靜態方法:

public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){
    return new ServiceLoader<>(service, loader);
}

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
    ClassLoader cl = ClassLoader.getSystemClassLoader();
    ClassLoader prev = null;
    while (cl != null) {
        prev = cl;
        cl = cl.getParent();
    }
    return ServiceLoader.load(service, prev);
}

上面的三個公共靜態方法都是用於構造ServiceLoader例項,其中load(Class<S> service, ClassLoader loader)就是典型的靜態工廠方法,直接呼叫ServiceLoader的私有構造器進行例項化,除了需要指定載入類的目標型別,還需要傳入類載入器的例項。load(Class<S> service)實際上也是委託到load(Class<S> service, ClassLoader loader),不過它使用的類載入器指定為執行緒上下文類載入器,一般情況下,執行緒上下文類載入器獲取到的就是應用類載入器(系統類載入器)。loadInstalled(Class<S> service)方法又看出了"雙親委派模型"的影子,它指定類載入器為最頂層的啟動類載入器,最後也是委託到load(Class<S> service, ClassLoader loader)。接著我們需要重點分析ServiceLoader#iterator()

public Iterator<S> iterator() {

    //Iterator的匿名實現
    return new Iterator<S>() {
        
    //目標類實現類例項快取的Map的Entry的迭代器例項
    Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();
        
        //先從快取中判斷是否有下一個例項,否則通過懶載入迭代器LazyIterator去判斷是否存在下一個例項
        public boolean hasNext() {
            if (knownProviders.hasNext())
                return true;
            return lookupIterator.hasNext();
        }

        //如果快取中判斷是否有下一個例項,如果有則從快取中的值直接返回
        //否則通過懶載入迭代器LazyIterator獲取下一個例項
        public S next() {
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            return lookupIterator.next();
        }

        //不支援移除操作,直接拋異常
        public void remove() {
            throw new UnsupportedOperationException();
        }
    };
}

iterator()內部僅僅是Iterator介面的匿名實現,hasNext()next()方法都是優先判斷快取中是否已經存在實現類的例項,如果存在則直接從快取中返回,否則呼叫懶載入迭代器LazyIterator的例項去獲取,而LazyIterator本身也是一個Iterator介面的實現,它是ServiceLoader的一個私有內部類,原始碼如下:

private class LazyIteratorimplements 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() {
            //如果下一個需要載入的實現類的全限定類名不為null,則說明資源中存在內容
            if (nextName != null) {
                return true;
            }
            //如果載入的資源的URL集合為null則嘗試進行載入
            if (configs == null) {
                try {
                    //資源的名稱,META-INF/services + '需要載入的類的全限定類名'
                    //這樣得到的剛好是需要載入的檔案的資源名稱
                    String fullName = PREFIX + service.getName();
                    //這裡其實ClassLoader例項應該不會為null
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        //從ClassPath載入資源
                        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<S>例項
                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 {
                //通過Class#newInstance()進行例項化,並且強制轉化為對應的型別的例項
                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也是Iterator介面的實現,它的Lazy特性表明它總是在ServiceLoader的Iterator介面匿名實現iterator()執行hasNext()判斷是否有下一個實現或者next()獲取下一個實現類的例項的時候才會"懶判斷"或者"懶載入"下一個實現類的例項。最後是載入資原始檔後對資原始檔的解析過程的原始碼:

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();
            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);
            }
        }
        //返回的是ArrayList的迭代器例項
        return names.iterator();
}

//解析資原始檔中每一行的內容
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) {
            //不能存在空格字元' '和特殊字元'\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;
    }

整個資原始檔的解析過程並不複雜,主要包括檔案內容的字符合法性判斷和快取避免重複載入的判斷。

小結

SPI被廣泛使用在第三方外掛式類庫的載入,最常見的如JDBC、JNDI、JCE(Java加密模組擴充套件)等類庫。理解ServiceLoader的工作原理有助於編寫擴充套件性良好的可插拔的類庫。

(本文完 c-1-d e-20181014)