1. 程式人生 > >探秘 Java 熱部署二(Java agent premain)

探秘 Java 熱部署二(Java agent premain)

業務 方法 instr 自己 就是 還要 是我 java 代理 命令

技術分享圖片

# 前言

在前文 探秘 Java 熱部署 中,我們通過在死循環中重復加載 ClassLoader 和 Class 文件實現了熱部署的功能,但我們也指出了缺點-----不夠靈活。需要手動修改文件等操作。

如果有那麽一種功能,當你需要重新加載類並修改類的時候,有那麽一個轉換器自動幫你修改已有的 Class 文件變成你設定的 Class 文件,那麽就不需要手動修改編譯了。

也許你第一想到的就是在自定義類加載器中做文章,比如在 loadClass 中,得到字節碼之後,通過 ASM 或者 javassist 修改字節碼,然後再調用 defineClass 方法。

確實可行,但是這種侵入性太大。如果JVM 在底層提供一種類似 “類轉換器” 的東西,是不是侵入性就不大了呢?

實際上,JVM 確實給我們提供了一個工具,那就是今天的主角------java agent。

1. 什麽是 Java agent

在 JDK 1.5 中,Java 引入了 java.lang.Instrument 包,該包提供了一些工具幫助開發人員在 Java 程序運行時,動態修改系統中的 Class 類型。其中,使用該軟件包的一個關鍵組件就是 Java agent。從名字上看,似乎是個 Java 代理之類的,而實際上,他的功能更像是一個Class 類型的轉換器,他可以在運行時接受重新外部請求,對Class 類型進行修改。

如果在命令行執行 java 命令,會出現一些命令幫助,其中就有 java agent的選項:

技術分享圖片

2. Java agent 詳細介紹

參數 javaagent 可以用於指定一個 jar 包,並且對該 java 包有2個要求:

  1. 這個 jar 包的MANIFEST.MF 文件必須指定 Premain-Class 項。
  2. Premain-Class 指定的那個類必須實現 premain()方法。

重點就在 premain 方法,也就是我們今天的標題。從字面上理解,就是運行在 main 函數之前的的類。當Java 虛擬機啟動時,在執行 main 函數之前,JVM 會先運行 -javaagent 所指定 jar 包內 Premain-Class 這個類的 premain 方法,其中,該方法可以簽名如下:

1.public static void premain(String agentArgs, Instrumentation inst)
2.public static void premain(String agentArgs)

JVM 會優先加載 1 簽名的方法,加載成功忽略 2,如果1 沒有,加載 2 方法。這個邏輯在sun.instrument.InstrumentationImpl 類中:

技術分享圖片

參數 agentArgs 時通過命令行傳給 Java Agent 的參數, inst 是 Java Class 字節碼轉換的工具,Instrumentation 常用方法如下:

  1. void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
    增加一個Class 文件的轉換器,轉換器用於改變 Class 二進制流的數據,參數 canRetransform 設置是否允許重新轉換。

  2. void redefineClasses(ClassDefinition... definitions) hrows ClassNotFoundException, UnmodifiableClassException;
    在類加載之前,重新定義 Class 文件,ClassDefinition 表示對一個類新的定義,如果在類加載之後,需要使用 retransformClasses 方法重新定義。

  3. boolean removeTransformer(ClassFileTransformer transformer);
    刪除一個類轉換器

  4. void retransformClasses(Class<?>... classes) throws UnmodifiableClassException
    在類加載之後,重新定義 Class。這個很重要,該方法是1.6 之後加入的,事實上,該方法是 update 了一個類。

3. 如何使用?

使用 javaagent 需要幾個步驟:

  1. 定義一個 MANIFEST.MF 文件,必須包含 Premain-Class 選項,通常也會加入Can-Redefine-Classes 和 Can-Retransform-Classes 選項。
  2. 創建一個Premain-Class 指定的類,類中包含 premain 方法,方法邏輯由用戶自己確定。
  3. 將 premain 的類和 MANIFEST.MF 文件打成 jar 包。
  4. 使用參數 -javaagent:/jar包路徑=[agentArgs 參數] 啟動要代理的方法。

在執行以上步驟後,JVM 會先執行 premain 方法,大部分類加載都會通過該方法,註意:是大部分,不是所有。當然,遺漏的主要是系統類,因為很多系統類先於 agent 執行,而用戶類的加載肯定是會被攔截的。

也就是說,這個方法是在 main 方法啟動前攔截大部分類的加載活動,註意:是類加載之前。也就是說,我們可以在這個縫隙中做很多文章,比如修改字節碼。

讓我們來試試:

1 首先定義一個 MANIFEST.MF 文件:

Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: cn.think.in.java.clazz.loader.asm.agent.PreMainTraceAgent
  1. 創建一個Premain-Class 指定的類,類中包含 premain 方法:
    ````java
    public class PreMainTraceAgent {

public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("agentArgs : " + agentArgs);
inst.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException {
System.out.println("premain load Class :" + className);
return classfileBuffer;
}
}, true);
}
}

````

  1. 將 premain 的類和 MANIFEST.MF 文件打成 jar 包 .
    使用 IDEA 的 build ,當然你也可以使用 maven。具體請 google。

  2. 使用參數 -javaagent:/jar包路徑=[agentArgs 參數] 啟動要代理的方法。
    我們當然需要一個測試類:


class AccountMain {


  public static void main(String[] args)
      throws ClassNotFoundException, InterruptedException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
    while (true) {
     Account account = new Account();
     account.operation();
    }
  }

}

class Account {
  public void operation() {
    System.out.println("operation....");
  }
}

VM 參數 -javaagent:/jar包路徑=[agentArgs 參數] 。

運行結果:
技術分享圖片

可以看到,我們的 premain 方法確實在 main 方法之前被調用了,並且是在類加載的時候被調用的,而我們重寫的 transform 方法其中的 classfileBuffer 參數就是即將被載入虛擬機的字節碼,因此,我們可以使用各種字節碼庫進行修改。具體修改,這裏就暫時不表,以後有機會好好寫寫。

4. 和熱部署有什麽關系?

說了這麽多,Java agent 確實可以在 main 方法之前並加載字節碼的同時進行代理,有點類似 AOP。但到底和熱部署有什麽關系呢?

回憶剛開始我們說的,我們如果自己自定義一個類加載器,那麽就可以在重新加載類(新的類加載器)的時候對字節碼進行修改,但是對業務代碼侵入性較大,如果在底層,也就是 JVM 層面,在加載字節碼的時候回調某個方法,在該方法中修改字節碼,豈不是達到了我們的目的?

我們再看看 premain 方法:

  public static void premain(String agentArgs, Instrumentation inst) {
    System.out.println("agentArgs : " + agentArgs);
    inst.addTransformer(new ClassFileTransformer() {
      @Override
      public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
          ProtectionDomain protectionDomain, byte[] classfileBuffer)
          throws IllegalClassFormatException {
        System.out.println("premain load Class     :" + className);
        return classfileBuffer;
      }
    }, true);
  }

該方法中的 Instrumentation 添加了一個類轉換器,該轉換器是長期有效的,當該轉換器被添加之後,只要有類加載的活動,都會被攔截。假設,我們的業務是當某個類需要修改,我們就重新加載(重新創建類加載器的前提)原來的字節碼,加載之後,註意:加載之後對該字節碼進行修改。而這些操作對業務代碼來說,完全是透明的,基本沒有侵入性(加入了 VM 參數)。

總結

通過上面的步驟,我們將 探秘 Java 熱部署 的代碼進行了優化,原本是直接手動修改字節碼,現在通過加載原來的字節碼,在原來的字節碼基礎上進行修改,再重新加載,完成了一次熱部署。

而著一切得益於 java agent,雖然使用自定義的類加載也可以做到,但是似乎顯得不是很優雅。使用 java agent 能讓修改字節碼這個動作化於無形,對業務透明,減少侵入性。

實際上,premain 還是有缺點的。什麽缺點?竟然還要再命令行加參數?能不能不加參數?可以! java 1.6 已經為我們準備了這個工具。也就是 agentmain ,可以不加任何參數,就可以修改一個類,甚至不需要重新創建類加載器!神奇嗎?我們說,一個類加載器只能加載一個類,要想修改一個類,必須重新創建一個新的類加載器。但是,JVM 為我們做了很多,他在底層直接修改了類定義。使得我們不必重新創建類加載器了。具體我們將在下篇文章中詳細介紹。

good luck!!!!!

探秘 Java 熱部署二(Java agent premain)