JavaAgent學習小結

前言

最近由於公司須要,須要瞭解下java探針,在網上找資料,發現資料仍是有不少的,可是例子太少,有的直接把公司代碼粘貼出來,太複雜了,有的又特別簡單不是我想要的例子, 我想要這樣的一個例子:html

jvm在運行,我想動態修改一個類,jvm在不用重啓的狀況下, 自動加載新的類定義. 動態修改類定義,聽着感受就很酷. 本文將實現一個方法監控的例子, 開始方法是沒有監控的, 動態修改後, 方法執行結束會打印方法耗時.java

Instrumentation介紹

使用 Instrumentation,開發者能夠構建一個獨立於應用程序的代理程序(Agent),用來監測和協助運行在 JVM 上的程序,甚至可以替換和修改某些類的定義。有了這樣的功能,開發者就能夠實現更爲靈活的運行時虛擬機監控和 Java 類操做了,這樣的特性實際上提供了一種虛擬機級別支持的 AOP 實現方式,使得開發者無需對 JDK 作任何升級和改動,就能夠實現某些 AOP 的功能了。編程

在 Java SE 5 中,Instrument 要求在運行前利用命令行參數或者系統參數來設置代理類,在實際的運行之中,虛擬機在初始化之時(在絕大多數的 Java 類庫被載入以前),啓動instrumentation 的設置,從而能夠在加載字節碼以前,修改類的定義。app

在 Java SE6 裏面,則更進一步,能夠在jvm運行時,動態修改類定義,使用就更方便了,本文也主要是講着一種方式.eclipse

Instrumentation 類 定義以下:jvm

 1 /*有兩種獲取Instrumentation接口實例的方法:
 2 1.以指示代理類的方式啓動JVM時。 在這種狀況下,將Instrumentation實例傳遞給代理類的premain方法。
 3 2. JVM提供了一種在JVM啓動後的某個時間啓動代理的機制。 在這種狀況下,將Instrumentation實例傳遞給代理代碼的agentmain方法。
 4 這些機制在包裝規範中進行了描述。
 5 代理獲取某個Instrumentation實例後,該代理能夠隨時在該實例上調用方法。
 6 */
 7 public interface Instrumentation {
 8     //增長一個Class 文件的轉換器,轉換器用於改變 Class 二進制流的數據,參數 canRetransform 設置是否容許從新轉換。
 9     void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
10     //註冊一個轉換器
11     void addTransformer(ClassFileTransformer transformer);
12 
13     //刪除一個類轉換器
14     boolean removeTransformer(ClassFileTransformer transformer);
15 
16     boolean isRetransformClassesSupported();
17 
18     //在類加載以後,從新定義 Class。這個很重要,該方法是1.6 以後加入的,事實上,該方法是 update 了一個類。
19     void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
20 
21     boolean isRedefineClassesSupported();
22     /*此方法用於替換類的定義,而無需引用現有的類文件字節,除了在常規JVM語義下會發生的初始化以外,此方法不會引發任何初始化。換句話說,從新定義類不會致使其初始化程序運行。靜態變量的值將保持調用前的狀態。
23 從新定義的類的實例不受影響。*/
24     void redefineClasses(ClassDefinition... definitions)
25         throws  ClassNotFoundException, UnmodifiableClassException;
26 
27     boolean isModifiableClass(Class<?> theClass);
28     //獲取全部已經加載的類
29     @SuppressWarnings("rawtypes")
30     Class[] getAllLoadedClasses();
31 
32     @SuppressWarnings("rawtypes")
33     Class[] getInitiatedClasses(ClassLoader loader);
34     //獲取一個對象的大小
35     long getObjectSize(Object objectToSize);
36    
37     void appendToBootstrapClassLoaderSearch(JarFile jarfile);
38     
39     void appendToSystemClassLoaderSearch(JarFile jarfile);
40     boolean isNativeMethodPrefixSupported();
41     void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
42 }
  • 其中addTransformer 和 retransformClasses 是有關聯的, addTransformer 註冊轉換器,retransformClasses 觸發轉換器.
  • redefineClass是除了Transformer 以外另一中轉變類定義的方式.

Instrument的兩種方式

第一種: JVM啓動前靜態Instrument

使用Javaagent命令啓動代理程序。參數 javaagent 能夠用於指定一個 jar 包,而且對該 java 包有2個要求:編程語言

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

premain 方法,從字面上理解,就是運行在 main 函數以前的的類。當Java 虛擬機啓動時,在執行 main 函數以前,JVM 會先運行-javaagent所指定 jar 包內 Premain-Class 這個類的 premain 方法 。ide

在命令行輸入 java能夠看到相應的參數,其中有 和 java agent相關的:函數

-agentlib:<libname>[=<選項>] 加載本機代理庫 <libname>, 例如 -agentlib:hprof
    另請參閱 -agentlib:jdwp=help 和 -agentlib:hprof=help
-agentpath:<pathname>[=<選項>]
    按完整路徑名加載本機代理庫
-javaagent:<jarpath>[=<選項>]
    加載 Java 編程語言代理, 請參閱 java.lang.instrument

從本質上講,Java Agent 是一個遵循一組嚴格約定的常規 Java 類。 上面說到 javaagent命令要求指定的類中必需要有premain()方法,而且對premain方法的簽名也有要求,簽名必須知足如下兩種格式:工具

public static void premain(String agentArgs, Instrumentation inst)
    
public static void premain(String agentArgs)

JVM 會優先加載 帶 Instrumentation 簽名的方法,加載成功忽略第二種,若是第一種沒有,則加載第二種方法。這個邏輯在sun.instrument.InstrumentationImpl 類中.

如何使用javaagent?

使用 javaagent 須要幾個步驟:

  1. 定義一個 MANIFEST.MF 文件,必須包含 Premain-Class 選項,一般也會加入Can-Redefine-Classes 和 Can-Retransform-Classes 選項。
  2. 建立一個Premain-Class 指定的類,類中包含 premain 方法,方法邏輯由用戶本身肯定。
  3. 將 premain 的類和 MANIFEST.MF 文件打成 jar 包。
  4. 使用參數 -javaagent: jar包路徑 啓動要代理的方法。

在執行以上步驟後,JVM 會先執行 premain 方法,大部分類加載都會經過該方法,注意:是大部分,不是全部。固然,遺漏的主要是系統類,由於不少系統類先於 agent 執行,而用戶類的加載確定是會被攔截的。也就是說,這個方法是在 main 方法啓動前攔截大部分類的加載活動,既然能夠攔截類的加載,那麼就能夠去作重寫類這樣的操做,結合第三方的字節碼編譯工具,好比ASM,javassist,cglib等等來改寫實現類。

MANIFREST.MF文件的經常使用配置:

Premain-Class :包含 premain 方法的類(類的全路徑名)

Agent-Class :包含 agentmain 方法的類(類的全路徑名)

Boot-Class-Path :設置引導類加載器搜索的路徑列表。查找類的特定於平臺的機制失敗後,引導類加載器會搜索這些路徑。按列出的順序搜索路徑。列表中的路徑由一個或多個空格分開。路徑使用分層 URI 的路徑組件語法。若是該路徑以斜槓字符(「/」)開頭,則爲絕對路徑,不然爲相對路徑。相對路徑根據代理 JAR 文件的絕對路徑解析。忽略格式不正確的路徑和不存在的路徑。若是代理是在 VM 啓動以後某一時刻啓動的,則忽略不表示 JAR 文件的路徑。(可選)

Can-Redefine-Classes :true表示能重定義此代理所需的類,默認值爲 false(可選)

Can-Retransform-Classes :true 表示能重轉換此代理所需的類,默認值爲 false (可選)

Can-Set-Native-Method-Prefix: true表示能設置此代理所需的本機方法前綴,默認值爲 false(可選)

列舉一個premain 的例子:

 1 public class PreMainTraceAgent {
 2     public static void premain(String agentArgs, Instrumentation inst) {
 3         System.out.println("agentArgs : " + agentArgs);
 4         inst.addTransformer(new DefineTransformer(), true);
 5     }
 6 
 7     static class DefineTransformer implements ClassFileTransformer{
 8         @Override
 9         public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
10             System.out.println("premain load Class:" + className);
11             return classfileBuffer;
12         }
13     }
14 }

因爲本文不關注這種靜態Instrumentation的方式,這裏只是作簡介,感興趣的能夠去搜索下.

第二種動態Instrumentation的方式

在 Java SE 6 的 Instrumentation 當中,有一個跟 premain「並駕齊驅」的「agentmain」方法,能夠在 main 函數開始運行以後再運行。

跟 premain 函數同樣, 開發者能夠編寫一個含有「agentmain」函數的 Java 類:

因爲本文不關注這種靜態Instrumentation的方式,這裏只是作簡介,感興趣的能夠去搜索下.
第二種動態Instrumentation的方式

在 Java SE 6 的 Instrumentation 當中,有一個跟 premain「並駕齊驅」的「agentmain」方法,能夠在 main 函數開始運行以後再運行。
跟 premain 函數同樣, 開發者能夠編寫一個含有「agentmain」函數的 Java 類:

跟 premain 函數同樣,開發者能夠在 agentmain 中進行對類的各類操做。其中的 agentArgs 和 Inst 的用法跟 premain 相同。

與「Premain-Class」相似,開發者必須在 manifest 文件裏面設置「Agent-Class」來指定包含 agentmain 函數的類。

但是,跟 premain 不一樣的是,agentmain 須要在 main 函數開始運行後才啓動,至於該方法如何運行,怎麼跟正在運行的jvm 關聯上, 就須要介紹下Attach API.

Attach API 不是 Java 的標準 API,而是 Sun 公司提供的一套擴展 API,用來向目標 JVM 」附着」(Attach)代理工具程序的。有了它,開發者能夠方便的監控一個 JVM,運行一個外加的代理程序。

Attach API 很簡單,只有 2 個主要的類,都在 com.sun.tools.attach 包裏面: VirtualMachine 表明一個 Java 虛擬機,也就是程序須要監控的目標虛擬機,提供了 JVM 枚舉,Attach 動做和 Detach 動做(Attach 動做的相反行爲,從 JVM 上面解除一個代理)等等 ; VirtualMachineDescriptor 則是一個描述虛擬機的容器類,配合 VirtualMachine 類完成各類功能。

下邊咱們利用上邊說的實現一個監控方法執行耗時的例子: 定時執行一個方法,開始方法是沒有監控的, 方法重定義加上監控。

一個簡單的方法監控例子 

那麼咱們想一下須要實現這個例子,須要幾個模塊.

  • 一個代理模塊(監控邏輯);
  • 一個main函數(運行的jvm);
  • 一個把上邊兩個模塊關聯在一塊兒的程序.

從代理模塊開始:

1. 須要監控的TimeTest類:

/**
 * @ClassName TimeTest
 * @Author jiangyuechao
 * @Date 2020/1/20-10:36
 * @Version 1.0
 */
public class TimeTest {

    public static void sayHello( ){
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("sayhHello..........");
    }

    public static void sayHello2(String word){
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("sayhHello2.........."+word);
    }
}

2. 編寫agent 代碼

字節碼轉換類:

 1 public class MyTransformer implements ClassFileTransformer {
 2 
 3     // 被處理的方法列表
 4     final static Map<String, List<String>> methodMap = new HashMap<String, List<String>>();
 5 
 6     public MyTransformer() {
 7         add("com.chaochao.java.agent.TimeTest.sayHello");
 8         add("com.chaochao.java.agent.TimeTest.sayHello2");
 9     }
10 
11     private void add(String methodString) {
12         String className = methodString.substring(0, methodString.lastIndexOf("."));
13         String methodName = methodString.substring(methodString.lastIndexOf(".") + 1);
14         List<String> list = methodMap.get(className);
15         if (list == null) {
16             list = new ArrayList<String>();
17             methodMap.put(className, list);
18         }
19         list.add(methodName);
20     }
21 
22     @Override
23     public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
24                             ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
25         System.out.println("className:"+className);
26         if (methodMap.containsKey(className)) {// 判斷加載的class的包路徑是否是須要監控的類
27             try {
28                 ClassPool classPool=new ClassPool();
29                 classPool.insertClassPath(new LoaderClassPath(loader));
30                 CtClass ctClass= classPool.get(className.replace("/","."));
31 //                CtMethod ctMethod= ctClass.getDeclaredMethod("run");
32                 CtMethod[] declaredMethods = ctClass.getDeclaredMethods();
33                 for (CtMethod ctMethod : declaredMethods) {
34                        //插入本地變量
35                     ctMethod.addLocalVariable("begin",CtClass.longType);
36                     ctMethod.addLocalVariable("end",CtClass.longType);
37 
38                     ctMethod.insertBefore("begin=System.currentTimeMillis();System.out.println(\"begin=\"+begin);");
39                     //前面插入:最後插入的放最上面
40                     ctMethod.insertBefore("System.out.println( \"埋點開始-1\" );");
41 
42                     ctMethod.insertAfter("end=System.currentTimeMillis();System.out.println(\"end=\"+end);");
43                     ctMethod.insertAfter("System.out.println(\"性能:\"+(end-begin)+\"毫秒\");");
44 
45                     //後面插入:最後插入的放最下面
46                     ctMethod.insertAfter("System.out.println( \"埋點結束-1\" );");
47                 }
48                 return ctClass.toBytecode();
49             }  catch (NotFoundException | CannotCompileException|IOException e) {
50                 e.printStackTrace();
51             } 
52             return new byte[0];
53         }
54         else
55              System.out.println("沒找到.");
56         return null;
57     }
58     
59 }

上邊的類就是在方法先後加上耗時打印.

下邊是定義的AgentMainTest: 

import java.lang.instrument.Instrumentation;

public class AgentMainTest {
   //關聯後執行的方法
    public static void agentmain(String args, Instrumentation inst) throws Exception {
        System.out.println("Args:" + args);
        Class[] classes = inst.getAllLoadedClasses();
        for (Class clazz : classes) 
        {
           System.out.println(clazz.getName());
        }
        System.out.println("開始執行自定義MyTransformer");
        // 添加Transformer
        inst.addTransformer(new MyTransformer(),true);
        
        inst.retransformClasses(TimeTest.class);
    }
    
    public static void premain(String args, Instrumentation inst) throws Exception 
    {
        System.out.println("Pre Args:" + args);
        Class[] classes = inst.getAllLoadedClasses();
        for (Class clazz : classes) 
        {
           System.out.println(clazz.getName());
        }
    } 
}

MANIFREST.MF文件定義,注意最後一行是空格:

Manifest-Version: 1.0
Premain-Class: com.chaochao.java.agent.AgentMainTest
Agent-Class: com.chaochao.java.agent.AgentMainTest
Can-Redefine-Classes: true
Can-Retransform-Classes: true

 

代理模塊介紹完畢, 下邊是一個main函數程序.這個就很簡單了.

 1 public class TestMan {
 2 
 3     public static void main(String[] args) throws InterruptedException 
 4     {
 5         TimeTest tt = new TimeTest();
 6         tt.sayHello();
 7         tt.sayHello2("one");
 8         while(true)
 9         {
10             Thread.sleep(60000);
11             new Thread(new WaitThread()).start();  
12             tt.sayHello();
13             tt.sayHello2("two");
14         }
15     }
16      
17    static class WaitThread implements Runnable 
18    {
19         @Override  
20         public void run()
21         {
22             System.out.println("Hello"); 
23         }
24    }
25 }

最後一個關聯模塊:

/**
 * 
 * @author jiangyuechao
 *
 */
public class AttachMain {

    public static void main(String[] args) throws Exception{
        VirtualMachine vm = null;  
        String pid = null;
        List<VirtualMachineDescriptor> list = VirtualMachine.list();  
        for (VirtualMachineDescriptor vmd : list)  
        {
            System.out.println("pid:" + vmd.id() + ":" + vmd.displayName());
            if(vmd.displayName().contains("TestMan")) {
                pid = vmd.id();
            }
        }
        //E:\eclipse-workspace\JavaStudyAll\JVMStudy\target
       // String agentjarpath = "E:/jee-workspace/javaAgent/TestAgent.jar"; //agentjar路徑  
        String agentjarpath = "E:/jee-workspace/javaAgent/AgentMainTest.jar"; //agentjar路徑  
        vm = VirtualMachine.attach(pid);//目標JVM的進程ID(PID)  
        vm.loadAgent(agentjarpath, "This is Args to the Agent.");  
        vm.detach();  
      }

}

也很簡單, 第一步獲取pid ,第二步使用attach 方法關聯jvm.

上便代碼準備好了,那麼怎麼把他們運行起來呢, 須要幾步:

  1. 先把agent 代碼打包爲jar 包
  2. 運行main 函數,執行agent

agent 打包

把agent代碼打包爲普通的jar 包便可, 使用eclipse或intellij 均可以. 以eclipse 爲例,只須要注意一步使用你寫好的MANIFREST文件 

可是我推薦使用另一種方式,命令行的方式, 使用java 命令行直接來的, 既方便又快捷.

首先把須要的類放在一個文件夾下, javac編譯:

javac -encoding UTF-8 -classpath .;E:\tools\jdk1.8.0_65\lib\tools.jar;E:\eclipse-workspace\JavaStudyAll\JVMStudy\lib\javassist.jar; AgentMainTest.java MyTransformer.java

其中須要依賴tools.jar和 javassist jar包.

編譯後的class文件打包爲jar包:

jar cvmf MANIFEST.MF AgentMainTest.jar AgentMainTest.class MyTransformer.class 

以下所示:

agent包準備好以後, 就簡單了,先運行main函數,啓動一個虛擬機. 運行入下:

sayhHello..........
sayhHello2..........one

運行AttachMain 類,關聯agent程序,就會看到以下的輸出:

能夠看到 在方法執行結束後, 已經有了耗時的打印. 測試成功.

Instrumentation的侷限性

大多數狀況下,咱們使用Instrumentation都是使用其字節碼插樁的功能,或者籠統說就是類重定義(Class Redefine)的功能,可是有如下的侷限性:

  1. premain和agentmain兩種方式修改字節碼的時機都是類文件加載以後,也就是說必需要帶有Class類型的參數,不能經過字節碼文件和自定義的類名從新定義一個原本不存在的類。
  2. 類的字節碼修改稱爲類轉換(Class Transform),類轉換其實最終都回歸到類重定義Instrumentation#redefineClasses()方法,此方法有如下限制:
    1. 新類和老類的父類必須相同;
    2. 新類和老類實現的接口數也要相同,而且是相同的接口;
    3. 新類和老類訪問符必須一致。 新類和老類字段數和字段名要一致;
    4. 新類和老類新增或刪除的方法必須是private static/final修飾的;
    5. 能夠修改方法體。

除了上面的方式,若是想要從新定義一個類,能夠考慮基於類加載器隔離的方式:建立一個新的自定義類加載器去經過新的字節碼去定義一個全新的類,不過也存在只能經過反射調用該全新類的侷限性。

參考:

http://www.javashuo.com/article/p-dzoqxtxo-gu.html

https://www.ibm.com/developerworks/cn/java/j-lo-jse61/index.html 

 

轉發請註明出處: https://www.cnblogs.com/jycboy/p/12249472.html 

相關文章
相關標籤/搜索