手把手教你實現熱更新功能,帶你瞭解 Arthas 熱更新背後的原理

文章來源:studyidea.cn/java-hotswa…html

1、前言

一天下午正在摸魚的時候,測試小姐姐走了過來求助,說是須要改動測試環境 mock 應用。可是這個應用一時半會又找不到源代碼存在何處。可是測試小姐姐的活仍是必定要幫,忽然想起了 Arthas 能夠熱更新應用代碼,按照網上的步驟,反編譯應用代碼,加上須要改動的邏輯,最後熱更新成功。對此,測試小姐姐很滿意,並表示下次會少提 Bug。java

嘿嘿,之前一直對熱更新背後原理很好奇,藉着這個機會,研究一下熱更新的原理。git

2、Arthas 熱更新

咱們先來看下 Arthas 是如何熱更新的。github

詳情參考:阿里巴巴Arthas實踐--jad/mc/redefine線上熱更新一條龍app

假設咱們如今有一個 HelloService 類,邏輯以下,如今咱們使用 Arthas 熱更新代碼,讓其輸出 hello arthas框架

public class HelloService {

    public static void main(String[] args) throws InterruptedException {

        while (true){
            TimeUnit.SECONDS.sleep(1);
            hello();
        }
    }

    public static void hello(){
        System.out.println("hello world");
    }

}
複製代碼

2.一、jad 反編譯代碼

首先運行 jad 命令反編譯 class 文件獲取源代碼,運行命令以下:。dom

jad --source-only com.andyxh.HelloService > /tmp/HelloService.java
複製代碼

2.二、修改反編譯以後的代碼

拿到源代碼以後,使用 VIM 等文本編輯工具編輯源代碼,加入須要改動的邏輯。jvm

2.三、查找 ClassLoader

而後使用 sc 命令查找加載修改類的 ClassLoader,運行命令以下:socket

$ sc -d  com.andyxh.HelloService | grep classLoaderHash
 classLoaderHash   4f8e5cde
複製代碼

這裏運行以後將會獲得 ClassLoader 哈希值。maven

2.四、 mc 內存編譯源代碼

使用 mc 命令編譯上一步修改保存的源代碼,生成最終 class 文件。

$ mc -c 4f8e5cde  /tmp/HelloService.java  -d /tmp
Memory compiler output:
/tmp/com/andyxh/HelloService.class
Affect(row-cnt:1) cost in 463 ms.
複製代碼

2.五、redefine 熱更新代碼

運行 redefine 命令:

$ redefine /tmp/com/andyxh/HelloService.class
redefine success, size: 1
複製代碼

熱更新成功以後,程序輸出結果以下:

image.png

通常狀況下,咱們本地將會有源代碼,上面的步驟咱們能夠進一步省略,咱們能夠先在本身 IDE 上改動代碼,編譯生成 class 文件。這樣咱們只須要運行 redefine 命令便可。也就是說實際上起到做用只是 redefine

3、 Instrumentation 與 attach 機制

Arthas 熱更新功能看起來很神奇,實際上離不開 JDK 一些 API,分別爲 instrument API 與 attach API。

3.1 Instrumentation

Java Instrumentation 是 JDK5 以後提供接口。使用這組接口,咱們能夠獲取到正在運行 JVM 相關信息,使用這些信息咱們構建相關監控程序檢測 JVM。另外, 最重要咱們能夠替換修改類的,這樣就實現了熱更新。

Instrumentation 存在兩種使用方式,一種爲 pre-main 方式,這種方式須要在虛擬機參數指定 Instrumentation 程序,而後程序啓動以前將會完成修改或替換類。使用方式以下:

java -javaagent:jar Instrumentation_jar -jar xxx.jar
複製代碼

有沒有以爲這種啓動方式很熟悉,仔細觀察一下 IDEA 運行輸出窗口。

image.png

另外不少應用監控工具,如:zipkin、pinpoint、skywalking。

這種方式只能在應用啓動以前生效,存在必定的侷限性。

JDK6 針對這種狀況做出了改進,增長 agent-main 方式。咱們能夠在應用啓動以後,再運行 Instrumentation 程序。啓動以後,只有鏈接上相應的應用,咱們才能作出相應改動,這裏咱們就須要使用 Java 提供 attach API。

3.2 Attach API

Attach API 位於 tools.jar 包,能夠用來鏈接目標 JVM。Attach API 很是簡單,內部只有兩個主要的類,VirtualMachineVirtualMachineDescriptor

VirtualMachine 表明一個 JVM 實例, 使用它提供 attach 方法,咱們就能夠鏈接上目標 JVM。

VirtualMachine vm = VirtualMachine.attach(pid);
複製代碼

VirtualMachineDescriptor 則是一個描述虛擬機的容器類,經過該實例咱們能夠獲取到 JVM PID(進程 ID),該實例主要經過 VirtualMachine#list 方法獲取。

for (VirtualMachineDescriptor descriptor : VirtualMachine.list()){

            System.out.println(descriptor.id());
        }
複製代碼

介紹完熱更新涉及的相關原理,接下去使用上面 API 實現熱更新功能。

4、實現熱更新功能

這裏咱們使用 Instrumentation agent-main 方式。

4.一、實現 agent-main

首先須要編寫一個類,包含如下兩個方法:

public static void agentmain (String agentArgs, Instrumentation inst);          [1]
public static void agentmain (String agentArgs);            [2]
複製代碼

上面的方法只須要實現一個便可。若兩個都實現, [1] 優先級大於 [2],將會被優先執行。

接着讀取外部傳入 class 文件,調用 Instrumentation#redefineClasses,這個方法將會使用新 class 替換當前正在運行的 class,這樣咱們就完成了類的修改。

public class AgentMain {
    /** * * @param agentArgs 外部傳入的參數,相似於 main 函數 args * @param inst */
    public static void agentmain(String agentArgs, Instrumentation inst) {
        // 從 agentArgs 獲取外部參數
        System.out.println("開始熱更新代碼");
        // 這裏將會傳入 class 文件路徑
        String path = agentArgs;
        try {
            // 讀取 class 文件字節碼
            RandomAccessFile f = new RandomAccessFile(path, "r");
            final byte[] bytes = new byte[(int) f.length()];
            f.readFully(bytes);
            // 使用 asm 框架獲取類名
            final String clazzName = readClassName(bytes);

            // inst.getAllLoadedClasses 方法將會獲取全部已加載的 class
            for (Class clazz : inst.getAllLoadedClasses()) {
                // 匹配須要替換 class
                if (clazz.getName().equals(clazzName)) {
                    ClassDefinition definition = new ClassDefinition(clazz, bytes);
                    // 使用指定的 class 替換當前系統正在使用 class
                    inst.redefineClasses(definition);
                }
            }

        } catch (UnmodifiableClassException | IOException | ClassNotFoundException e) {
            System.out.println("熱更新數據失敗");
        }


    }

    /** * 使用 asm 讀取類名 * * @param bytes * @return */
    private static String readClassName(final byte[] bytes) {
        return new ClassReader(bytes).getClassName().replace("/", ".");
    }
}
複製代碼

完成代碼以後,咱們還須要往 jar 包 manifest 寫入如下屬性。

## 指定 agent-main 全名
Agent-Class: com.andyxh.AgentMain
## 設置權限,默認爲 false,沒有權限替換 class
Can-Redefine-Classes: true
複製代碼

咱們使用 maven-assembly-plugin,將上面的屬性寫入文件中。

<plugin>
    <artifactId>maven-assembly-plugin</artifactId>
    <version>3.1.0</version>
    <configuration>
        <!--指定最後產生 jar 名字-->
        <finalName>hotswap-jdk</finalName>
        <appendAssemblyId>false</appendAssemblyId>
        <descriptorRefs>
            <!--將工程依賴 jar 一塊打包-->
            <descriptorRef>jar-with-dependencies</descriptorRef>
        </descriptorRefs>
        <archive>
            <manifestEntries>
                <!--指定 class 名字-->
                <Agent-Class>
                    com.andyxh.AgentMain
                </Agent-Class>
                <Can-Redefine-Classes>
                    true
                </Can-Redefine-Classes>
            </manifestEntries>
            <manifest>
                <!--指定 mian 類名字,下面將會使用到-->
                <mainClass>com.andyxh.JvmAttachMain</mainClass>
            </manifest>
        </archive>
    </configuration>
    <executions>
        <execution>
            <id>make-assembly</id> <!-- this is used for inheritance merges -->
            <phase>package</phase> <!-- bind to the packaging phase -->
            <goals>
                <goal>single</goal>
            </goals>
        </execution>
    </executions>
</plugin>
複製代碼

到這裏咱們就完成熱更新主要代碼,接着使用 Attach API,鏈接目標虛擬機,觸發熱更新的代碼。

public class JvmAttachMain {
    public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        // 輸入參數,第一個參數爲須要 Attach jvm pid 第二參數爲 class 路徑
        if(args==null||args.length<2){
            System.out.println("請輸入必要參數,第一個參數爲 pid,第二參數爲 class 絕對路徑");
            return;
        }
        String pid=args[0];
        String classPath=args[1];
        System.out.println("當前須要熱更新 jvm pid 爲 "+pid);
        System.out.println("更換 class 絕對路徑爲 "+classPath);
        // 獲取當前 jar 路徑
        URL jarUrl=JvmAttachMain.class.getProtectionDomain().getCodeSource().getLocation();
        String jarPath=jarUrl.getPath();

        System.out.println("當前熱更新工具 jar 路徑爲 "+jarPath);
        VirtualMachine vm = VirtualMachine.attach(pid);//7997是待綁定的jvm進程的pid號
        // 運行最終 AgentMain 中方法
        vm.loadAgent(jarPath, classPath);
    }
}
複製代碼

在這個啓動類,咱們最終調用 VirtualMachine#loadAgent,JVM 將會使用上面 AgentMain 方法使用傳入 class 文件替換正在運行 class。

4.二、運行

這裏咱們繼續開頭使用的例子,不過這裏加入一個方法獲取 JVM 運行進程 ID。

public class HelloService {

    public static void main(String[] args) throws InterruptedException {
        System.out.println(getPid());
        while (true){
            TimeUnit.SECONDS.sleep(1);
            hello();
        }
    }

    public static void hello(){
        System.out.println("hello world");
    }

    /** * 獲取當前運行 JVM PID * @return */
    private static String getPid() {
        // get name representing the running Java virtual machine.
        String name = ManagementFactory.getRuntimeMXBean().getName();
        System.out.println(name);
        // get pid
        return name.split("@")[0];
    }

}
複製代碼

首先運行 HelloService,獲取當前 PID,接着複製 HelloService 代碼到另外一個工程,修改 hello 方法輸出 hello agent,從新編譯生成新的 class 文件。

最後在命令行運行生成的 jar 包。

image.png

HelloService 輸出效果以下所示:

image.png

源代碼地址:https://github.com/9526xu/hotswap-example

4.三、調試技巧

普通的應用咱們能夠在 IDE 直接使用 Debug 模式調試程序,可是上面的程序沒法直接使用 Debug。剛開始運行的程序碰到不少問題,無奈之下,只能選擇最原始的辦法,打印錯誤日誌。後來查看 arthas 的文檔,發現上面一篇文章介紹使用 IDEA Remote Debug 模式調試程序。

首先咱們須要在 HelloService JVM 參數加入如下參數:

-Xrunjdwp:transport=dt_socket,server=y,address=8001  
複製代碼

此時程序將會被阻塞,直到遠程調試程序鏈接上 8001 端口,輸出以下:

image.png

而後在 Agent-main 這個工程增長一個 remote 調試。

image.png

image.png

圖中參數以下:

-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8001
複製代碼

Agent-main 工程打上斷點,運行遠程調試, HelloService 程序將會被啓動。

最後在命令行窗口運行 Agent-main 程序,遠程調試將會暫停到相應斷點處,接下來調試就跟普通 Debug 模式同樣,再也不敘述。

4.四、相關問題

因爲 Attach API 位於 tools.jar 中,而在 JDK8 以前 tools.jar 與咱們經常使用JDK jar 包並不在同一個位置,因此編譯與運行過程可能找不到該 jar 包,從而致使報錯。

若是 maven 編譯與運行都使用 JDK9 以後,不用擔憂下面問題。

maven 編譯問題

maven 編譯過程可能發生以下錯誤。

image.png

解決辦法爲在 pom 下加入 tools.jar 。

<dependency>
            <groupId>jdk.tools</groupId>
            <artifactId>jdk.tools</artifactId>
            <scope>system</scope>
            <version>1.6</version>
            <systemPath>${java.home}/../lib/tools.jar</systemPath>
        </dependency>
複製代碼

或者使用下面依賴。

<dependency>
            <groupId>com.github.olivergondza</groupId>
            <artifactId>maven-jdk-tools-wrapper</artifactId>
            <version>0.1</version>
            <scope>provided</scope>
            <optional>true</optional>
        </dependency>
複製代碼

程序運行過程 tools.jar 找不到

運行程序時拋出 java.lang.NoClassDefFoundError,主要緣由仍是系統未找到 tools.jar 致使。

image.png

在運行參數加入 -Xbootclasspath/a:${java_home}/lib/tools.jar,完整運行命令以下:

image.png

4.五、熱更新存在一些限制

並非全部改動熱更新都將會成功,當前使用 Instrumentation#redefineClasses 仍是存在一些限制。咱們僅只能修改方法內部邏輯,屬性值等,不能添加,刪除方法或字段,也不能更改方法的簽名或繼承關係。

5、彩蛋

寫完熱更新代碼,收到一封系統郵件提示 xxx bug 待修復。恩,說好的少提 Bug 呢 o(╥﹏╥)o。

6、幫助

1.深刻探索 Java 熱部署
2.Instrumentation 新功能

歡迎關注個人公衆號:程序通事,得到平常乾貨推送。若是您對個人專題內容感興趣,也能夠關注個人博客:studyidea.cn

其餘平臺.png
相關文章
相關標籤/搜索