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

探秘 Java 熱部署三(Java agent agentmain)

des 一個 事情 AD read initial virtual ring tran

技術分享圖片

前言

讓我們繼續探秘 Java 熱部署。在前文 探秘 Java 熱部署二(Java agent premain)中,我們介紹了 Java agent premain。通過在main方法之前通過類似 AOP 的方式添加 premain 方法,我們可以在類加載之前做修改字節碼的操作,無論是第一次加載,還是每次新的 ClassLoader 加載,都會經過 ClassFileTransformer 的 transform 方法,也就是說,都可以在這個方法中修改字節碼,雖然他的方法名是 premain ,但是我們確實可以利用這個特性做這個事情。

在文章的最後,我們也提到了,雖然相比較在自定義類中修改字節碼,premain 沒有什麽侵入性,對業務透明,但是美中不足的是,他還需要在啟動的時候增加參數。

我們還提到了,premain 雖然可以熱部署,但是還需要重新創建類加載器,雖然,這的確也符合 JVM 關於類的唯一性的定義。但是,有一種情況,如果使用的是系統類加載器,我們也無法創建新的ClassLoader對象。那麽我們也就無法重新加載類了,怎麽辦呢?還好 Java 6 為我們提供了一種方法,也就是今天的主角 agentmain。

1. 什麽是 agentmain?

和 premain 師出同門,我們知道,premain 只能在類加載之前修改字節碼,類加載之後無能為力,只能通過重新創建ClassLoader 這種方式重新加載。而 agentmain 就是為了彌補這種缺點而誕生的。簡而言之,agentmain 可以在類加載之後再次加載一個類,也就是重定義,你就可以通過在重定義的時候進行修改類了,甚至不需要創建新的類加載器,JVM 已經在內部對類進行了重定義(重定義的過程相當復雜)。

但是這種方式對類的修改是由限制的,對比原來的老類,由如下要求:

1.父類是同一個;

  1. 實習那的接口數也要相同;
  2. 類訪問符必須一致;
  3. 字段數和字段名必須一致;
  4. 新增的方法必須是 private static/final 的;
  5. 可以刪除修改方法;

可以看的出來,相比較重新創建類加載器,限制還是挺多的,最重要的字段是無法修改的。因此,使用的時候要註意。

但是,agentmain 還有一個強大的特點,就是目標程序什麽都不需要管,就能夠被代理。還記得 premain 是如何使用的嗎?需要在目標應用啟動的時候增加 -javaagent 參數。雖說沒有侵入性,但相比 agentmain 而言,還是有侵入性的,畢竟 agentmain 什麽都不要。目標程序獨立運行,什麽都不用管。

那我們就來試試吧!

2. 如何使用?

agentmain 使用步驟如下:

  1. 定義一個MANIFEST.MF 文件,文件中必須包含 Agent-Class;
  2. 創建一個 Agent-Class 指定的類,該類必須包含 agentmain 方法(參數和 premian 相同)。
  3. 將MANIFEST.MF 和 Agent 類打成 jar 包;
  4. 將 jar 包載入目標虛擬機。目標虛擬機將會自動執行 agentmain 方法執行方法邏輯,同時,ClassFileTransformer 也會長期有效,在每一個類加載器加載 Class 的時候都會攔截。

讓我們按著步驟來一遍吧!

  1. 定義一個MANIFEST.MF 文件,文件中必須包含 Agent-Class;
Manifest-Version: 1.0
Can-Redefine-Classes: true
Agent-Class: cn.think.in.java.clazz.loader.asm.agent.AgentMainTraceAgent 
Can-Retransform-Classes: true
  1. 創建一個 Agent-Class 指定的類,該類必須包含 agentmain 方法(參數和 premian 相同)。
public class AgentMainTraceAgent {

  public static void agentmain(String agentArgs, Instrumentation inst)
      throws UnmodifiableClassException {
    System.out.println("Agent Main called");
    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("agentmain load Class  :" + className);
          return classfileBuffer;
      }
    }, true);
    inst.retransformClasses(Account.class);
  }

上面的類邏輯很簡單,打印 Agent Main called,並打印參數,然後添加一個類轉換器,轉換器中的邏輯只是打印字符串,為了簡單起見,並沒有修改字節碼(各位可自行使用ASM 等類庫修改)。最後一點很重要,執行了 inst.retransformClasses(Account.class); 這段代碼的意思是,重新轉換目標類,也就是 Account 類。也就是說,你需要重新定義哪個類,需要指定,否則 JVM 不可能知道。還有一個類似的方法 redefineClasses ,註意,這個方法是在類加載前使用的。類加載後需要使用 retransformClasses 方法。

  1. 將MANIFEST.MF 和 Agent 類打成 jar 包;
    這個請自行 google。maven 或者 ide 都可以。
  2. 將 jar 包載入目標虛擬機。目標虛擬機將會自動執行 agentmain 方法執行方法邏輯,同時,ClassFileTransformer 也會長期有效,在每一個類加載器加載 Class 的時候都會攔截。

這段代碼很重要:

class JVMTIThread {

  public static void main(String[] args)
      throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
    List<VirtualMachineDescriptor> list = VirtualMachine.list();
    for (VirtualMachineDescriptor vmd : list) {
      if (vmd.displayName().endsWith("AccountMain")) {
        VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
        virtualMachine.loadAgent("E:\\self\\demo\\out\\artifacts\\test\\test.jar ", "cxs");
        System.out.println("ok");
        virtualMachine.detach();
      }
    }
  }
}

註意:寫這段代碼的時候 IDE 可能提示找不到 jar 包,這時候將 jdk/lib/tools.jar 添加的項目的 classpath 中,具體請自行 google。
該main方法步驟如下:

  1. 獲取當前系統所有的虛擬機,類似 jps 命令。
  2. 循環所有虛擬機,找到 AccountMain 虛擬機。
  3. 將當前JVM 鏈接上目標JVM,並加載 loadAgent jar 包且傳遞參數。
  4. 卸載虛擬機。

如何測試呢?

首先看測試類,也就是AccountMain類:

class AccountMain {

  public static void main(String[] args) throws InterruptedException {
    for (;;) {
      new Account().operation();
      Thread.sleep(5000);
    }

  }
}

當我們一切準備就緒,啟動 AccountMain 後,再啟動 JVMTIThread 類,結果如下:

技術分享圖片

可以看到,執行了1遍 operation方法後,我們啟動了 attach jvm,隨後,agentmain 方法就被執行了,並打印了我們我們傳遞的字符串參數,同時也進入到了 ClassFileTransformer 方法中,表示重新加載了 Account 類。有點遺憾的是,限於篇幅,我們沒有修改字節碼。不過樓主還是展示一下樓主修改字節碼的結果吧,假設樓主想統計 operation 方法的時長,樓主將使用 ASM 增加一段統計時長的代碼:

public class TimeStat {
  static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
  public static void start() {
    threadLocal.set(System.currentTimeMillis());
  }
  public static void end() {
    long time = System.currentTimeMillis() - threadLocal.get();
    System.out.print(Thread.currentThread().getStackTrace()[2] + "方法耗費時間: ");
    System.out.println(time + "毫秒");
  }
}

然後修改 agentmain 方法中ClassFileTransformer 的transform 邏輯,也就是在這裏修改字節碼。
運行結果如下:

技術分享圖片

可以看到,首先重定義了 Account 類,又主動加載了 TimeStat 類,然後生效,在 operation 字符串後面打印了方法的耗時。

總結

通過對 agentmain 的使用,我們感受到了他的強大,在目標程序絲毫不改動的,甚至連啟動參數都不加的情況下,可以修改類,並且是運行後修改,而且不重新創建類加載器。其主要得益於 JVM 底層的對類的重定義,關於底層代碼解釋,Jvm 大神寒泉子有篇文章 JVM源碼分析之javaagent原理完全解讀 ,詳細分析了 javaagent 的原理。但 agentmain 有一些功能上的限制,比如字段不能修改增減。所以,使用的時候需要權衡,到底使用哪種方式實現熱部署。

說了這麽久的熱部署,其實就是動態或者說運行時修改類,大的方向說有2種方式:

  1. 使用 agentmain,不需要重新創建類加載器,可直接修改類,但是有很多限制。
  2. 使用 premain 可以在類第一次加載之前修改,加載之後修改需要重新創建類加載器。或者在自定義的類加載器種修改,但這種方式比較耦合。

無論是哪種,都需要字節碼修改的庫,比如ASM,javassist ,cglib 等,很多。總之,通過java.lang.instrument 包配合字節碼庫,可以很方便的動態修改類,或者進行熱部署。

good luck!!!!!

參考: JVM源碼分析之javaagent原理完全解讀
《實戰Java 虛擬機》
javaagent
Instrumentation 新功能

探秘 Java 熱部署三(Java agent agentmain)