基于Java Agent的premain方式實現(xiàn)方法耗時監(jiān)控

Java Agent是依附于java應(yīng)用程序并能對其字節(jié)碼做相關(guān)更改的一項技術(shù),它也是一個Jar包,但并不能獨立運行,有點像寄生蟲的感覺。當(dāng)今的許多開源工具尤其是監(jiān)控和診斷工具,很多都是基于Java Agent來實現(xiàn)的,如最近阿里剛開源的Arthas。一個Java Agent既可以在程序運行前加載(premain方式), 又可以在程序運行后加載(attach方式)。本文通過實現(xiàn)一個對Java方法耗時監(jiān)控的Agent來讓大家了解一下Java Agent的premain方式具體應(yīng)用。

首先給出測試類,如下所示,該類的代碼很簡單,最終要達到的目的就是在不修改這段代碼的情況下,能夠知道運行這段程序時每個方法的具體耗時,也就是實現(xiàn)一個Java方法耗時監(jiān)控的Agent。

MyAgentTest.java

public class MyAgentTest {
    public static void main(String[] args) throws InterruptedException {
        MyAgentTest mat = new MyAgentTest();
        mat.test();
        Thread.sleep((long)(Math.random() * 10));//隨機暫停0-10ms
    }

    public void test() throws InterruptedException {
        System.out.println("I'm TestAgent");
        Thread.sleep((long)(Math.random() * 100));//隨機暫停0-100ms
    }
}

接下來就是要創(chuàng)建一個名為myagent的工程,項目結(jié)構(gòu)如下:

myagent
├── pom.xml
├── src
│   └── main
│       ├── java
│       │   └── test
│       │       ├── MyAgent.java
│       │       └── MyTransformer.java
│       └── resources
│           └── META-INF
│               └── MANIFEST.MF

從上面可以看到,項目中主要文件只有兩個java類和一個MANIFEST.MF,所以Java Agent其實也并沒有那么神秘。

先看看pom.xml這個文件,因為字節(jié)碼的相關(guān)操作要依賴于javassist這個包,所以要添加相關(guān)依賴。在默認(rèn)情況下,用maven進行打包時會覆蓋掉我們自己的MANIFEST.MF,以及不會引進依賴的jar包,所以在build中要引進maven-assembly-plugin插件并添加相關(guān)配置。

pom.xml

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.hebh.me</groupId>
  <artifactId>demo-myagent</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>demo-myagent Maven Webapp</name>
  <url>http://www.example.com</url>

  <dependencies>
    <dependency>
      <groupId>javassist</groupId>
      <artifactId>javassist</artifactId>
      <version>3.12.1.GA</version>
    </dependency>
  </dependencies>

  <build>
    <finalName>myagent</finalName>
    <plugins>
      <plugin>
        <artifactId>maven-assembly-plugin</artifactId>
        <configuration>
          <archive>
            <!--避免MANIFEST.MF被覆蓋-->
            <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
          </archive>
          <descriptorRefs>
            <!--打包時加入依賴-->
            <descriptorRef>jar-with-dependencies</descriptorRef>
          </descriptorRefs>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

接下來就是最重要MyAgent類,而premain(String args, Instrumentation inst)這個方法是關(guān)鍵,也是Agent的入口, 在方法里我們直接打印"Hi, I'm agent!"文本并添加一個自己實現(xiàn)的字節(jié)碼轉(zhuǎn)換器。

MyAgent.java

package test;

import java.lang.instrument.Instrumentation;
public class MyAgent {
    public static void premain(String args, Instrumentation inst){
        System.out.println("Hi, I'm agent!");
        inst.addTransformer(new MyTransformer());
    }
}

然后看看這個字節(jié)碼轉(zhuǎn)換器的具體實現(xiàn),首先是要實現(xiàn)ClassFileTransformer接口,然后實現(xiàn)接口中的transform方法,jdk中源碼對該接口的說明如下

An agent provides an implementation of this interface in order to transform class files. The transformation occurs before the class is defined by the JVM

翻譯過來也就是我們可以通過實現(xiàn)該接口來在虛擬機加載類之前對字節(jié)碼進行相關(guān)更改。

對于該方法的說明文字比較多,在這里我們只需要知道該方法傳入類的所有相關(guān)信息,然后返回一個修改后的類的字節(jié)碼。要達到對方法耗時的監(jiān)控最核心的代碼在在這個方法里面,如下,首先過濾我們不關(guān)注的類,復(fù)制關(guān)注類的原方法并在執(zhí)行之前獲取時間戳,執(zhí)行后再次獲取時間戳,兩者取差值即為方法耗時,為一直觀顯示我們直接添加相關(guān)打印代碼。

MyTransformer.java

package test;

import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;

/**
 * 檢測方法的執(zhí)行時間
 */
public class MyTransformer implements ClassFileTransformer {

    final static String prefix = "\nlong startTime = System.currentTimeMillis();\n";
    final static String postfix = "\nlong endTime = System.currentTimeMillis();\n";

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain, byte[] classfileBuffer){
        //java自帶的方法不進行處理
        if(className.startsWith("java") || className.startsWith("sun")){
            return null;
        }
        className = className.replace("/", ".");
        CtClass ctclass = null;
        try {
            ctclass = ClassPool.getDefault().get(className);// 使用全稱,用于取得字節(jié)碼類<使用javassist>
            for(CtMethod ctMethod : ctclass.getDeclaredMethods()){
                String methodName = ctMethod.getName();
                String newMethodName = methodName + "$old";// 新定義一個方法叫做比如sayHello$old
                ctMethod.setName(newMethodName);// 將原來的方法名字修改

                // 創(chuàng)建新的方法,復(fù)制原來的方法,名字為原來的名字
                CtMethod newMethod = CtNewMethod.copy(ctMethod, methodName, ctclass, null);

                // 構(gòu)建新的方法體
                StringBuilder bodyStr = new StringBuilder();
                bodyStr.append("{");
                bodyStr.append("System.out.println(\"==============Enter Method: " + className + "." + methodName + " ==============\");");
                bodyStr.append(prefix);
                bodyStr.append(newMethodName + "($$);\n");// 調(diào)用原有代碼,類似于method();($$)表示所有的參數(shù)
                bodyStr.append(postfix);
                bodyStr.append("System.out.println(\"==============Exit Method: " + className + "." + methodName + " Cost:\" +(endTime - startTime) +\"ms " + "===\");");
                bodyStr.append("}");

                newMethod.setBody(bodyStr.toString());// 替換新方法
                ctclass.addMethod(newMethod);// 增加新方法
            }
            return ctclass.toBytecode();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

最后也是非常重要并且容易出錯的地方就是在resources文件夾下面創(chuàng)建META-INF文件夾,然后定義MANIFEST.MF文件,通過Premain-Class屬性來指定Agent的入口,需要注意的是冒號后面必須要有一個空格,并且最后要空出一行。

MANIFEST.MF

Manifest-Version: 1.0
Created-By: 0.0.1 (Demo Inc.)
Premain-Class: test.MyAgent

到此為止我們就已經(jīng)完成了myagent工程的所有代碼,為了使用它就必須將其打包為jar文件,用如下命令:

mvn assembly:assembly

執(zhí)行mvn命令后就可以在項目的target目錄下看到生成的myagent-jar-with-dependencies.jar文件。

然后編譯在最開始用來測試的類:

javac MyAgentTest.java

編譯后就生成了.class文件,為了方便,我們把.class文件放到和myagent-jar-with-dependencies.jar同一個目錄。

如果不使用我們的agent直接執(zhí)行java命令,效果如下:

image-20190105205332639

如果在javaagent參數(shù)中加上agent,效果如下:

image-20190105205205594

首先在執(zhí)行所有方法前,會執(zhí)行MyAgent的premain方法。并且可以直觀看到,MyAgentTest在運行時首先是進入main方法,然后再是test方法,執(zhí)行完test方法邏輯后退出test方法,最后退出main方法,不僅能看到每個方法的最終耗時也能看到方法執(zhí)行的軌跡。

目標(biāo)達成,完。。。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,748評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,165評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,595評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,633評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,435評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,943評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,035評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,175評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,713評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,599評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,788評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,303評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,034評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,412評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,664評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,408評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,747評論 2 370

推薦閱讀更多精彩內(nèi)容