雙親委派 & URLClassLoader & SPI

  • ClassLoader & 雙親委派
  • URLClassLoader
  • SPI(Service Provider Interface)

ClassLoader & 雙親委派

ClassLoader 編程語言 加載內容 Parent ClassLoader
BootstrapClassLoader C++ jre/lib;jre/classes Null Java虛擬機啓動後初始化
ExtClassLoader Java jre/lib/ext;java.ext.dirs BootstrapClassLoader
AppClassLoader Java classpath指定位置 ExtClassLoader ClassLoader.getSystemClassLoader 返回值

ClassLoader的基本模型:

Java默認ClassLoader委派模型
載錄:https://www.cnblogs.com/doit8791/p/5820037.html

URLClassLoader

  • ClassLoader.loadClass
  • URLClassLoader.findClass
  • ExtClassLoader & AppClassLoader 的URLClassPath

ClassLoader.loadClass

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            if (resolve) { resolveClass(c); }
            return c;
        }
    }

    // ClassLoader method
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

其中findClass是關鍵:一來沒有具體實現,二來修飾符是protected,所以 很明顯是爲了“開閉”。

URLClassLoader.findClass

// URLClassLoader overwrite
    protected Class<?> findClass(final String name)
        throws ClassNotFoundException
    {
        final Class<?> result;
        try {
            result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
                    public Class<?> run() throws ClassNotFoundException {
                        String path = name.replace('.', '/').concat(".class");
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
                            try {
                                return defineClass(name, res);
                            } catch (IOException e) {
                                throw new ClassNotFoundException(name, e);
                            }
                        } else {
                            return null;
                        }
                    }
                }, acc);
        } catch (java.security.PrivilegedActionException pae) {
            throw (ClassNotFoundException) pae.getException();
        }
        if (result == null) {
            throw new ClassNotFoundException(name);
        }
        return result;
    }

這裏要關注的點是: ucp.getResource

所有繼承至URLClassLoader的子類(不overwrite findClass的情況下),都是 【只能通過ucp來進行資源的加載】。

URLClassPath做到了兩點:
- 指定資源的來源:可以來自於Local,來自於Remote;可以是Jar,也可以是War等等(指定URL即可)。
- 限制資源的來源:當指定了該ucp,那麼該ClassLoader的資源來源也就被【限制】只能是來自於這裏。

ExtClassLoader & AppClassLoader

AppClassLoader&ExtClassLoader繼承關係圖
相比之下ExtClassLoader和AppClassLoader有何區別呢?通過源碼對比一下URLClassPath

  • ExtClassLoader
    static class ExtClassLoader extends URLClassLoader {
        public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
            final File[] var0 = getExtDirs();

            try {
                return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
                    public Launcher.ExtClassLoader run() throws IOException {
                        int var1 = var0.length;

                        for(int var2 = 0; var2 < var1; ++var2) {
                            MetaIndex.registerDirectory(var0[var2]);
                        }

                        return new Launcher.ExtClassLoader(var0);
                    }
                });
            } catch (PrivilegedActionException var2) {
                throw (IOException)var2.getException();
            }
        }

        public ExtClassLoader(File[] var1) throws IOException {
            super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
            SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
        }

        private static File[] getExtDirs() {
            String var0 = System.getProperty("java.ext.dirs");
            File[] var1;
            if (var0 != null) {
                StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
                int var3 = var2.countTokens();
                var1 = new File[var3];

                for(int var4 = 0; var4 < var3; ++var4) {
                    var1[var4] = new File(var2.nextToken());
                }
            } else {
                var1 = new File[0];
            }

            return var1;
        }

        private static URL[] getExtURLs(File[] var0) throws IOException {
            Vector var1 = new Vector();

            for(int var2 = 0; var2 < var0.length; ++var2) {
                String[] var3 = var0[var2].list();
                if (var3 != null) {
                    for(int var4 = 0; var4 < var3.length; ++var4) {
                        if (!var3[var4].equals("meta-index")) {
                            File var5 = new File(var0[var2], var3[var4]);
                            var1.add(Launcher.getFileURL(var5));
                        }
                    }
                }
            }

            URL[] var6 = new URL[var1.size()];
            var1.copyInto(var6);
            return var6;
        }
    }

(通過構造函數反推)結論:通過getExtClassLoader來獲取ExtClassLoader時,直接採用環境變量中的“java.ext.dirs”作爲資源

  • AppClassLoader
    static class AppClassLoader extends URLClassLoader {
        final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);

        public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
            final String var1 = System.getProperty("java.class.path");
            final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
            return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
                public Launcher.AppClassLoader run() {
                    URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
                    return new Launcher.AppClassLoader(var1x, var0);
                }
            });
        }

        AppClassLoader(URL[] var1, ClassLoader var2) {
            super(var1, var2, Launcher.factory);
            this.ucp.initLookupCache(this);
        }
    }

(通過構造函數反推)結論:通過getAppClassLoader來獲取AppClassLoader時,直接採用環境變量中的“java.class.path”作爲資源

URLClassLoader 總結

對於自定義ClassLoader 只需要關心兩點

  • 一:URLClassPath – 通常是構造函數或參數
  • 二:委派過程 – findClass

SPI(Service Provider Interface)

SPI機制簡介
SPI的全名爲Service Provider Interface,主要是應用於廠商自定義組件或插件中。在java.util.ServiceLoader的文檔裏有比較詳細的介紹。簡單的總結下java SPI機制的思想:我們系統裏抽象的各個模塊,往往有很多不同的實現方案,比如日誌模塊、xml解析模塊、jdbc模塊等方案。面向的對象的設計裏,我們一般推薦模塊之間基於接口編程,模塊之間不對實現類進行硬編碼。一旦代碼裏涉及具體的實現類,就違反了可拔插的原則,如果需要替換一種實現,就需要修改代碼。爲了實現在模塊裝配的時候能不在程序裏動態指明,這就需要一種服務發現機制。 Java SPI就是提供這樣的一個機制:爲某個接口尋找服務實現的機制。有點類似IOC的思想,就是將裝配的控制權移到程序之外,在模塊化設計中這個機制尤其重要。
SPI具體約定
Java SPI的具體約定爲:當服務的提供者提供了服務接口的一種實現之後,在jar包的META-INF/services/目錄裏同時創建一個以服務接口命名的文件。該文件裏就是實現該服務接口的具體實現類。而當外部程序裝配這個模塊的時候,就能通過該jar包META-INF/services/裏的配置文件找到具體的實現類名,並裝載實例化,完成模塊的注入。基於這樣一個約定就能很好的找到服務接口的實現類,而不需要再代碼裏制定。jdk提供服務實現查找的一個工具類:java.util.ServiceLoader。
載錄:https://blog.csdn.net/sigangjun/article/details/79071850

首先,ServiceLoader的構造函數 是private的,那麼自然就需要定位static方法了。
其中load是c位,它有兩個重載

  1. public static <S> ServiceLoader<S> load(Class<S> service) 即不提供ClassLoader
  2. public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) 即提供ClassLoader

其中 1中的邏輯 通過Thread.currentThread().getContextClassLoader()獲取ClassLoader,並調用2。

所以 想要說清楚ServiceLoader,就不得不提到 Thread中的ContextClassLoader

問題引出

想象一下:如果讓BootstrapClassLoader(或者ExtClassLoader)去load一個MySQL的Driver,正常情況下會成功嗎?
答案是,不會成功的。

其實,細想也是能理解的,畢竟每一個ClassLoader都是有自己負責和管轄的URLClassPath,而MySQL的Driver並不在 BootstrapClassLoader(或者ExtClassLoader)所管轄的URLClassPath下,自然 也就不能成功load到了。
那麼,該如何是好呢?(其實,其中還隱藏了一些問題:load事件何時發生?發生時候的上下文環境又是如何?這是JVM的範疇,這裏不討論)
通過上面的描述,可以猜到 解決的方法也是簡單的,就是將MySQL的Driver放置到“正確”的URLClassPath下即可。 但是,如果真的是這樣,那豈不是把所有的都放在BootstrapClassLoader下面進行load就萬無一失了?

這裏筆者會想到一個問題:(不考慮說BootstrapClassLoader的壓力,因爲這點筆者也說不清楚),由於純理論 可能會描述不清,所以舉例說明:
如 Kafka Connect中,當集羣啓動之後,會在集羣上啓動不同的任務(A任務,B任務),這時如果多任務之間都依賴於一個小模塊,但是又正好,它們依賴的模塊的版本不同且不兼容,那可如何是好?
這裏就需要考慮到Class Resource的隔離了,這也是雙親委派設計的優點。(細節不與說明)

以上都是題外話(不過 不是廢話,筆者覺得 提出的問題還是挺有趣的),回到剛剛的問題:如果讓BootstrapClassLoader(或者ExtClassLoader)去load一個MySQL的Driver,正常情況下會成功嗎?

其實這個問題吧 本身也是有問題的,問題就是:爲什麼非要讓BootstrapClassLoader(或者ExtClassLoader)去load一個MySQL的Driver,讓對應的ClassLoader去load不就好了嗎?例如AppClassLoader。 – 這個問題的本質 就是SPI(Service Provider Interface)了。

回到SPI

SPI的關鍵字:接口編程(不對實現類進行硬編碼)、可拔插、服務發現機制、動態注入。
簡單通過DriverManager來說明ServiceLoader所提供的功能和使用方式:

首先:裝載DriverManager,並觸發靜態邏輯:通過ServiceLoader裝載所有Driver的實現類
接着:通過DriverManager.getConnection()獲取匹配的Driver實現類
最後:由於是面向接口編程,所以直接使用Connection接口即可操作後續邏輯

以上過程看似簡單,但是仔細看 可以提出一些問題:

  1. DriverManager 是在哪裏,被哪個ClassLoader加載?
  2. Driver的實現類 是在哪裏,被哪個ClassLoader加載?

從Coding的角度看,DriverManager總是在AppClassLoader或者更上層被加載(調用)的,那麼自然 DriverManager會按照雙親委派的方式 不斷的向上 委託加載,直到ExtClassLoader(因爲DriverManager是在rt.jar的java.sql包下)。到此,第一個問題就解答完畢了。

接着,第二個問題的第一小問。Driver的實現類 是在哪裏被加載的?

public class DriverManager {
    static { loadInitialDrivers(); }

    private static void loadInitialDrivers() {
        String drivers;
        ......
        // If the driver is packaged as a Service Provider, load it.
        // Get all the drivers through the classloader
        // exposed as a java.sql.Driver.class service.
        // ServiceLoader.load() replaces the sun.misc.Providers()

        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                ......
            }
        });
		......
    }
}

答案就是這個,Driver的實現類 是在DriverManager的靜態方法中 通過ServiceLoader進行加載的。

然後,第二個問題的第二小問。Driver的實現類 是被哪個ClassLoader加載?
觀察加載邏輯,會發現 Driver的實現類 的加載 是通過 ServiceLoader實現的。而ServiceLoader在加載時,則是通過獲取Thread中的ContextClassLoader進行加載的 – Thread.currentThread().getContextClassLoader()。
那麼 這個ContextClassLoader到底是什麼呢? – 默認情況下(可以set進行修改)是 AppClassLoader。這樣就會形成如下的圖:
在這裏插入圖片描述
所以:如果讓BootstrapClassLoader(或者ExtClassLoader)去load一個MySQL的Driver,會發現 並不能成功的load到這個Driver實現類,但是,完全可以通過這種"破壞"的方式 來成功的回到上流的ClassLoader來load這個Driver實現類。
這就是 所謂的 “對雙親委派的破壞”,這也正是SPI的實現原理。

ServiceLoader使用方式 & SPI使用案例

ServiceLoader的使用需要三步操作

  1. 創建META-INF/services/service.name文件
  2. 將service的實現類的全限定名稱寫入該文件中
  3. 使用ServiceLoader.load(Service.class)來獲取實現類

SPI的使用案例很多,有興趣的可以好好看看:

  • org.slf4j.spi.SLF4JServiceProvider
  • com.facebook.presto.spi.Plugin
  • org.apache.flink.table.factories.TableFactory

後記

1 -- this.getClass().getResource(String name)
2 -- this.getClass().getClassLoader().getResource(String name)

最近正好使用了上面的來個方法,隨便跟蹤了一下源碼,這裏記錄一下:
只需要關注(1)的源碼即可

public java.net.URL getResource(String name) {
	name = resolveName(name);
	// 從這裏可以看出,上面的(1)實際上還是調用了(2)
    ClassLoader cl = getClassLoader0();
    // 這裏主要是 向上委派的遞歸過程(源碼就不展示了)
    return cl.getResource(name);
}

這裏關注一下resolveName邏輯

private String resolveName(String name) {
    if (!name.startsWith("/")) {
    	// this.getClass().getResource(".......") -- 將定位至this的相對路徑下
        Class<?> c = this;
        while (c.isArray()) { c = c.getComponentType(); }
        String baseName = c.getName();
        int index = baseName.lastIndexOf('.');
        if (index != -1) { name = baseName.substring(0, index).replace('.', '/')+"/"+name; }
    } else { 
		// this.getClass().getResource("/.......") -- 將定位至classpath下
	    name = name.substring(1); 
	}
    return name;
}

結論:

  • this.getClass().getResource("") – 相對於this的路徑
  • this.getClass().getResource("/") – classpath絕對路徑
  • this.getClass().getClassLoader().getResource("") – classpath絕對路徑
  • this.getClass().getClassLoader().getResource("/") – 爆炸?
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章