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

前言

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

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

咱們還提到了,premain 雖然能夠熱部署,可是還須要從新建立類加載器,雖然,這的確也符合 JVM 關於類的惟一性的定義。可是,有一種狀況,若是使用的是系統類加載器,咱們也沒法建立新的ClassLoader對象。那麼咱們也就沒法從新加載類了,怎麼辦呢?還好 Java 6 爲咱們提供了一種方法,也就是今天的主角 agentmain。git

1. 什麼是 agentmain?

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

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

1.父類是同一個;maven

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

能夠看的出來,相比較從新建立類加載器,限制仍是挺多的,最重要的字段是沒法修改的。所以,使用的時候要注意。ide

可是,agentmain 還有一個強大的特色,就是目標程序什麼都不須要管,就可以被代理。還記得 premain 是如何使用的嗎?須要在目標應用啓動的時候增長 -javaagent 參數。雖然說沒有侵入性,但相比 agentmain 而言,仍是有侵入性的,畢竟 agentmain 什麼都不要。目標程序獨立運行,什麼都不用管。源碼分析

那咱們就來試試吧!測試

2. 如何使用?

agentmain 使用步驟以下:google

  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 新功能

相關文章
相關標籤/搜索