Classloader, 你究竟能干啥

我們知道java語言是一次編譯炫贤,多平臺運(yùn)行疆导。這得益于Java在設(shè)計(jì)的時(shí)候饮戳,把編譯和運(yùn)行是獨(dú)立的兩個(gè)流程。編譯負(fù)責(zé)把源代碼編譯成 JVM 可識別的字節(jié)碼另假,運(yùn)行時(shí)加載字節(jié)碼,并解釋成機(jī)器指令運(yùn)行怕犁。

因?yàn)槭窃创a編譯成字節(jié)碼,所以 JVM 平臺除了java語言外,還有g(shù)roovy奏甫,scala等戈轿。
因?yàn)槭羌虞d字節(jié)碼運(yùn)行,所以有apm阵子,自定義classloader思杯,動(dòng)態(tài)語言等技術(shù)。構(gòu)成了豐富的Java 世界挠进。

javac 編譯流程

javac 編譯流程
  1. parse:讀壬.java源文件,做詞法分析(LEXER)和語法分析(PARSER)
  2. enter:生成符號表
  3. process:處理注解
  4. attr:檢查語義合法性领突、常量折疊
  5. flow:數(shù)據(jù)流分析
  6. desugar:去除語法糖
  7. generate:生成字節(jié)碼

編譯期主要的目的是把 java 源代碼編譯為 符合 jvm 規(guī)范的的字節(jié)碼暖璧。在運(yùn)行期,由 jvm 加載字節(jié)碼并執(zhí)行君旦,程序就運(yùn)行起來了澎办。

其實(shí)java語言和 jvm 是沒有綁定關(guān)系。只要符合jvm規(guī)范的字節(jié)碼都可以執(zhí)行金砍,但是字節(jié)碼不一定由Java語言編譯而來局蚀。正因如此,jvm 平臺涌現(xiàn)出了groovy恕稠,scala琅绅,kotlin等眾多語言。

如果你感興趣鹅巍,也可以把把你喜歡的語言搬到 jvm 上運(yùn)行千扶。

類的生命周期

類的聲明周期
  1. loading:加載。是第一個(gè)階段昆著,主要是加載字節(jié)碼县貌,靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)數(shù)據(jù)結(jié)構(gòu),生成class對象凑懂。這里沒有限制字節(jié)碼的來源煤痕,可以是文件、zip接谨,網(wǎng)絡(luò)摆碉、jsp,甚至是加密文件脓豪。這個(gè)階段可以使用自定義 classloader 實(shí)現(xiàn)自定義行為巷帝,這就給字節(jié)碼帶來了很多可能的玩法。
  2. verification:驗(yàn)證扫夜。確保字節(jié)碼符合 jvm 規(guī)范楞泼。
  3. preparation:準(zhǔn)備驰徊。是正式為類中定義的變量設(shè)置初始值。
  4. resolution:解析堕阔。將常量池內(nèi)的符號引用替換為直接引用的過程棍厂。
  5. initialization: 初始化。這里將程序的主導(dǎo)權(quán)交給了應(yīng)用程序超陆,會執(zhí)行·<clinit>()和構(gòu)造函數(shù)牺弹。
  6. using:使用。使用初始化后的類时呀,這里就到了應(yīng)用邏輯的范疇张漂。
  7. unloading:卸載。需要滿足該類所有實(shí)例已經(jīng)被GC谨娜,加載該類的ClassLoader已經(jīng)被GC航攒,該類的java.lang.Class對象已經(jīng)沒有被引用。在tomcat jsp 熱加載的場景會用到瞧预,每個(gè)jsp都是單獨(dú)的 classloader屎债,當(dāng)jsp由變動(dòng)時(shí),會卸載舊的classloader垢油,創(chuàng)建新的classloader加載jsp盆驹,這樣就實(shí)現(xiàn)了熱加載。

在 initialization 階段之前滩愁,只有 loading 段可以通過自定義 Classloader 添加自定義邏輯躯喇,其他階段都是由 JVM 完成的。這就是本文想要表達(dá)的重點(diǎn)硝枉,Classloader 究竟能做什么呢廉丽。

雙親委派

在了解 Classloader 究竟能做什么之前,必須要先了解一下雙親委派模型妻味。眾所周知正压,java 是單繼承的,classloader 也繼承了這種設(shè)計(jì)思想责球。

這里針對 JDK 8 版本介紹焦履,JDK9 之后引入了模塊功能,classloader 繼承關(guān)系有所變化雏逾。

雙親委派

站在 JVM 的角度嘉裤,只有兩種加載器,一種是Bootstrap classloader栖博,由C++或者java實(shí)現(xiàn)屑宠。另一種是其他 classloader。都是用java語言編寫仇让,繼承自 java.lang.ClassLoader 抽象類典奉。

jdk 8 classloader 繼承關(guān)系
  1. Application Classloader躺翻。負(fù)責(zé)加載用戶路徑下的類,如果沒有自定義類加載器秋柄,這個(gè)就是默認(rèn)的類加載器获枝。
  2. Extension Classloader。負(fù)責(zé)加載<JAVA_HOME>\lib\ext骇笔,或java.ext.dirs系統(tǒng)變量所
    指定的路徑中所有的類庫。
  3. BootStrap Classloader嚣崭。負(fù)責(zé)加載<JAVA_HOME>\lib笨触,-Xbootclasspath參數(shù)指定的類。應(yīng)用獲取不到這個(gè) Classloader 雹舀,以null代替芦劣。

ClassLoader 應(yīng)用案例

上面簡單介紹的是背景知識,下面是重頭戲说榆。在了解了javac 編譯流程虚吟,類的生命周期,classloader 雙親委派之后签财,能用它來做什么呢串慰。

在了解“類的生命周期”之后,知道 ClassLoader 只有在 loading 階段課可以可以自定義唱蒸,其他階段都是由 JVM 實(shí)現(xiàn)的邦鲫。下面我看看幾個(gè)應(yīng)用場景,直觀的感受一下神汹。

Java SPI 中的應(yīng)用

Java SPI (Service Provider Interface) 是動(dòng)態(tài)加載服務(wù)的機(jī)制庆捺。可以按照規(guī)則實(shí)現(xiàn)自己的SPI屁魏,使用 ServiceLoader 加載服務(wù)滔以。

Java SPI 的組件:

  1. 服務(wù)接口: 一個(gè)接口或者抽象類定義服務(wù)功能。
  2. 服務(wù)提供方: 服務(wù)接口的實(shí)現(xiàn)氓拼,提供具體的服務(wù)你画。
  3. 配置文件:需要在 META-INF/services 目錄下放置一個(gè)服務(wù)接口名相同的文件,每一行是一個(gè)實(shí)現(xiàn)類的全類名披诗。
  4. ServiceLoader:Java SPI 的主類撬即,用來通過服務(wù)接口加載服務(wù)實(shí)現(xiàn),有很多工具方法呈队,可實(shí)現(xiàn)重新加載服務(wù)剥槐。

Java SPI Example

實(shí)現(xiàn)一個(gè) SPI 并且使用 ServiceLoader 加載服務(wù)。

  1. 定義服務(wù)接口
public interface MessageServiceProvider {
    void sendMessage(String message);
}
  1. 定義服務(wù)接口
    實(shí)現(xiàn) email 和 推送消息連個(gè)實(shí)現(xiàn)宪摧。
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. 編寫服務(wù)配置
    在 META-INF/services 創(chuàng)建 util.spi.MessageServiceProvider 文件粒竖,內(nèi)容是服務(wù)類全路徑
util.spi.EmailServiceProvider
util.spi.PushNotificationServiceProvider
  1. ServiceLoader 加載服務(wù)
    最后颅崩,通過 ServiceLoader 加載服務(wù)并測試。
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

下面是項(xiàng)目文件結(jié)構(gòu):

項(xiàng)目結(jié)構(gòu)

Java SPI class loader 的思考

ServiceLoader 類在 rt.jar 包中蕊苗,應(yīng)該是由 Bootstrap Classloader 加載沿后,而 EmailServiceProvider 是我定義的類,應(yīng)該是由 Application Classloader 加載朽砰。先驗(yàn)證一下這個(gè)想法尖滚。

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

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

結(jié)果如下:

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

按照classloader的繼承關(guān)系瞧柔,Bootstrap Classloader 是不能加載應(yīng)用類的漆弄,那ServiceLoader是如何引用到 SPI 服務(wù)的呢?

java.util.ServiceLoader#load(java.lang.Class<S>)

看下load方法做了什么造锅。

  1. ①撼唾,③,是同一個(gè) ClassLoader 哥蔚,是main線程的 contextClassLoader倒谷,而main線程的 contextClassLoader 是jvm設(shè)置的。有了這個(gè)線程糙箍,可以推測 ServiceLoader 是通過 contextClassLoader 加載服務(wù)的渤愁。
  2. ②是要加載的服務(wù)。
image
  1. 從調(diào)用棻睹遥可以看到 ServiceLoader 的迭代器是通過懶加載的方式加載服務(wù)猴伶。
  2. ① 是 Application Classloader,從線程上下文中獲取的塌西。
  3. ② 使用線程 contextClassLoader 加載的服務(wù)實(shí)現(xiàn)他挎,繞開了雙親委派。

jdbc driver 也是SPI服務(wù)

mysql 驅(qū)動(dòng)包中也由驅(qū)動(dòng)服務(wù)接口的實(shí)現(xiàn)配置捡需。


image

DriverManager 在加載的時(shí)候會調(diào)用 loadInitialDrivers 方法加載驅(qū)動(dòng)服務(wù)

// 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!");
    }
}

因?yàn)榉?wù)是懶加載的办桨,所以會遍歷迭代器,在Mysql 驅(qū)動(dòng)類中站辉,會把自己注冊到 DriverManager 中呢撞,這樣就 DriverManager 中就管理了所有的驅(qū)動(dòng)程序。

自定義文件名

有些時(shí)候可能需要防止正常的訪問饰剥,可以通過自定義 ClassLoader 殊霞,在loading的時(shí)候進(jìn)行處理

比如 lombok,使用 ShadowClassLoader 加載SCL.lombok文件 汰蓉。


image

加密 class 文件

實(shí)現(xiàn)一個(gè)加密class文件绷蹲,并使用自定義 ClassLoader 加載的 demo。

  1. 加密 class 文件

使用 xor 的方式加密,因?yàn)閮纱?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. 編寫加密類
    類的邏輯比較簡單拦英,構(gòu)造的時(shí)候打印一句話蜒什。編譯后的class會通過上一步的方法加密,重命名為.class_文件用來區(qū)分疤估。
public class MyClass {
  public MyClass(){
    System.out.println("My class");
  }
}

加密后的文件是不能通過正常方式解析的灾常,可以用javap命令驗(yàn)證一下

D:\workspace\mygit\jdk-learn\jdk8\src\main\resources>javap -v  lang.classloader.encrypt.Myclass
錯(cuò)誤: 讀取lang.classloader.encrypt.Myclass的常量池時(shí)出錯(cuò): unexpected tag at #1: 245
  1. 編寫自定義 ClassLoader
    首先定義一個(gè)引導(dǎo)類,引導(dǎo)類由自定義 ClassLoader加載做裙。之后引導(dǎo)類創(chuàng)建類時(shí)會使用 自定義 ClassLoader 加載岗憋。這個(gè)流程和 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);

        //將一個(gè)byte數(shù)組轉(zhuǎn)換為Class類的實(shí)例
        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 {
    // 加載類文件內(nèi)容
    byte[] bytes = getClassFileBytesInDir(name);
    // 解密
    byte[] decodedBytes = decodeClassBytes(bytes);
    // 初始化類,由 jvm 實(shí)現(xiàn)
    return defineClass(name, decodedBytes, 0, bytes.length);
  }

  // 讀取加密class文件
  private static byte[] getClassFileBytesInDir(String className) throws ClassNotFoundException {
    try {
      return FileUtils.readFileToByteArray(
          new File(className.replace(".", "http://") + ".class_"));
    } catch (IOException e) {
      throw new ClassNotFoundException(className, e);
    }
  }
}
  1. 測試程序
    測試時(shí)关串,先創(chuàng)建自定義類加載器拧廊,然后用自定義類加載器去加載啟動(dòng)類,啟動(dòng)類會使用自定義類加載器去加載MyClass晋修。

通過反射調(diào)用 EncryptApp 方法的說明很重要吧碾,可以嘗試直接類型轉(zhuǎn)換看看拋出的異常。

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");
    
    // 重要:必須通過反射的方式獲取方法墓卦,
    // 因?yàn)楫?dāng)前線程的classloader倦春,和加載 EncryptApp 的不一樣,
    // 所以不能類型轉(zhuǎn)換落剪,必須用object
    Object encryptApp = startupClass.getConstructor().newInstance();
    String methodName = "printClassLoader";
    Method method = encryptApp.getClass().getMethod(methodName);
    method.invoke(encryptApp);
  }

結(jié)果如下:

// EncryptApp 是有 MyCustomClassLoader 加載
EncryptApp:lang.classloader.encrypt.MyCustomClassLoader@1a6c5a9e
// EncryptApp 啟動(dòng)類加載 MyClass 也是使用 MyCustomClassLoader
MyClass.class.getClassLoader() = lang.classloader.encrypt.MyCustomClassLoader@1a6c5a9e
My class
image

總結(jié)

ClassLoader 是一個(gè)重要的工具睁本,但是平時(shí)很少需要自定義一個(gè) ClassLoader 。通過自定義 ClassLoader 加載字節(jié)碼還是令人興奮的忠怖。

從類的生命周期理解 ClassLoader呢堰,更清楚它能做什么。很多時(shí)候需要結(jié)合字節(jié)碼技術(shù)凡泣,更能發(fā)揮他的威力枉疼。很多框架也是這么做的,比如 APM鞋拟。

參考資料

  • 深入理解Java虛擬機(jī):JVM高級特性與最佳實(shí)踐(第3版)
  • 深入理解 jvm 字節(jié)碼
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末骂维,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子贺纲,更是在濱河造成了極大的恐慌航闺,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件哮笆,死亡現(xiàn)場離奇詭異来颤,居然都是意外死亡汰扭,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門福铅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來萝毛,“玉大人,你說我怎么就攤上這事滑黔“拾” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵略荡,是天一觀的道長庵佣。 經(jīng)常有香客問我,道長汛兜,這世上最難降的妖魔是什么巴粪? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮粥谬,結(jié)果婚禮上肛根,老公的妹妹穿的比我還像新娘。我一直安慰自己漏策,他們只是感情好派哲,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著掺喻,像睡著了一般芭届。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上感耙,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天褂乍,我揣著相機(jī)與錄音,去河邊找鬼抑月。 笑死树叽,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的谦絮。 我是一名探鬼主播题诵,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼层皱!你這毒婦竟也來了性锭?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤叫胖,失蹤者是張志新(化名)和其女友劉穎草冈,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡怎棱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年哩俭,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拳恋。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡凡资,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出谬运,到底是詐尸還是另有隱情隙赁,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布梆暖,位于F島的核電站伞访,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏轰驳。R本人自食惡果不足惜厚掷,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望级解。 院中可真熱鬧蝗肪,春花似錦、人聲如沸蠕趁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽俺陋。三九已至,卻和暖如春昙篙,著一層夾襖步出監(jiān)牢的瞬間腊状,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工苔可, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留缴挖,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓焚辅,卻偏偏與公主長得像映屋,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子同蜻,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345