基于Java Agent的premain方式实现方法耗时监控

Java Agent是依附于java应用程序并能对其字节码做相关更改的一项技术,它也是一个Jar包,但并不能独立运行,有点像寄生虫的感觉。当今的许多开源工具尤其是监控和诊断工具,很多都是基于Java Agent来实现的,如最近阿里刚开源的Arthas。一个Java Agent既可以在程序运行前加载(premain方式), 又可以在程序运行后加载(attach方式)。本文通过实现一个对Java方法耗时监控的Agent来让大家了解一下Java Agent的premain方式具体应用。

首先给出测试类,如下所示,该类的代码很简单,最终要达到的目的就是在不修改这段代码的情况下,能够知道运行这段程序时每个方法的具体耗时,也就是实现一个Java方法耗时监控的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
    }
}

接下来就是要创建一个名为myagent的工程,项目结构如下:

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

从上面可以看到,项目中主要文件只有两个java类和一个MANIFEST.MF,所以Java Agent其实也并没有那么神秘。

先看看pom.xml这个文件,因为字节码的相关操作要依赖于javassist这个包,所以要添加相关依赖。在默认情况下,用maven进行打包时会覆盖掉我们自己的MANIFEST.MF,以及不会引进依赖的jar包,所以在build中要引进maven-assembly-plugin插件并添加相关配置。

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)这个方法是关键,也是Agent的入口, 在方法里我们直接打印"Hi, I'm agent!"文本并添加一个自己实现的字节码转换器。

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());
    }
}

然后看看这个字节码转换器的具体实现,首先是要实现ClassFileTransformer接口,然后实现接口中的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

翻译过来也就是我们可以通过实现该接口来在虚拟机加载类之前对字节码进行相关更改。

对于该方法的说明文字比较多,在这里我们只需要知道该方法传入类的所有相关信息,然后返回一个修改后的类的字节码。要达到对方法耗时的监控最核心的代码在在这个方法里面,如下,首先过滤我们不关注的类,复制关注类的原方法并在执行之前获取时间戳,执行后再次获取时间戳,两者取差值即为方法耗时,为一直观显示我们直接添加相关打印代码。

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;

/**
 * 检测方法的执行时间
 */
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);// 使用全称,用于取得字节码类<使用javassist>
            for(CtMethod ctMethod : ctclass.getDeclaredMethods()){
                String methodName = ctMethod.getName();
                String newMethodName = methodName + "$old";// 新定义一个方法叫做比如sayHello$old
                ctMethod.setName(newMethodName);// 将原来的方法名字修改

                // 创建新的方法,复制原来的方法,名字为原来的名字
                CtMethod newMethod = CtNewMethod.copy(ctMethod, methodName, ctclass, null);

                // 构建新的方法体
                StringBuilder bodyStr = new StringBuilder();
                bodyStr.append("{");
                bodyStr.append("System.out.println(\"==============Enter Method: " + className + "." + methodName + " ==============\");");
                bodyStr.append(prefix);
                bodyStr.append(newMethodName + "($$);\n");// 调用原有代码,类似于method();($$)表示所有的参数
                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文件夹下面创建META-INF文件夹,然后定义MANIFEST.MF文件,通过Premain-Class属性来指定Agent的入口,需要注意的是冒号后面必须要有一个空格,并且最后要空出一行。

MANIFEST.MF

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

到此为止我们就已经完成了myagent工程的所有代码,为了使用它就必须将其打包为jar文件,用如下命令:

mvn assembly:assembly

执行mvn命令后就可以在项目的target目录下看到生成的myagent-jar-with-dependencies.jar文件。

然后编译在最开始用来测试的类:

javac MyAgentTest.java

编译后就生成了.class文件,为了方便,我们把.class文件放到和myagent-jar-with-dependencies.jar同一个目录。

如果不使用我们的agent直接执行java命令,效果如下:

image-20190105205332639

如果在javaagent参数中加上agent,效果如下:

image-20190105205205594

首先在执行所有方法前,会执行MyAgent的premain方法。并且可以直观看到,MyAgentTest在运行时首先是进入main方法,然后再是test方法,执行完test方法逻辑后退出test方法,最后退出main方法,不仅能看到每个方法的最终耗时也能看到方法执行的轨迹。

目标达成,完。。。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容