天天看點

打破雙親委派機制

java類加載器

1,Bootstrap Classloader:根類加載器,負責加載java的核心類(java.lang.*等),它不是java.lang.ClassLoader的子類,而是由JVM自身實作,c++實作,構造ExtClassLoader和APPClassLoader。

2,Extension ClassLoader:擴充類加載器,擴充類加載器的加載路徑是JDK目錄下jre/lib/ext,擴充類的getParent()方法傳回null,實際上擴充類加載器的父類加載器是根加載器,隻是根加載器并不是Java實作的。

3,System ClassLoader:系統(應用)類加載器,它負責在JVM啟動時加載來自java指令的-classpath選項、java.class.path系統屬性或CLASSPATH環境變量所指定的jar包和類路徑。程式可以通過getSystemClassLoader()來擷取系統類加載器,,如果我們沒有實作自定義的類加載器那這玩意就是我們程式中的預設加載器,主要負責加載應用程式的主函數類。

雙親委派模型的工作過程

打破雙親委派機制

當一個Hello.class這樣的檔案要被加載時。不考慮我們自定義類加載器,首先會在AppClassLoader中檢查是否加載過,如果有那就無需再加載了。如果沒有,那麼會拿到父加載器,然後調用父加載器的loadClass方法。父類中同理會先檢查自己是否已經加載過,如果沒有再往上。注意這個過程,知道到達Bootstrap classLoader之前,都是沒有哪個加載器自己選擇加載的。如果父加載器無法加載,會下沉到子加載器去加載,一直到最底層,如果沒有任何加載器能加載,就會抛出ClassNotFoundException。

雙親委派機制代碼

public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}
//              -----??-----
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
        // 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);
            }
        }
        return c;
}
           

為什麼要設定雙親委派機制

這種設計有個好處是,如果有人想替換系統級别的類:String.java。篡改它的實作,但是在這種機制下這些系統的類已經被Bootstrap classLoader加載過了,是以并不會再去加載,從一定程度上防止了危險代碼的植入。

如何打破雙親委派模型

1,自定義類加載器,重寫loadClass方法;

2,使用線程上下文類加載器;

雙親委派破壞史

1,第一次破壞

由于雙親委派模型是在JDK1.2之後才被引入的,而類加載器和抽象類java.lang.ClassLoader則在JDK1.0時代就已經存在,面對已經存在的使用者自定義類加載器的實作代碼,Java設計者引入雙親委派模型時不得不做出一些妥協。在此之前,使用者去繼承java.lang.ClassLoader的唯一目的就是為了重寫loadClass()方法,因為虛拟機在進行類加載的時候會調用加載器的私有方法loadClassInternal(),而這個方法唯一邏輯就是去調用自己的loadClass()。

2,第二次破壞

雙親委派模型的第二次“被破壞”是由這個模型自身的缺陷所導緻的,雙親委派很好地解決了各個類加載器的基礎類的同一問題(越基礎的類由越上層的加載器進行加載),基礎類之是以稱為“基礎”,是因為它們總是作為被使用者代碼調用的API,但世事往往沒有絕對的完美。

如果基礎類又要調用回使用者的代碼,那該麼辦?

一個典型的例子就是JNDI服務,JNDI現在已經是Java的标準服務,

它的代碼由啟動類加載器去加載(在JDK1.3時放進去的rt.jar),但JNDI的目的就是對資源進行集中管理和查找,它需要調用由獨立廠商實作并部署在應用程式的ClassPath下的JNDI接口提供者的代碼,但啟動類加載器不可能“認識”這些代碼。

為了解決這個問題,Java設計團隊隻好引入了一個不太優雅的設計:線程上下文類加載器(Thread Context ClassLoader)。這個類加載器可以通過java.lang.Thread類的setContextClassLoader()方法進行設定,如果建立線程時還未設定,他将會從父線程中繼承一個,如果在應用程式的全局範圍内都沒有設定過的話,那這個類加載器預設就是應用程式類加載器。

有了線程上下文加載器,JNDI服務就可以使用它去加載所需要的SPI代碼,也就是父類加載器請求子類加載器去完成類加載的動作,這種行為實際上就是打通了雙親委派模型層次結構來逆向使用類加載器,實際上已經違背了雙親委派模型的一般性原則,但這也是無可奈何的事情。Java中所有涉及SPI的加載動作基本上都采用這種方式,例如JNDI、JDBC、JCE、JAXB和JBI等。

以JDBC加載驅動為例:

在JDBC4.0之後支援SPI方式加載java.sql.Driver的實作類。SPI實作方式為,通過ServiceLoader.load(Driver.class)方法,去各自實作Driver接口的lib的META-INF/services/java.sql.Driver檔案裡找到實作類的名字,通過Thread.currentThread().getContextClassLoader()類加載器加載實作類并傳回執行個體。

驅動加載的過程大緻如上,那麼是在什麼地方打破了雙親委派模型呢?

先看下如果不用Thread.currentThread().getContextClassLoader()加載器加載,整個流程會怎麼樣。

從META-INF/services/java.sql.Driver檔案得到實作類名字DriverA

Class.forName(“xx.xx.DriverA”)來加載實作類

Class.forName()方法預設使用目前類的ClassLoader,JDBC是在DriverManager類裡調用Driver的,目前類也就是DriverManager,它的加載器是BootstrapClassLoader。

用BootstrapClassLoader去加載非rt.jar包裡的類xx.xx.DriverA,就會找不到

要加載xx.xx.DriverA需要用到AppClassLoader或其他自定義ClassLoader

最終沖突出現在,要在BootstrapClassLoader加載的類裡,調用AppClassLoader去加載實作類

3,第三次破壞

雙親委派模型的第三次“被破壞”是由于使用者對程式動态性的追求導緻的,這裡所說的“動态性”指的是目前一些非常“熱門”的名詞:代碼熱替換、子產品熱部署等,簡答的說就是機器不用重新開機,隻要部署上就能用。

OSGi實作子產品化熱部署的關鍵則是它自定義的類加載器機制的實作。每一個程式子產品(Bundle)都有一個自己的類加載器,當需要更換一個Bundle時,就把Bundle連同類加載器一起換掉以實作代碼的熱替換。在OSGi幻境下,類加載器不再是雙親委派模型中的樹狀結構,而是進一步發展為更加複雜的網狀結構,當受到類加載請求時,OSGi将按照下面的順序進行類搜尋:

1)将java.*開頭的類委派給父類加載器加載。

2)否則,将委派清單名單内的類委派給父類加載器加載。

3)否則,将Import清單中的類委派給Export這個類的Bundle的類加載器加載。

4)否則,查找目前Bundle的ClassPath,使用自己的類加載器加載。

5)否則,查找類是否在自己的Fragment Bundle中,如果在,則委派給Fragment Bundle的類加載器加載。

6)否則,查找Dynamic Import清單的Bundle,委派給對應Bundle的類加載器加載。

7)否則,類加載器失敗。

打破雙親委派機制代碼實作

1,自己寫一個類加載器。

2,重寫loadclass方法。

3,重寫findclass方法。

4,定義Test類。

public class Test {
  public Test(){
    System.out.println(this.getClass().getClassLoader().toString());
  }
}
           

重新定義一個繼承ClassLoader的TestClassLoaderN類,它除了重寫findClass方法外還重寫了loadClass方法,預設的loadClass方法是實作了雙親委派機制的邏輯,即會先讓父類加載器加載,當無法加載時才由自己加載。這裡為了破壞雙親委派機制必須重寫loadClass方法,即這裡先嘗試交由System類加載器加載,加載失敗才會由自己加載。它并沒有優先交給父類加載器,這就打破了雙親委派機制。

public class TestClassLoaderN extends ClassLoader {

  private String name;

  public TestClassLoaderN(ClassLoader parent, String name) {
    super(parent);
    this.name = name;
  }

  @Override
  public String toString() {
    return this.name;
  }

  @Override
  public Class<?> loadClass(String name) throws ClassNotFoundException {
    Class<?> clazz = null;
    ClassLoader system = getSystemClassLoader();
    try {
      clazz = system.loadClass(name);
    } catch (Exception e) {
      // ignore
    }
    if (clazz != null)
      return clazz;
    clazz = findClass(name);
    return clazz;
  }

  @Override
  public Class<?> findClass(String name) {

    InputStream is = null;
    byte[] data = null;
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    try {
      is = new FileInputStream(new File("d:/Test.class"));
      int c = 0;
      while (-1 != (c = is.read())) {
        baos.write(c);
      }
      data = baos.toByteArray();
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      try {
        is.close();
        baos.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
    return this.defineClass(name, data, 0, data.length);
  }

  public static void main(String[] args) {
    TestClassLoaderN loader = new TestClassLoaderN(
        TestClassLoaderN.class.getClassLoader(), "TestLoaderN");
    Class clazz;
    try {
      clazz = loader.loadClass("test.classloader.Test");
      Object object = clazz.newInstance();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

}
           

如何在父加載器加載的類中,去調用子加載器去加載類?

1,jdk提供了兩種方式,Thread.currentThread().getContextClassLoader()和ClassLoader.getSystemClassLoader()一般都指向AppClassLoader,他們能加載classpath中的類。

2,SPI則用Thread.currentThread().getContextClassLoader()來加載實作類,實作在核心包裡的基礎類調用使用者代碼。