天天看點

深入分析 ClassLoader結論其它case分析(SpringBoot RestartClassLoader)

起因

針對下面這個代碼

public static void loadFile(){
    try(InputStream is = XXX.class.getClassLoader.getResourceAsStream("xxxxx.txt"){
        //xxxxxx
    }
}
           

有同僚提出為啥此處要用“XXX” claas,用其它的class是否可行?

然後我一通解釋,甚至還般出了

Thread.currentThread().getContextClassLoader().loadClass("xxx.xxx.xx")

的方案。但是進一步探讨時,發現成功的給自己挖了個坑。有以下幾個問題自己都解釋不通。

  • Thread上下文擷取到的類加載器與Class.getClassLoader()拿到的有什麼差別
  • 在加載一個類的時候,是如何找到一個類的位置的
  • 在加載檔案的檔案,第三方jar包中的配置檔案與自己項目下的Resource目錄下的加載方式有沒有什麼差別,是否需要不同的類的ClassLoader
  • 在加載檔案或者類資源的時候,對檔案的命名有何要求

在上面幾個問題的引導下,重新研究了下ClassLoader的相關文檔,并且寫例子驗證了這些問題,進而有了此文。

ClassLoader原理闡述

ClassLoader,顧名思義,就是類加載器。當我們調用一個new語句時,相應的class檔案需在被加載到Jvm中。此時ClassLoader就需要負責查找class,讀class檔案,校驗,連接配接,最終加載到Jvm中

  • java提供的預設ClassLoader
    • BootStrap ClassLoader: 加載位于

      System.getProperty("sun.boot.class.path")

      處的class檔案,是以可以通過

      sun.boot.class.path

      參數設定BootStrap ClassLoader負責加載的類
    • ExtClassLoader:加載位于

      System.getProperty("java.ext.dirs")

      的類
    • AppClassLoader:也稱

      SystemClassLoader

      , 加載位于

      System.getProperty("java.class.path")

      處的類
  • ClassLoader的雙親委托機制

    ClassLoader是一個虛類,除BootStrap ClassLoader外,每一個ClassLoader都需要繼承ClassLoader,并且每個都有一個其父ClassLoader的引用(同樣,BootStrap除外)。BootStrap ClassLoader是C++ 語言寫的,在系統啟動的時候,就會啟動BootStrap類加載器。BootStrap ClassLoader是ExtClassLoader的父加載器,但是如果擷取ExtClassLoader的父加載器,拿到是null;ExtClassLoader 是AppClassLoader的父加器

    但是雙親委托機制并不是絕對的,像後文中提到的SpringBoot的 RestartClassLoader就打破了該機制。

    在jvm啟動的時候,會加載類

    sun.misc.Launcher

    ,在該類的造函數中,會建立ExtClassLoader及AppClassLoader,并且把ExtClassLoader設定為AppClassLoader的父ClassLoader,同時設定線程的上下文加載器為AppClassLoader。
  • 類加載過程(預設的

    loadClass

    流程)
    • 首先AppClassLoader會檢查是否加載過該類,如果加載過,直接傳回
    • 沒有加載過,委托父加載器進行加載(父加載器執行同樣的流程)
    • 父加載器加載成功,直接傳回,如果沒有加載成功,則目前加載器開始查找Class再進行加載。
    ClassLoader進行了封裝,對于子類,不建議覆寫

    loadClass

    流程,隻實作其

    findClass

    方法即可,這樣就可以不破壞加載器的雙親委機制。是以對于自定義的ClassLoader而言,實作如何查找類,在哪裡查找類就是關鍵。

    對于查找資源檔案,與加載類一緻,在下面的源碼分析中會提到。

    對于線程的上下文加載器,等下在源碼分析中也可以看到一點,總體而言就是将一下加載器儲存線上程的上下文中,在某些特定的場景中使用,比如tomcat中會使用到。同時線程的上下文加載器與目前的類可能并不一是緻。

ClassLoader相關源碼分析

com.sun.Launcher

源碼(分析預設的幾個類加載器的構造過程)

public class Launcher {
    //系統的ClassLoader,後面會被指派為AppClassLoader
    private ClassLoader loader;
    //BootStrap ClassLoader要加載的類的範圍
    private static String bootClassPath = System.getProperty("sun.boot.class.path");

    public Launcher() {
        // 建立ExtClassLoader
        ClassLoader extcl = ExtClassLoader.getExtClassLoader();
        // 建立AppClassLoader,同時設定extcl為其父加載器
        loader = AppClassLoader.getAppClassLoader(extcl);
        //設定線程上下文類加載器為 AppClassLoader        Thread.currentThread().setContextClassLoader(loader);
    }

    /*
     * 擷取類加載器
     */
    public ClassLoader getClassLoader() {
        return loader;
    }

    /*
     * 具體加載ExtClassLoader的過程,并且可以看出,其繼承自URLClassLoader
     */
    static class ExtClassLoader extends URLClassLoader {
        private File[] dirs;
        // 建立方法
        public static ExtClassLoader getExtClassLoader() throws IOException
        {
            final File[] dirs = getExtDirs();

            try {
                return (ExtClassLoader) AccessController.doPrivileged(
                     new PrivilegedExceptionAction() {
                        public Object run() throws IOException {
                            int len = dirs.length;
                            for (int i = ; i < len; i++) {
                                MetaIndex.registerDirectory(dirs[i]);
                            }
                            return new ExtClassLoader(dirs);
                        }
                    });
            } catch (java.security.PrivilegedActionException e) {
                    throw (IOException) e.getException();
            }
        }

        /*
         * Creates a new ExtClassLoader for the specified directories.
         */
        public ExtClassLoader(File[] dirs) throws IOException {

            // 第二個參數就是parent ClassLoader,此處可以看到為null
            super(getExtURLs(dirs), null, factory);
            this.dirs = dirs;
        }

        private static File[] getExtDirs() {
            String s = System.getProperty("java.ext.dirs");
            File[] dirs;
            // 處理s相關的内容
            return dirs;
        }
    }

    /**
     * 建立AppClassLoader 
     */
    static class AppClassLoader extends URLClassLoader {

        public static ClassLoader getAppClassLoader(final ClassLoader extcl)
            throws IOException
        {
            // 參數  System.getProperty("java.class.path")
            final String s = System.getProperty("java.class.path");
            final File[] path = (s == null) ? new File[] : getClassPath(s);
            return (AppClassLoader)
                AccessController.doPrivileged(new PrivilegedAction() {
                public Object run() {
                    URL[] urls =
                        (s == null) ? new URL[] : pathToURLs(path);
                    return new AppClassLoader(urls, extcl);
                }
            });
        }

        /*
         * Creates a new AppClassLoader
         */
        AppClassLoader(URL[] urls, ClassLoader parent) {

            // 此處可以看到ExtClassLoader 是 AppClassLoader的父ClassLoader
            super(urls, parent, factory);
        }
    }
}
           

上面代碼可以看到,在建立Launcher的時候,建立了ExtClassLoader,并且将其parent ClassLoader設定為null,建立了AppClassLoader将ExtClassLoader設定為其parent ClassLoader。 同時可以看到每個ClassLoader都繼承自URLClassLoader,并且可以看到各ClassLoader負責加載的類的範圍

分析ClassLoader的源碼

public abstract class ClassLoader {
    // 儲存的地父加載器的引用
    private final ClassLoader parent;

    // 加載class檔案的核心代碼
    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) {
                long t0 = System.nanoTime();
                try {
                    // 如果父加載器不為空,則委托父加載器進行加載,可以看到雙親委托機制
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        // 加載器為空,即ExtClassLoader
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                //沒有加載到類,抛異常後,吞掉
                }

                // 如果父加載器沒有加載到,輪到自己進行加載
                if (c == null) {
                    // 這個方法是一個虛方法,還未實作,子類實作即可
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }    
}
           

從上面代碼可以看到,查找類的時候,采用雙親委托機制,先委托父類進行查找類,如果找不到自己再進行查找。

再來簡單介紹一個URLClassLoader:ClassLoader中有一個URLClassPath屬性,URLClassPath包含一個List的屬性,該屬性就儲存着目前ClassLoader可以查找的範圍。當URL以’/’結尾時,說明是一個directory,當是以非’/’結尾的時候,說明是一個jar包。當查找類或者資源檔案時,周遊該URL集合,根據是directory還是jar包分别進行處理。

再看查找資源檔案,getResource的方法:

//class 的getResource檔案, Class 類
   public java.net.URL getResource(String name) {
        name = resolveName(name);
        ClassLoader cl = getClassLoader0();
        if (cl==null) {
            // A system class.
            return ClassLoader.getSystemResource(name);
        }
        return cl.getResource(name);
    }
           

可以看到,Class.getResource() 最後是通過調用

ClassLoader.getResource()

實作的。目前在此之前,先進行了

resolveName(name)

方法。該方法的作用是擷取該資源檔案相關對于ClassPath的路徑,即如果以’/’打頭,則認為是一個完整路徑,去掉’/’,如果以非’/’打頭,則認為是相對目前類檔案的地方,則會将目前Class檔案的package名加到當明的name前,并且将’.’用’/’替換,形成相對于ClassPath的路徑。

ClassLoader的的getResouce的方法

// 同樣執行雙親委托機制
public URL getResource(String name) {
        URL url;
        if (parent != null) {
            url = parent.getResource(name);
        } else {
            url = getBootstrapResource(name);
        }
        if (url == null) {
            // findResource 是一個虛方法,子類可以實作,與類加載的過程類似。
            // 而對于AppClassLoader以及ExtClassLoader,即從adsw包含的Url屬性中查找相關的資源,包含從jar檔案中及目錄中。
            url = findResource(name);
        }
        return url;
    }
           

實驗證明

  • 自定義類加載器,加載非classPath下的檔案,并且驗證不同的類加載器加載到的類是否是同一個類
    //先定義一個我們想要加載的類
    package com.person;
    
    public class Test{
      // 該方法輸出自己的類加載器
      public void sayClassLoader(){
        System.out.println("classLoader:" + Test.class.getClassLoader());
      }
    }
    
    public class MainTest {
    
      public static void main(String[] args) throws Exception {
        // 我們把剛定義的java檔案編譯,放在/tmp/com/person下,其中 ‘/com/person’是對應java檔案的package
        String path = "/tmp/";
        URL url = new File(path).toURI().toURL();
        List<Class<?>> classes = Lists.newArrayList();
        for (int i = ; i < ; i++) {
          Thread thread = new Thread(() -> {
            try {
              //使用自定義的ClassLoader
              MyClassLoader loader = new MyClassLoader(new URL[]{url}, MainTest.class.getClassLoader());
              Class<?> aClass = loader.loadClass("com.person.Test");
              // 反射構執行個體以及調用 sayClassLoader 方法
              Object instance = aClass.newInstance();
              Method method = aClass.getMethod("sayClassLoader");
              method.invoke(instance);
              classes.add(aClass);
            } catch (ClassNotFoundException | IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) {
              e.printStackTrace();
            }
          });
          thread.start();
        }
        // 等待兩個線程執行完畢
        Thread.sleep();
        //判定兩個類是否是同一個類
        System.out.println(classes.get() == classes.get());
      }
    }
    
    //自定義的類加載器
    class MyClassLoader extends URLClassLoader{
    
      public MyClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
      }
    }
               
    執行結果
    classLoader:com.yqg.collection.MyClassLoader@20c489d2
    classLoader:com.yqg.collection.MyClassLoader@5e9b8a25
    false
               
    上面案例中,自定義的類加載器繼承了URLClassLoader,通過上面的case可以看出,自定義的類加載器加載了自定義的目錄上的檔案,并且發現兩個ClassLoader加載的類,即使是同一個類檔案,但是也不是同一個類。
  • 驗證類的雙新委托機制
    public static void main(String[] args) throws Exception {
    URLClassLoader classLoader = (URLClassLoader) MainTest.class.getClassLoader();
    // 這個反射的方法用于增加該ClassLoader的查找範圍,現在增加了/tmp目錄
    Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
    method.setAccessible(true);
    String path = "/tmp/";
    URL url = new File(path).toURI().toURL();
    method.invoke(classLoader, url);
    
    //用自定義的類加載器進行加載
    MyClassLoader loader = new MyClassLoader(new URL[]{url}, MainTest.class.getClassLoader());
    Class<?> aClass = loader.loadClass("com.person.Test");
    
    // 輸出最終類的加載器
    System.out.println(aClass.getClassLoader());
    }
               
    運作結果如下
    sun.misc.Launcher$AppClassLoader@18b4aac2
               
    從上面看出,

    com.person.Test

    類雖然是通過調用MyClassLoader進行加載但是根據雙親委托機制,最終由其父加載器AppClassLoader完成加載。

結論

  • 類通過ClassLoader進行加載,ClassLoader持有其父加載器的引有,正常情況下,當需要加載一個類或者一個資源檔案時,委托父加載器進行加載,當父加載器加載不到時,由目前加載器進行加載。但是這并不是必須的,像ogsi,tomcat就都有違反此機制的情況。
  • 加載資源檔案時,與加載類檔案一緻,隻要加載器是一樣的,其可加載的範圍就是一樣的,配置檔案可以是目前工程的配置檔案,也可能是第三方jar的配置檔案。
  • 當有多個資源檔案時,與類加載器的搜尋順序相關。是以可以在findResouce的時候,設定優先級及順序。
  • 不同的類加載器加載到的類檔案,不是同一個類,這個可用于在一些特定的場景中,比如tomcat的WebappClassLoader用于隔離多個Context,SpringBoot的RestartClassLoader僅用于加載适用于熱更新的類,像第三方jar包則由其它的類加載器進行加載。
  • 查找類檔案與查找檢查資源檔案基本上是一緻的,也取決于類加載器的實作。

其它case分析(SpringBoot RestartClassLoader)

// 同樣繼承了URLClassLoader
// 該類加載器隻關心可熱更新的類(自己項目),而第三方jar包會有其它的ClassLoader進行加載,當項目中的類發生變更時,隻需替換目前項目中的類即可,不需要加載全部的類
public class RestartClassLoader extends URLClassLoader implements SmartClassLoader {

    /**
     * Create a new {@link RestartClassLoader} instance.
     * @param parent the parent classloader
     * @param updatedFiles any files that have been updated since the JARs referenced in
     * URLs were created.
     * @param urls the urls managed by the classloader
     * @param logger the logger used for messages
     */
    public RestartClassLoader(ClassLoader parent, URL[] urls,
            ClassLoaderFileRepository updatedFiles, Log logger) {
        super(urls, parent);
        Assert.notNull(parent, "Parent must not be null");
        Assert.notNull(updatedFiles, "UpdatedFiles must not be null");
        Assert.notNull(logger, "Logger must not be null");
        this.updatedFiles = updatedFiles;
        this.logger = logger;
        if (logger.isDebugEnabled()) {
            logger.debug("Created RestartClassLoader " + toString());
        }
    }

    // 重寫loadClass方法
    public Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
        String path = name.replace('.', '/').concat(".class");
        // 先在目前的updatedFiles中找
        ClassLoaderFile file = this.updatedFiles.getFile(path);
        if (file != null && file.getKind() == Kind.DELETED) {
            throw new ClassNotFoundException(name);
        }
        Class<?> loadedClass = findLoadedClass(name);
        //  如是沒有找到
        if (loadedClass == null) {
            try {
                // 先自行進行加載
                loadedClass = findClass(name);
            }
            catch (ClassNotFoundException ex) {
                //如果沒有加載到,再調用父加載器進行加載,從這個地方可以看到,為了滿足熱更新,違背了雙親委托原則。
                loadedClass = getParent().loadClass(name);
            }
        }
        if (resolve) {
            resolveClass(loadedClass);
        }
        return loadedClass;
    }
}
           

繼續閱讀