1. 程式人生 > >重寫類載入器,實現簡單的熱替換

重寫類載入器,實現簡單的熱替換

一、前言

關於類載入器,前面寫了三篇,這篇是第四篇。

實戰分析Tomcat的類載入器結構(使用Eclipse MAT驗證)

還是Tomcat,關於類載入器的趣味實驗

了不得,我可能發現了Jar 包衝突的祕密

 

本篇寫個簡單的例子,來說說類的熱替換。

 先說個原則,在同一個類載入器內,不能重複載入同一個類(因為 classloader 在 loadClass 一次後會快取在類載入器內部,此時如果再次載入,其實是直接從快取取,我意思的載入,是指真正去呼叫 defineClass 去載入。)。所以,要熱替換一個類,必須連其類載入器一起換掉。

 

 

二、步驟

1、原始碼

一共兩個工程,工程1,只有下面這一個類

測試類,TestSample.java,這個類的用處就是,我們不斷改變其 printClassLoader 的程式碼,並重新編譯後,放到指定位置:

/**
 * desc:
 *
 * @author : caokunliang
 * creat_date: 2019/6/15 0015
 * creat_time: 14:01
 **/
public class TestSample {

    public void printClassLoader(TestSample testSample) {
        System.out.println(testSample.getClass().getClassLoader());
    }
}

 

工程2,兩個類:

ReloadMainTest.java,主要是啟動一個定時任務,定時任務會每隔3s,用一個自定義的類載入器,去指定位置(為了簡單,直接路徑寫死了)載入 TestSample.class,並呼叫其方法進行列印,檢視是否熱替換成功:

import java.lang.reflect.Method;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * desc:
 *
 * @author : caokunliang
 * creat_date: 2019/6/14 0014
 * creat_time: 17:04
 **/
public class ReloadMainTest {
    public static void reload()throws Exception{

        String className = "TestSample";
        MyClassLoader classLoader = new MyClassLoader("/home/test/TestSample.class", className);
        Class<?> loadClass = classLoader.findClass(className);
        Object instance = loadClass.newInstance();
        Method method = instance.getClass().getMethod("printClassLoader", new Class[]{loadClass});
        method.invoke(instance,instance);


    }


    public static void main(String[] args) throws Exception {
          testReload();
    }

    public static void testReload(){
        //建立一個2s執行一次的定時任務
        Executors.newScheduledThreadPool(1).scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                try {
                    reload();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },0,3, TimeUnit.SECONDS);


    }
}

 

MyClassLoader.java,自定義的類載入器:
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.UnsupportedEncodingException;

/**
 * desc:
 *
 * @author : caokunliang
 * creat_date: 2019/6/13 0013
 * creat_time: 10:19
 **/
public class MyClassLoader extends ClassLoader {
    private String classPath;
    private String className;


    public MyClassLoader(String classPath, String className) {
        this.classPath = classPath;
        this.className = className;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] data = getData();
        return defineClass(className,data,0,data.length);
    }

    private byte[] getData(){
        String path = classPath;

        try {
            FileInputStream inputStream = new FileInputStream(path);
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            byte[] bytes = new byte[2048];
            int num = 0;
            while ((num = inputStream.read(bytes)) != -1){
                byteArrayOutputStream.write(bytes, 0,num);
            }

            return byteArrayOutputStream.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }
}

 

2、測試

我這邊的測試路徑為:/home/test, MyClassLoader.java已經編譯

[root@localhost test]# pwd
/home/test
[root@localhost test]# ll MyClassLoader.*
-rw-r--r--. 1 root root 1175 Jun 13 11:25 MyClassLoader.class
-rw-r--r--. 1 root root 1242 Jun 13 11:25 MyClassLoader.java

 

我的工程2 的程式碼放在另一個目錄下:

[root@localhost test-reload]# pwd
/home/test/test-reload
[root@localhost test-reload]# ll
total 20
-rw-r--r--. 1 root root 1464 Jun 15 17:55 MyClassLoader.class
-rw-r--r--. 1 root root 1458 Jun 15 17:55 MyClassLoader.java
-rw-r--r--. 1 root root  511 Jun 15 17:55 ReloadMainTest$1.class
-rw-r--r--. 1 root root 1531 Jun 15 17:55 ReloadMainTest.class
-rw-r--r--. 1 root root 1218 Jun 15 17:52 ReloadMainTest.java

 

執行 java ReloadMainTest,啟動測試類,就會每個3s,執行 TestSample 的方法:

 

此時,我們在另一個視窗中,去修改 TestSample.java,並重新編譯之:

 

此時,我們切回原視窗,可以發現輸出發生了變化:

 

3、測試進階

這裡要介紹一個工具,阿里開源的arthas。 (https://alibaba.github.io/arthas/en/install-detail.html)

這款工具,功能很強,下圖是其簡單介紹:

 

這裡,我打算使用其 類搜尋功能,通過搜尋  TestSample 類,來檢視該類是從哪個類載入器載入而來,使用方式極其簡單,直接java 啟動 arthas,然後選擇要attach的java 應用。

[root@localhost test]# java -jar arthas-boot.jar 
[INFO] arthas-boot version: 3.1.1
[INFO] Found existing java process, please choose one and hit RETURN.
* [1]: 10100 org.apache.catalina.startup.Bootstrap
  [2]: 25517 ReloadMainTest
2
[INFO] arthas home: /root/.arthas/lib/3.1.1/arthas
[INFO] Try to attach process 25517
[INFO] Attach process 25517 success.
[INFO] arthas-client connect 127.0.0.1 3658
  ,---.  ,------. ,--------.,--.  ,--.  ,---.   ,---.                           
 /  O  \ |  .--. ''--.  .--'|  '--'  | /  O  \ '   .-'                          
|  .-.  ||  '--'.'   |  |   |  .--.  ||  .-.  |`.  `-.                          
|  | |  ||  |\  \    |  |   |  |  |  ||  | |  |.-'    |                         
`--' `--'`--' '--'   `--'   `--'  `--'`--' `--'`-----'                          
                                                                                

wiki      https://alibaba.github.io/arthas                                      
tutorials https://alibaba.github.io/arthas/arthas-tutorials                     
version   3.1.1                                                                 
pid       25517                                                                 
time      2019-06-15 19:16:59

 

下面我們搜尋下TestSample類,(直接輸入:sc -df TestSample):

 

是不是看到類載入器了,但這只是我截了一部分的圖而已,這個命令會把 當前java程序中所有的匹配這個類的都搜出來。我們看看到底搜出來多少:

 

這裡顯示了,一共有9行,也就是說,在我們的定時器執行緒的不斷執行下,每隔3s就用一個新的類載入器去載入 TestSample,目前java 程序中,已經有9個 TestSample 類了。

多個同名類,(但不同類載入器),會不會有問題?按理說應該不會,因為假設另一個類B引用該類,那麼類B預設就會用它自己的類載入器來載入該類,按理說,是載入不到的,直接就報錯了。(存疑。。。)

 

說回來(實在是編不下去了。。),這裡我們的 ReloadMainTest,都是 把一個classloader 用完即棄,包括 該classloader 載入的類,以及用載入的類new出來的物件,都是在一個方法內,屬於區域性變數,跑完一次迴圈,就沒人持有他們的引用了。

但是,為什麼我們還看到有9個類存在呢? 這個主要還是因為,class 相關的資料都是存放在 永久代,永久代平時一般不進行垃圾回收,所以我們才能看到那些廢棄類的屍體。我們可以試試呼叫垃圾回收,通過jmap就可以觸發。

[root@localhost test]# jmap -dump:live,format=b,file=heap23.bin  25517
Dumping heap to /home/test/heap23.bin ...
Heap dump file created

 

此時再看類的數量,是不是變了:

 

 三、簡單總結

這篇簡單介紹瞭如何進行類的熱替換。這裡的熱替換,建立在這樣的基礎上:我們載入了新的class,然後new了物件,呼叫了物件的方法後,整個過程就結束了,沒涉及到和其他類的互動。正因為如此,新生成的物件沒有被任何地方引用,所以可以進行垃圾回收;物件被回收後,perm區的class物件也就可以進行回收了,於是,classloader也沒被任何地方引用,也可以進行回收,所以最後的那個測試才能出現上述的結果(即:jmap觸發full gc後,TestSample的數量變回1)。

客觀來說,暫時還沒發現在真實環境裡能發揮出什麼作用,但是作為學習案例,是夠了的。為什麼在真實環境沒用(比如 java web專案),在這類專案中,應用被打成一個war包(jar包的spring boot方式還沒研究內部的類載入器結構,不能亂說),應用的WEB-INF下的classes和lib目錄下的 jar 包,都是由同一個類載入器(也就是webappclassloader)載入。如果要替換的話,只能整個 webappclassloader 全部換掉才可能。能不能單獨換一個類呢,我感覺是不行的,假設 ControllerA 裡面引用了 AService,AServiceImpl實現AService,你說我現在想換掉 AServiceImpl,假設我們重新用自定義的類載入器 去某個位置載入 了新的 AServiceImpl ,那麼我們要怎麼才能讓 AService 引用到這個新的 實現類呢? 且不說這二者由不同的類載入器載入,其次,還得把之前的舊的實現的被別處引用的地方給換掉。。。想想還是很不好搞。。。

這裡預告一下,下一篇會是一個黑科技,尤其是對java web、java 後臺開發人員而言,主要是給後臺程式開個後門,執行我們的任意程式碼,在程式不重啟的情況下進行除錯、全域性引數檢視、方法執行等,給同事們演示了一下,效果還是很不錯的。