一文帶你深扒ClassLoader內核,揭開它的神祕面紗!

  • 「MoreThanJava」 宣揚的是 「學習,不止 CODE」
  • 若是以爲 「不錯」 的朋友,歡迎 「關注 + 留言 + 分享」,文末有完整的獲取連接,您的支持是我前進的最大的動力!

前言

ClassLoader 能夠說是 Java 最爲神祕的功能之一了,好像你們都知道怎麼回事兒 (雙親委派模型好像都都能說得出來...),又都說不清楚具體是怎麼一回事 (爲何須要須要有什麼實際用途就很模糊了...)html

今天,咱們就來深度扒一扒,揭開它神祕的面紗!java

Part 1. 類加載是作什麼的?

首先,咱們知道,Java 爲了實現 「一次編譯,處處運行」 的目標,採用了一種特別的方案:先 編譯與任何具體及其環境及操做系統環境無關的中間代碼(也就是 .class 字節碼文件),而後交由各個平臺特定的 Java 解釋器(也就是 JVM)來負責 解釋 運行。git

ClassLoader (顧名思義就是類加載器) 就是那個把字節碼交給 JVM 的搬運工 (加載進內存)。它負責將 字節碼形式 的 Class 轉換成 JVM 中 內存形式 的 Class 對象。程序員

字節碼能夠是來自於磁盤上的 .class 文件,也能夠是 jar 包裏的 *.class,甚至是來自遠程服務器提供的字節流。字節碼的本質其實就是一個有特定複雜格式的字節數組 byte[] (從後面解析 ClassLoader 類中的方法時更能體會)github

另外,類加載器不光能夠把 Class 加載到 JVM 之中並解析成 JVM 統一要求的對象格式,還有一個重要的做用就是 審查每一個類應該由誰加載web

並且,這些 Java 類不會一次所有加載到內存,而是在應用程序須要時加載,這也是須要類加載器的地方。面試

Part 2. ClassLoader 類結構分析

如下就是 ClassLoader 的主要方法了:數據庫

  • defineClass() 用於將 byte 字節流解析成 JVM 可以識別的 Class 對象。有了這個方法意味着咱們不只能夠經過 .class 文件實例化對象,還能夠經過其餘方式實例化對象,例如經過網絡接收到一個類的字節碼。segmentfault

    (注意,若是直接調用這個方法生成類的 Class 對象,這個類的 Class 對象尚未 resolve,JVM 會在這個對象真正實例化時才調用 resolveClass() 進行連接)數組

  • findClass() 一般和 defineClass() 一塊兒使用,咱們須要直接覆蓋 ClassLoader 父類的 findClass() 方法來實現類的加載規則,從而取得要加載類的字節碼。(如下是 ClassLoader 源碼)

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

    若是你不想從新定義加載類的規則,也沒有複雜的處理邏輯,只想在運行時可以加載本身制定的一個類,那麼你能夠用 this.getClass().getClassLoader().loadClass("class") 調用 ClassLoader 的 loadClass() 方法來獲取這個類的 Class 對象,這個 loadClass() 還有重載方法,你一樣能夠決定再何時解析這個類。

  • loadClass() 用於接受一個全類名,而後返回一個 Class 類型的對象。(該方法源碼蘊含了著名的雙親委派模型)

  • resolveClass() 用於對 Class 進行 連接,也就是把單一的 Class 加入到有繼承關係的類樹中。若是你想在類被加載到 JVM 中時就被連接(Link),那麼能夠在調用 defineClass() 以後緊接着調用一個 resolveClass() 方法,固然你也能夠選擇讓 JVM 來解決何時才連接這個類(一般是真正被實實例化的時候)。

ClassLoader 是個抽象類,它還有不少子類,若是咱們要實現本身的 ClassLoader,通常都會繼承 URLClassLoader 這個子類,由於這個類已經幫咱們實現了大部分工做。

例如,咱們來看一下 java.net.URLClassLoader.findClass() 方法的實現:

// 入參爲 Class 的 binary name,如 java.lang.String
protected Class<?> findClass(final String name) throws ClassNotFoundException {
    // 以上代碼省略
  
    // 經過 binary name 生成包路徑,如 java.lang.String -> java/lang/String.class
    String path = name.replace('.''/').concat(".class");
    // 根據包路徑,找到該 Class 的文件資源
    Resource res = ucp.getResource(path, false);
    if (res != null) {
        try {
           // 調用 defineClass 生成 java.lang.Class 對象
            return defineClass(name, res);
        } catch (IOException e) {
            throw new ClassNotFoundException(name, e);
        }
    } else {
        return null;
    }
  
    // 如下代碼省略
}

Part 3. Java 類加載流程詳解

如下就是 ClassLoader 加載一個 class 文件到 JVM 時須要通過的步驟。

事實上,咱們每一次在 IDEA 中點擊運行時,IDE 都會默認替咱們執行如下的命令:

  • javac Xxxx.java ➡️ 找到源文件中的 public class,再找 public class 引用的其餘類,Java 編譯器會根據每個類生成一個字節碼文件;
  • java Xxxx ➡️ 找到文件中的惟一主類 public class,並根據 public static 關鍵字找到跟主類關聯可執行的 main 方法 (這也是爲何 main 方法須要被定義爲 public static void 的緣由了——咱們須要在類沒有加載時訪問),開始執行。

在真正的運行 main 方法以前,JVM 須要 加載、連接 以及 初始化 上述的 Xxxx 類。

第一步:加載(Loading)

這一步是讀取到類文件產生的二進制流(findClass()),並轉換爲特定的數據結構(defineClass()),初步校驗 cafe babe 魔法數 (二進制中前四個字節爲 0xCAFEBABE 用來標識該文件是 Java 文件,這是不少軟件的作法,好比 zip壓縮文件、常量池、文件長度、是否有父類等,而後在 Java 中建立對應類的 java.lang.Class 實例,類中存儲的各部分信息也須要對應放入 運行時數據區 中(例如靜態變量、類信息等放入方法區)。

如下是一個 Class 文件具備的基本結構的簡單圖示:

若是對 Class 文件更多細節感興趣的能夠進一步閱讀:https://juejin.im/post/6844904199617003528

這裏咱們可能會有一個疑問,爲何 JVM 容許尚未進行驗證、準備和解析的類信息放入方法區呢?

答案是加載階段和連接階段的部分動做(好比一部分字節碼文件格式驗證動做)是 交叉進行 的,也就是說 加載階段還沒完成,連接階段可能已經開始了。但這些夾雜在加載階段的動做(驗證文件格式等)仍然屬於連接操做。

第二步:連接(Linking)

Link 階段包括驗證、準備、解析三個步驟。下面👇咱們來詳細說說。

驗證:確保被加載的類的正確性

驗證是鏈接階段的第一步,這一階段的目的是 爲了確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。驗證階段大體會完成 4 個階段的檢驗動做:

  • 文件格式驗證: 驗證字節流是否符合 Class 文件格式的規範;例如:是否以 0xCAFEBABE 開頭、主次版本號是否在當前虛擬機的處理範圍以內、常量池中的常量是否有不被支持的類型。
  • 元數據驗證: 對字節碼描述的信息進行語義分析(注意:對比 javac 編譯階段的語義分析),以保證其描述的信息符合 Java 語言規範的要求;例如:這個類是否有父類,除了 java.lang.Object 以外。
  • 字節碼驗證: 經過數據流和控制流分析,肯定程序語義是合法的、符合邏輯的。
  • 符號引用驗證: 確保解析動做能正確執行。

驗證階段是很是重要的,但不是必須的,它對程序運行期沒有影響,若是所引用的類通過反覆驗證,那麼能夠考慮採用 -Xverifynone 參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。

準備:爲類的靜態變量分配內存,並將其初始化爲默認值

準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些內存都將在 方法區 中分配。對於該階段有如下幾點須要注意:

  • 1️⃣ 這時候進行內存分配的 僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨着對象一塊分配在 Java 堆中。

  • 2️⃣ 這裏所設置的 初始值一般狀況下是數據類型默認的零值(如 00Lnullfalse等),而不是被在 Java 代碼中被顯式地賦予的值。

  • 3️⃣ 若是類字段的字段屬性表中存在 ConstantValue 屬性,即 同時被 finalstatic 修飾,那麼在準備階段變量 value 就會被初始化爲 ConstValue 屬性所指定的值。

➡️ 例如,假設這裏有一個類變量 public static int value = 666;,在準備階段時初始值是 0 而不是 666,在 初始化階段 纔會被真正賦值爲 666

➡️ 假設是一個靜態類變量 public static final int value = 666;,則再準備階段 JVM 就已經賦值爲 666 了。

解析:把類中的符號引用轉換爲直接引用(重要)

解析階段是虛擬機將常量池內的 符號引用 替換爲 直接引用 的過程,解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符 7 類符號引用進行。

➡️ 符號引用 的做用是在編譯的過程當中,JVM 並不知道引用的具體地址,因此用符號引用進行代替,而在解析階段將會將這個符號引用轉換爲真正的內存地址。

➡️ 直接引用 能夠理解爲指向 類、變量、方法 的指針,指向 實例 的指針和一個 間接定位 到對象的對象句柄。

爲了理解👆上面兩種概念的區別,來看一個實際的例子吧:

public class Tester {

    public static void main(String[] args) {
        String str = "關注【我沒有三顆心臟】,關注更多精彩";
        System.out.println(str);
    }
}

咱們先在該類同級目錄下運行 javac Tester 編譯成 .class 文件而後再利用 javap -verbose Tester 查看類的詳細信息 (爲了節省篇幅只截取了 main 方法反編譯後的代碼)

// 上面是類的詳細信息省略...
{
 // .....
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: ldc           #7                  // String 關注【我沒有三顆心臟】,關注更多精彩
         2: astore_1
         3: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
         6: aload_1
         7: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        10: return
      LineNumberTable:
        line 4: 0
        line 5: 3
        line 6: 10
}
SourceFile: "Tester.java"

能夠看到,上面👆定義的 str 變量在編譯階段會被解析稱爲 符號引用,符號引用的標誌是 astore_<n>,這裏就是 astore_1

store_1的含義是將操做數棧頂的 關注【我沒有三顆心臟】,關注更多精彩 保存回索引爲 1 的局部變量表中,此時訪問變量 str 就會讀取局部變量表索引值爲 1 中的數據。因此局部變量 str 就是一個符號引用。

再來看另一個例子:

public class Tester {

    public static void main(String[] args) {
        System.out.println("關注【我沒有三顆心臟】,關注更多精彩");
    }
}

這一段代碼反編譯以後獲得以下的代碼:

// 上面是類的詳細信息省略...
{
  // ......
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #13                 // String 關注【我沒有三顆心臟】,關注更多精彩
         5: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 4: 0
        line 5: 8
}
SourceFile: "Tester.java"

咱們能夠看到這裏直接使用了 ldc 指令將 關注【我沒有三顆心臟】,關注更多精彩 推送到了棧,緊接着就是調用指令 invokevirtual,並無將字符串存入局部變量表中,這裏的字符串就是一個 直接引用

第三步:初始化(Initialization)

初始化,爲類的靜態變量賦予正確的初始值,JVM 負責對類進行初始化,主要對類變量進行初始化。在 Java 中對類變量進行初始值設定有兩種方式:

  • 1️⃣ 聲明類變量是指定初始值;
  • 2️⃣ 使用靜態代碼塊爲類變量指定初始值;

JVM 初始化步驟:

  • 1️⃣ 假如這個類尚未被加載和鏈接,則程序先加載並鏈接該類
  • 2️⃣ 假如該類的直接父類尚未被初始化,則先初始化其直接父類
  • 3️⃣ 假如類中有初始化語句,則系統依次執行這些初始化語句

類初始化時機:只有當對類的主動使用的時候纔會致使類的初始化,類的主動使用包括如下幾種:

  • 建立類的實例,也就是 new 的方式
  • 訪問某個類或接口的靜態變量,或者對該靜態變量賦值
  • 調用類的靜態方法
  • 反射(如 Class.forName("com.wmyskxz.Tester")
  • 初始化某個類的子類,則其父類也會被初始化
  • Java 虛擬機啓動時被標明爲啓動類的類,直接使用 java.exe 命令來運行某個主類
  • 使用 JDK 7 新加入的動態語言支持時,若是一個 java.lang.invoke.MethodHanlde 實例最後的解析結果爲 REF_getstaticREF_putstaticREF_invokeStaticREF_newInvokeSpecial 四種類型的方法句柄時,都須要先初始化該句柄對應的類
  • 接口中定義了 JDK 8 新加入的默認方法( default修飾符), 實現類在初始化以前須要先初始化其接口

Part 4. 深刻理解雙親委派模型

咱們在上面👆已經瞭解了一個類是如何被加載進 JVM 的——依靠類加載器——在 Java 語言中自帶有三個類加載器:

  • Bootstrap ClassLoader 最頂層的加載類,主要加載   核心類庫%JRE_HOME%\lib 下的 rt.jarresources.jarcharsets.jarclass 等。
  • Extention ClassLoader 擴展的類加載器,加載目錄 %JRE_HOME%\lib\ext 目錄下的 jar 包和 class 文件。
  • Appclass Loader 也稱爲 SystemAppClass 加載當前應用的 classpath 的全部類。

咱們能夠經過一個簡單的例子來簡單瞭解 Java 中這些自帶的類加載器:

public class PrintClassLoader {

    public static void main(String[] args) {
        printClassLoaders();
    }

    public static void printClassLoaders() {
        System.out.println("Classloader of this class:"
            + PrintClassLoader.class.getClassLoader());
        System.out.println("Classloader of Logging:"
            + com.sun.javafx.util.Logging.class.getClassLoader());
        System.out.println("Classloader of ArrayList:"
            + java.util.ArrayList.class.getClassLoader());
    }
}

上方程序打印輸出以下:

Classloader of this class:sun.misc.Launcher$AppClassLoader@18b4aac2
Classloader of Logging:sun.misc.Launcher$ExtClassLoader@60e53b93
Classloader of ArrayList:null

如咱們所見,這裏分別對應三種不一樣類型的類加載器:AppClassLoader、ExtClassLoader 和 BootstrapClassLoader(顯示爲 null)。

一個很好的問題是:Java 類是由 java.lang.ClassLoader 實例加載的,但類加載器自己也是類,那麼誰來加載類加載器呢?

咱們僞裝不知道,先來跟着源碼一步一步來看。

先來看看 Java 虛擬機入口代碼

在 JDK 源碼 sun.misc.Launcher 中,蘊含了 Java 虛擬機的入口方法:

public class Launcher {
    private static Launcher launcher = new Launcher();
    private static String bootClassPath =
        System.getProperty("sun.boot.class.path");

    public static Launcher getLauncher() {
        return launcher;
    }

    private ClassLoader loader;

    public Launcher() {
        // Create the extension class loader
        ClassLoader extcl;
        try {
            extcl = ExtClassLoader.getExtClassLoader();
        } catch (IOException e) {
            throw new InternalError(
                "Could not create extension class loader", e);
        }

        // Now create the class loader to use to launch the application
        try {
            loader = AppClassLoader.getAppClassLoader(extcl);
        } catch (IOException e) {
            throw new InternalError(
                "Could not create application class loader", e);
        }

        // 設置 AppClassLoader 爲線程上下文類加載器,這個文章後面部分講解
        Thread.currentThread().setContextClassLoader(loader);
    }
    /*
     * Returns the class loader used to launch the main application.
     */

    public ClassLoader getClassLoader() {
        return loader;
    }
    /*
     * The class loader used for loading installed extensions.
     */

    static class ExtClassLoader extends URLClassLoader {}
  /**
     * The class loader used for loading from java.class.path.
     * runs in a restricted security context.
     */

    static class AppClassLoader extends URLClassLoader {}
}

源碼有精簡,可是咱們能夠獲得如下信息:

1️⃣ Launcher 初始化了 ExtClassLoader 和 AppClassLoader。

2️⃣ Launcher 沒有看到 Bootstrap ClassLoader 的影子,可是有一個叫作 bootClassPath 的變量,大膽一猜就是 Bootstrap ClassLoader 加載的 jar 包的路徑。

(ps: 能夠本身嘗試輸出一下 System.getProperty("sun.boot.class.path") 的內容,它正好對應了 JDK 目錄 libclasses 目錄下的 jar 包——也就是一般你配置環境變量時設置的 %JAVA_HOME/lib 的目錄了——一樣的方式你也能夠看看 Ext 和 App 的源碼)

3️⃣ ExtClassLoader 和 AppClassLoader 都繼承自 URLClassLoader,進一步查看 ClassLoader 的繼承樹,傳說中的雙親委派模型也並無出現。(甚至看不到 Bootstrap ClassLoader 的影子,Ext 也沒有直接繼承自 App 類加載器)

ClassLoader 繼承樹

(⚠️注意,這裏能夠明確看到每個 ClassLoader 都有一個 parent 變量,用於標識本身的父類,下面👇詳細說)

4️⃣ 注意如下代碼:

ClassLoader extcl;
        
extcl = ExtClassLoader.getExtClassLoader();

loader = AppClassLoader.getAppClassLoader(extcl);

分別跟蹤查看到這兩個 ClassLoader 初始化時的代碼:

// 一直追蹤到最頂層的 ClassLoader 定義,構造器的第二個參數標識了類加載器的父類
private ClassLoader(Void unused, ClassLoader parent) {
  this.parent = parent;
  // 代碼省略.....
}
// Ext 設置本身的父類爲 null
public ExtClassLoader(File[] var1) throws IOException {
  super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
  SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}
// 手動把 Ext 設置爲 App 的 parent(這裏的 var2 是傳進來的 extc1)
AppClassLoader(URL[] var1, ClassLoader var2) {
  super(var1, var2, Launcher.factory);
  this.ucp.initLookupCache(this);
}

由此,咱們獲得了這樣一個類加載器的關係圖:

類加載器的父類都來自哪裏?

奇怪,爲何 ExtClassLoader 的 parent 明明是 null,咱們卻通常地認爲 Bootstrap ClassLoader 纔是 ExtClassLoader 的父加載器呢?

答案的一部分就藏在 java.lang.ClassLoader.loadClass() 方法裏面:(這也就是著名的「雙親委派模型」現場了)

protected Class<?> loadClass(String name, boolean resolve)
  throws ClassNotFoundException
{
  synchronized (getClassLoadingLock(name)) {
    // 首先檢查是否已經加載過了
    Class<?> c = findLoadedClass(name);
    if (c == null) {
      long t0 = System.nanoTime();
      try {
        if (parent != null) {
     // 父加載器不爲空則調用父加載器的 loadClass 方法
          c = parent.loadClass(name, false);
        } else {
          // 父加載器爲空則調用 Bootstrap ClassLoader
          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.
        long t1 = System.nanoTime();
        // 父加載器沒有找到,則調用 findclass
        c = findClass(name);

        // this is the defining class loader; record the stats
        sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
        sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
        sun.misc.PerfCounter.getFindClasses().increment();
      }
    }
    if (resolve) {
      // 調用 resolveClass()
      resolveClass(c);
    }
    return c;
  }
}

代碼邏輯很好地解釋了雙親委派的原理。

1️⃣ 當前 ClassLoader 首先從 本身已經加載的類中查詢是否此類已經加載,若是已經加載則直接返回原來已經加載的類。(每一個類加載器都有本身的加載緩存,當一個類被加載了之後就會放入緩存,等下次加載的時候就能夠直接返回了。)

2️⃣ 當前 ClassLoader 的緩存中沒有找到被加載的類的時候,委託父類加載器去加載,父類加載器採用一樣的策略,首先查看本身的緩存,而後委託父類的父類去加載,一直到 Bootstrap ClassLoader。(當全部的父類加載器都沒有加載的時候,再由當前的類加載器加載,並將其放入它本身的緩存中,以便下次有加載請求的時候直接返回。)

因此,答案的另外一部分是由於最高一層的類加載器 Bootstrap 是經過 C/C++ 實現的,並不存在於 JVM 體系內 (不是一個 Java 類,沒辦法直接表示爲 ExtClassLoader 的父加載器),因此輸出爲 null

(咱們能夠很輕易跟蹤到 findBootstrapClass() 方法被 native 修飾:private native Class<?> findBootstrapClass(String name);

➡️ OK,咱們理解了爲何 ExtClassLoader 的父加載器爲何是表示爲 null 的 Bootstrap 加載器,那咱們 本身實現的 ClassLoader 父加載器應該是誰呢?

觀察一下 ClassLoader 的源碼就知道了:

protected ClassLoader(ClassLoader parent) {
    this(checkCreateClassLoader(), parent);
}
protected ClassLoader() {
    this(checkCreateClassLoader(), getSystemClassLoader());
}

類加載器的 parent 的賦值是在 ClassLoader 對象的構造方法中,它有兩個狀況:

1️⃣ 由外部類建立 ClassLoader 時直接指定一個 ClassLoader 爲 parent

2️⃣ 由 getSystemClassLoader() 方法生成,也就是在 sun.misc.Laucher 經過 getClassLoader() 獲取,也就是 AppClassLoader。直白的說,一個 ClassLoader 建立時若是沒有指定 parent,那麼它的 parent 默認就是 AppClassLoader。(建議去看一下源碼)

爲何這樣設計呢?

簡單來講,主要是爲了 安全性,避免用戶本身編寫的類動態替換 Java 的一些核心類,好比 String,同時也 避免了重複加載,由於 JVM 中區分不一樣類,不只僅是根據類名,相同的 class 文件被不一樣的 ClassLoader 加載就是不一樣的兩個類,若是相互轉型的話會拋 java.lang.ClassCaseException

若是咱們要實現本身的類加載器,無論你是直接實現抽象類 ClassLoader,仍是繼承 URLClassLoader 類,或者其餘子類,它的父加載器都是 AppClassLoader。

由於無論調用哪一個父類構造器,建立的對象都必須最終調用 getSystemClassLoader() 做爲父加載器 (咱們已經從上面👆的源碼中看到了)。而該方法最終獲取到的正是 AppClassLoader (別稱 SystemClassLoader)

這也就是咱們熟知的最終的雙親委派模型了。

Part 5. 實現本身的類加載器

什麼狀況下須要自定義類加載器

在學習了類加載器的實現機制以後,咱們知道了雙親委派模型並不是強制模型,用戶能夠自定義類加載器,在什麼狀況下須要自定義類加載器呢?

1️⃣ 隔離加載類。在某些框架內進行中間件與應用的模塊隔離,把類加載器到不一樣的環境。好比,阿里內某容器框架經過自定義類加載器確保應用中依賴的 jar 包不會影響到中間件運行時使用的 jar 包。

2️⃣ 修改類加載方式。類的加載模型並不是強制,除了 Bootstrap 外,其餘的加載並不是必定要引入,或者根據實際狀況在某個時間點進行按需的動態加載。

3️⃣ 擴展加載源。好比從數據庫、網絡,甚至是電視機頂盒進行加載。(下面👇咱們會編寫一個從網絡加載類的例子)

4️⃣ 防止源碼泄露。Java 代碼容易被編譯和篡改,能夠進行編譯加密。那麼類加載器也須要自定義,還原加密的字節碼。

一個常規的例子

實現一個自定義的類加載器比較簡單:繼承 ClassLoader,重寫 findClass() 方法,調用 defineClass() 方法,就差很少行了。

Tester.java

咱們先來編寫一個測試用的類文件:

public class Tester {

    public void say() {
        System.out.println("關注【我沒有三顆心臟】,解鎖更多精彩!");
    }
}

在同級目錄下執行 javac Tester.java 命令,並把編譯後的 Tester.class 放到指定的目錄下(我這邊爲了方便就放在桌面上啦 /Users/wmyskxz/Desktop

MyClassLoader.java

咱們編寫自定義 ClassLoader 代碼:

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class MyClassLoader extends ClassLoader {

    private final String mLibPath;

    public MyClassLoader(String path) {
        // TODO Auto-generated constructor stub
        mLibPath = path;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // TODO Auto-generated method stub

        String fileName = getFileName(name);

        File file = new File(mLibPath, fileName);

        try {
            FileInputStream is = new FileInputStream(file);

            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            int len = 0;
            try {
                while ((len = is.read()) != -1) {
                    bos.write(len);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

            byte[] data = bos.toByteArray();
            is.close();
            bos.close();

            return defineClass(name, data, 0, data.length);

        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        return super.findClass(name);
    }

    // 獲取要加載的 class 文件名
    private String getFileName(String name) {
        // TODO Auto-generated method stub
        int index = name.lastIndexOf('.');
        if (index == -1) {
            return name + ".class";
        } else {
            return name.substring(index + 1) + ".class";
        }
    }
}

咱們在 findClass() 方法中定義了查找 class 的方法,而後數據經過 defineClass() 生成了 Class 對象。

ClassLoaderTester 測試類

咱們須要刪除剛纔在項目目錄建立的 Tester.java 和編譯後的 Tester.class 文件來觀察效果:

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ClassLoaderTester {

    public static void main(String[] args) {
        // 建立自定義的 ClassLoader 對象
        MyClassLoader myClassLoader = new MyClassLoader("/Users/wmyskxz/Desktop");
        try {
            // 加載class文件
            Class<?> c = myClassLoader.loadClass("Tester");

            if(c != null){
                try {
                    Object obj = c.newInstance();
                    Method method = c.getDeclaredMethod("say",null);
                    //經過反射調用Test類的say方法
                    method.invoke(obj, null);
                } catch (InstantiationException | IllegalAccessException
                    | NoSuchMethodException
                    | SecurityException |
                    IllegalArgumentException |
                    InvocationTargetException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

運行測試,正常輸出:

關注【我沒有三顆心臟】,解鎖更多精彩!

加密解密類加載器

突破了 JDK 系統內置加載路徑的限制以後,咱們就能夠編寫自定義的 ClassLoader。你徹底能夠按照本身的意願進行業務的定製,將 ClassLoader 玩出花樣來。

例如,一個加密解密的類加載器。(不涉及完整代碼,咱們能夠來講一下思路和關鍵代碼)

首先,在編譯以後的字節碼文件中動一動手腳,例如,給文件每個 byte 異或一個數字 2:(這就算是模擬加密過程)

File file = new File(path);
try {
  FileInputStream fis = new FileInputStream(file);
  FileOutputStream fos = new FileOutputStream(path+"en");
  int b = 0;
  int b1 = 0;
  try {
    while((b = fis.read()) != -1){
      // 每個 byte 異或一個數字 2
      fos.write(b ^ 2);
    }
    fos.close();
    fis.close();
  } catch (IOException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
  }
catch (FileNotFoundException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
}

而後咱們再在 findClass() 中本身解密:

File file = new File(mLibPath,fileName);

try {
  FileInputStream is = new FileInputStream(file);

  ByteArrayOutputStream bos = new ByteArrayOutputStream();
  int len = 0;
  byte b = 0;
  try {
    while ((len = is.read()) != -1) {
      // 將數據異或一個數字 2 進行解密
      b = (byte) (len ^ 2);
      bos.write(b);
    }
  } catch (IOException e) {
    e.printStackTrace();
  }

  byte[] data = bos.toByteArray();
  is.close();
  bos.close();

  return defineClass(name,data,0,data.length);

catch (IOException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
}

(代碼幾乎與上面👆一個例子等同,因此只說一下思路和完整代碼)

網絡類加載器

其實很是相似,也不作過多講解,直接上代碼:

import java.io.ByteArrayOutputStream;  
import java.io.InputStream;  
import java.net.URL;  
  
public class NetworkClassLoader extends ClassLoader {  
  
    private String rootUrl;  
  
    public NetworkClassLoader(String rootUrl) {  
        // 指定URL  
        this.rootUrl = rootUrl;  
    }  
  
    // 獲取類的字節碼  
    @Override  
    protected Class<?> findClass(String name) throws ClassNotFoundException {  
        byte[] classData = getClassData(name);  
        if (classData == null) {  
            throw new ClassNotFoundException();  
        } else {  
            return defineClass(name, classData, 0, classData.length);  
        }  
    }  
  
    private byte[] getClassData(String className) {  
        // 從網絡上讀取的類的字節  
        String path = classNameToPath(className);  
        try {  
            URL url = new URL(path);  
            InputStream ins = url.openStream();  
            ByteArrayOutputStream baos = new ByteArrayOutputStream();  
            int bufferSize = 4096;  
            byte[] buffer = new byte[bufferSize];  
            int bytesNumRead = 0;  
            // 讀取類文件的字節  
            while ((bytesNumRead = ins.read(buffer)) != -1) {  
                baos.write(buffer, 0, bytesNumRead);  
            }  
            return baos.toByteArray();  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
        return null;  
    }  
  
    private String classNameToPath(String className) {  
        // 獲得類文件的URL  
        return rootUrl + "/"  
                + className.replace('.''/') + ".class";  
    }  
}  

(代碼來自:https://blog.csdn.net/justloveyou_/article/details/72217806)

Part 6. 必要的擴展閱讀

學習到這裏,咱們對 ClassLoader 已經再也不陌生了,可是仍然有一些必要的知識點須要去掌握 (限於篇幅和能力這裏不擴展了),但願您能認真閱讀如下的材料:(可能排版上面層次不齊,但內容都是有質量的,並用 ♨️ 標註了更加劇點一些的內容)

1️⃣ ♨️能不能本身寫一個類叫 java.lang.System 或者 java.lang.String - https://blog.csdn.net/tang9140/article/details/42738433

2️⃣ 深刻理解 Java 之 JVM 啓動流程 - https://cloud.tencent.com/developer/article/1038435

3️⃣ ♨️真正理解線程上下文類加載器(多案例分析) - https://blog.csdn.net/yangcheng33/article/details/52631940

4️⃣ ♨️曹工雜談:Java 類加載器還會死鎖?這是什麼狀況? - https://www.cnblogs.com/grey-wolf/p/11378747.html#_label2

5️⃣ 謹防JDK8重複類定義形成的內存泄漏 - https://segmentfault.com/a/1190000022837543

7️⃣ ♨️Tomcat 類加載器的實現 - https://juejin.im/post/6844903945496690695

8️⃣ ♨️Spring 中的類加載機制 - https://www.shuzhiduo.com/A/gVdnwgAlzW/

參考資料

  1. 《深刻分析 Java Web 技術內幕》 | 許令波 著
  2. Java 類加載機制分析 - https://www.jianshu.com/p/3615403c7c84
  3. Class 文件解析實戰 - https://juejin.im/post/6844904199617003528
  4. 圖文兼備看懂類加載機制的各個階段,就差你了!- https://juejin.im/post/6844904119258316814
  5. Java面試知識點解析(三)——JVM篇 - https://www.wmyskxz.com/2018/05/16/java-mian-shi-zhi-shi-dian-jie-xi-san-jvm-pian/
  6. 一看你就懂,超詳細Java中的ClassLoader詳解 - https://blog.csdn.net/briblue/article/details/54973413
  • 本文已收錄至個人 Github 程序員成長系列 【More Than Java】,學習,不止 Code,歡迎 star:https://github.com/wmyskxz/MoreThanJava
  • 我的公衆號 :wmyskxz, 我的獨立域名博客:wmyskxz.com,堅持原創輸出,下方掃碼關注,2020,與您共同成長!

(END)




有你想看的 精彩




「MoreThanJava」計算機發展史—從織布機到IBM
全網最通透的Java8版本特性講解
什麼?Java9這些史詩級更新你都不知道?Java9特性一文打盡!
Java 14版本特性【一文了解】它來了!
這裏有你不得不瞭解的Java 11版本特性說明
面試問我,建立多少個線程合適?我該怎麼說
【吐血推薦】領域驅動設計學習輸出
「懂一點設計」一篇文章讀懂交互設計7大定律



很是感謝各位人才能 看到這裏,若是以爲本篇文章寫得不錯,以爲 「我沒有三顆心臟」有點東西 的話,求點贊,求關注,求分享,求留言!

創做不易,各位的支持和承認,就是我創做的最大動力,咱們下篇文章見!


點擊留言

本文分享自微信公衆號 - 我沒有三顆心臟(wmyskxz)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索