classloader, 你究竟能幹啥

咱們知道java語言是一次編譯,多平臺運行。這得益於Java在設計的時候,把編譯和運行是獨立的兩個流程。編譯負責把源代碼編譯成 JVM 可識別的字節碼,運行時加載字節碼,並解釋成機器指令運行。java

由於是源代碼編譯成字節碼,因此 JVM 平臺除了java語言外,還有groovy,scala等。 由於是加載字節碼運行,因此有apm,自定義classloader,動態語言等技術。構成了豐富的Java 世界。mysql

javac 編譯流程

javac 編譯流程

  1. parse:讀取.java源文件,作詞法分析(LEXER)和語法分析(PARSER)
  2. enter:生成符號表
  3. process:處理註解
  4. attr:檢查語義合法性、常量摺疊
  5. flow:數據流分析
  6. desugar:去除語法糖
  7. generate:生成字節碼

編譯期主要的目的是把 java 源代碼編譯爲 符合 jvm 規範的的字節碼。在運行期,由 jvm 加載字節碼並執行,程序就運行起來了。git

其實java語言和 jvm 是沒有綁定關係。只要符合jvm規範的字節碼均可以執行,可是字節碼不必定由Java語言編譯而來。正因如此,jvm 平臺涌現出了groovy,scala,kotlin等衆多語言。sql

若是你感興趣,也能夠把把你喜歡的語言搬到 jvm 上運行。數組

類的生命週期

類的聲明週期

  1. loading:加載。是第一個階段,主要是加載字節碼,靜態存儲結構轉化爲方法區數據結構,生成class對象。這裏沒有限制字節碼的來源,能夠是文件、zip,網絡、jsp,甚至是加密文件。這個階段可使用自定義 classloader 實現自定義行爲,這就給字節碼帶來了不少可能的玩法。
  2. verification:驗證。確保字節碼符合 jvm 規範。
  3. preparation:準備。正式爲類中定義的變量設置初始值。
  4. resolution:解析。將常量池內的符號引用替換爲直接引用的過程。
  5. initialization: 初始化。這裏將程序的主導權交給了應用程序,會執行·()和構造函數。
  6. using:使用。使用初始化後的類,這裏就到了應用邏輯的範疇。
  7. unloading:卸載。須要知足該類全部實例已經被GC,加載該類的ClassLoader已經被GC,該類的java.lang.Class對象已經沒有被引用。在tomcat jsp 熱加載的場景會用到,每一個jsp都是單獨的 classloader,當jsp有變更時,會卸載舊的classloader,建立新的classloader加載jsp,這樣就實現了熱加載。

在 initialization 階段以前,只有 loading 段能夠經過自定義 Classloader 添加自定義邏輯,其餘階段都是由 JVM 完成的。這就是本文想要表達的重點,Classloader 究竟能作什麼呢。tomcat

雙親委派

在瞭解 Classloader 究竟能作什麼以前,必需要先了解一下雙親委派模型。衆所周知,java 是單繼承的,classloader 也繼承了這種設計思想。安全

這裏針對 JDK 8 版本介紹,JDK9 以後引入了模塊功能,classloader 繼承關係有所變化。bash

雙親委派

站在 JVM 的角度,只有兩種加載器,一種是Bootstrap classloader,由C++或者java實現。另外一種是其餘 classloader。都是用java語言編寫,繼承自 java.lang.ClassLoader 抽象類。網絡

jdk 8 classloader 繼承關係

  1. Application Classloader。負責加載用戶路徑下的類,若是沒有自定義類加載器,這個就是默認的類加載器。
  2. Extension Classloader。負責加載<JAVA_HOME>\lib\ext,或java.ext.dirs系統變量所 指定的路徑中全部的類庫。
  3. BootStrap Classloader。負責加載<JAVA_HOME>\lib,-Xbootclasspath參數指定的類。應用獲取不到這個 Classloader ,以null代替。

ClassLoader 應用案例

上面簡單介紹的是背景知識,下面是重頭戲。在瞭解了javac 編譯流程,類的生命週期,classloader 雙親委派以後,能用它來作什麼呢。數據結構

在瞭解「類的生命週期」以後,知道 ClassLoader 只有在 loading 階段能夠自定義字節碼,其餘階段都是由 JVM 實現的。下面我看看幾個應用場景,直觀的感覺一下。

Java SPI 中的應用

Java SPI (Service Provider Interface) 是動態加載服務的機制。能夠按照規則實現本身的SPI,使用 ServiceLoader 加載服務。

Java SPI 的組件:

  1. 服務接口: 一個接口或者抽象類定義服務功能。
  2. 服務提供方: 服務接口的實現,提供具體的服務。
  3. 配置文件:須要在 META-INF/services 目錄下放置一個服務接口名相同的文件,每一行是一個實現類的全類名。
  4. ServiceLoader:Java SPI 的主類,用來經過服務接口加載服務實現,有不少工具方法,可實現從新加載服務。

Java SPI Example

實現一個 SPI 而且使用 ServiceLoader 加載服務。

  1. 定義服務接口
public interface MessageServiceProvider {
	void sendMessage(String message);
}
複製代碼
  1. 定義服務接口 實現 email 和 推送消息連個實現。
public class EmailServiceProvider implements MessageServiceProvider {
	public void sendMessage(String message) {
		System.out.println("Sending Email with Message = "+message);
	}
}
public class PushNotificationServiceProvider implements MessageServiceProvider {
	public void sendMessage(String message) {
		System.out.println("Sending Push Notification with Message = "+message);
	}
}
複製代碼
  1. 編寫服務配置 在 META-INF/services 建立 util.spi.MessageServiceProvider 文件,內容是服務類全路徑
util.spi.EmailServiceProvider
util.spi.PushNotificationServiceProvider
複製代碼
  1. ServiceLoader 加載服務 最後,經過 ServiceLoader 加載服務並測試。
public class ServiceLoaderTest {
  public static void main(String[] args) {
    ServiceLoader<MessageServiceProvider> serviceLoader = ServiceLoader
        .load(MessageServiceProvider.class);
    for (MessageServiceProvider service : serviceLoader) {
      service.sendMessage("Hello");
    }
}    
複製代碼

輸出以下:

Sending Email with Message = Hello
Sending Push Notification with Message = Hello
複製代碼

下面是項目文件結構:

項目結構

Java SPI class loader 的思考

ServiceLoader 類在 rt.jar 包中,應該是由 Bootstrap Classloader 加載,而 EmailServiceProvider 是我定義的類,應該是由 Application Classloader 加載。先驗證一下這個想法。

ServiceLoader<MessageServiceProvider> serviceLoader = ServiceLoader.load(MessageServiceProvider.class);
System.out.println(ServiceLoader.class.getClassLoader());

for (MessageServiceProvider service : serviceLoader) {
System.out.println(service.getClass().getClassLoader());
}
複製代碼

結果以下:

// ServiceLoader 由 Bootstrap Classloader 加載,獲取不到classLoader
null 
// 由 Application Classloader 加載
jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
jdk.internal.loader.ClassLoaders$AppClassLoader@3fee733d
複製代碼

按照classloader的繼承關係,Bootstrap Classloader 是不能加載應用類的,那ServiceLoader是如何引用到 SPI 服務的呢?

java.util.ServiceLoader#load(java.lang.Class<S>)
看下load方法作了什麼。

  1. ①,③,是同一個 ClassLoader ,是main線程的 contextClassLoader,而main線程的 contextClassLoader 是jvm設置的。有了這個線程,能夠推測 ServiceLoader 是經過 contextClassLoader 加載服務的。
  2. ②是要加載的服務。

  1. 從調用棧能夠看到 ServiceLoader 的迭代器是經過懶加載的方式加載服務。
  2. ① 是 Application Classloader,從線程上下文中獲取的。
  3. ② 使用線程 contextClassLoader 加載的服務實現,繞開了雙親委派。

jdbc driver 也是SPI服務

mysql 驅動也由驅動接口,經過 SPI 的方式加載的。

DriverManager 在加載的時候會調用 loadInitialDrivers 方法加載驅動服務

// DriverManager.loadInitialDrivers()
private static void loadInitialDrivers() {
       AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {

            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();

            try{
                while(driversIterator.hasNext()) {
                    driversIterator.next();
                }
            }
        }
    }
}    
// com.mysql.cj.jdbc.Driver
// 把本身註冊到 DriverManager 中
static {
    try {
        java.sql.DriverManager.registerDriver(new Driver());
    } catch (SQLException E) {
        throw new RuntimeException("Can't register driver!");
    }
}
複製代碼

由於服務是懶加載的,因此會遍歷迭代器,在Mysql 驅動類中,會把本身註冊到 DriverManager 中,這樣就 DriverManager 中就管理了全部的驅動程序。

自定義文件名

有些時候可能須要防止正常的訪問,能夠經過自定義 ClassLoader ,在loading的時候進行處理

好比 lombok,使用 ShadowClassLoader 加載SCL.lombok文件 。

加密 class 文件

實現一個加密class文件,並使用自定義 ClassLoader 加載的 demo。

  1. 加密 class 文件

使用 xor 的方式加密,由於兩次 xor 等於原值,是一種比較簡單的方式,安全級別更高的話能夠經過JNI或者公私鑰的方式。

/**
* 解密/解密 class文件
*/
public static byte[] decodeClassBytes(byte[] bytes) {
    byte[] decodedBytes = new byte[bytes.length];
    for (int i = 0; i < bytes.length; i++) {
      decodedBytes[i] = (byte) (bytes[i] ^ 0xFF);
    }
    return decodedBytes;
}
複製代碼
  1. 編寫加密類 類的邏輯比較簡單,構造的時候打印一句話。編譯後的class會經過上一步的方法加密,重命名爲.class_文件用來區分。
public class MyClass {
  public MyClass(){
    System.out.println("My class");
  }
}
複製代碼

加密後的文件是不能經過正常方式解析的,能夠用javap命令驗證一下

D:\workspace\mygit\jdk-learn\jdk8\src\main\resources>javap -v  lang.classloader.encrypt.Myclass
錯誤: 讀取lang.classloader.encrypt.Myclass的常量池時出錯: unexpected tag at #1: 245
複製代碼
  1. 編寫自定義 ClassLoader 首先定義一個引導類,引導類由自定義 ClassLoader加載。以後引導類建立類時會使用 自定義 ClassLoader 加載。這個流程和 Tomcat 自定義classLoader 是同樣的。
public class MyCustomClassLoader extends ClassLoader {

  // 加密的 class
  private Collection<String> encryptClass = new HashSet<>();
  // 忽略的類,未加密的類
  private Collection<String> skipClass = new HashSet<>();

  public void init() {
    skipClass.add("lang.classloader.encrypt.EncryptApp");
    encryptClass.add("lang.classloader.encrypt.MyClass");
  }

  @Override
  public Class<?> loadClass(String name) throws ClassNotFoundException {
    // 由父類加載的類
    if (name.startsWith("java.")
        && !encryptClass.contains(name)
        && !skipClass.contains(name)) {
      return super.loadClass(name);
    } 
    // 未加密的類
    else if (skipClass.contains(name)) {
      try {
        String classPath = name.replace('.', '/') + ".class";
        //返回讀取指定資源的輸入流
        URL resource = getClass().getClassLoader().getResource(classPath);
        InputStream is = resource != null ? resource.openStream() : null;
        if (is == null) {
          return super.loadClass(name);
        }
        byte[] b = new byte[is.available()];
        is.read(b);

        //將一個byte數組轉換爲Class類的實例
        return defineClass(name, b, 0, b.length);
      } catch (IOException e) {
        throw new ClassNotFoundException(name, e);
      }
    }
    // 加密的類
    return findClass(name);
  }

  @Override
  protected Class<?> findClass(String name) throws ClassNotFoundException {
    // 加載類文件內容
    byte[] bytes = getClassFileBytesInDir(name);
    // 解密
    byte[] decodedBytes = decodeClassBytes(bytes);
    // 初始化類,由 jvm 實現
    return defineClass(name, decodedBytes, 0, bytes.length);
  }

  // 讀取加密class文件
  private static byte[] getClassFileBytesInDir(String className) throws ClassNotFoundException {
    try {
      return FileUtils.readFileToByteArray(
          new File(className.replace(".", "//") + ".class_"));
    } catch (IOException e) {
      throw new ClassNotFoundException(className, e);
    }
  }
}
複製代碼
  1. 測試程序 測試時,先建立自定義類加載器,而後用自定義類加載器去加載啓動類,啓動類會使用自定義類加載器去加載MyClass。

經過反射調用 EncryptApp 方法的說明很重要,能夠嘗試直接類型轉換看看拋出的異常。

public class EncryptApp {
  public void printClassLoader() {
    System.out.println("EncryptApp:" + this.getClass().getClassLoader());
    System.out.println("MyClass.class.getClassLoader() = " + MyClass.class.getClassLoader());
    new MyClass();
  }
}

  public static void main(String[] args)
      throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    MyCustomClassLoader myCustomClassLoader = new MyCustomClassLoader();
    myCustomClassLoader.init();

    Class<?> startupClass = myCustomClassLoader.loadClass("lang.classloader.encrypt.EncryptApp");
    
    // 重要:必須經過反射的方式獲取方法,
    // 由於當前線程的classloader,和加載 EncryptApp 的不同,
    // 因此不能類型轉換,必須用object
    Object encryptApp = startupClass.getConstructor().newInstance();
    String methodName = "printClassLoader";
    Method method = encryptApp.getClass().getMethod(methodName);
    method.invoke(encryptApp);
  }
複製代碼

結果以下:

// EncryptApp 是由 MyCustomClassLoader 加載
EncryptApp:lang.classloader.encrypt.MyCustomClassLoader@1a6c5a9e
// EncryptApp 啓動類加載 MyClass 也是使用 MyCustomClassLoader
MyClass.class.getClassLoader() = lang.classloader.encrypt.MyCustomClassLoader@1a6c5a9e
My class
複製代碼

總結

ClassLoader 是一個重要的工具,可是平時不多須要自定義一個 ClassLoader 。經過自定義 ClassLoader 加載字節碼仍是使人興奮的。

從類的生命週期理解 ClassLoader,更清楚它能作什麼。不少時候須要結合字節碼技術,更能發揮他的威力。不少框架也是這麼作的,好比 APM。

參考資料

  • 深刻理解Java虛擬機:JVM高級特性與最佳實踐(第3版)
  • 深刻理解 jvm 字節碼
相關文章
相關標籤/搜索