简单聊下 Java Agent

JavaAgent 是 JDK 1.5 以后引入的,也可以叫做 Java 代理。
JavaAgent 是运行在 main 方法之前的拦截器,它内定的方法名叫 premain(),也就是说先执行 premain() 然后再执行 main 方法。

使用步骤:

  • 定义一个 MANIFEST.MF 文件,在其中添加 premain-class 配置项。
  • 创建 premain-class 配置项指定的类,并在其中实现 premain() 方法。
  • 将 MANIFEST.MF 文件和 premain-class 指定的类一起打包成一个 jar 包。
  • 使用 -javaagent: 指定该 jar 包的路径。

举个栗子

agent-demo 工程

必须提供的 premain() 方法。

1
2
3
4
5
6
7
8
9
10
11
public class AgentDemo {
  public static void premain(String agentArgs, Instrumentation inst) throws Exception {
    System.out.println("premain(String agentArgs, Instrumentation inst)");
    System.out.println("参数:" + agentArgs);
    System.out.println("参数:" + inst);
  }
  public static void premain(String agentArgs) throws Exception {
    System.out.println("premain(String agentArgs)");
    System.out.println("参数:" + agentArgs);
  }
}

agent-demo 工程编译成 agent-demo.jar 之后。打开压缩包修改 MANIFEST.MF 文件。

1
2
3
4
5
6
7
8
9
10
11
12
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Created-By: Apache Maven
Built-By: chenxinjie
Build-Jdk: 13.0.2
Specification-Title: agent-demo
Specification-Version: 0.0.1-SNAPSHOT
Implementation-Title: agent-demo
Implementation-Version: 0.0.1-SNAPSHOT
Implementation-Vendor-Id: cn.live
Can-Retransform-Classes: true
Premain-Class: cn.live.AgentDemo

main-demo 工程

写一个简单的 Java Application 方法,在命令行输出一段字符串。

1
2
3
4
5
public class MainDemo {
  public static void main(String[] args) throws Exception {
    System.out.println("MainDemo end");
  }
}

直接运行 Java Application,得到的结果是:

1
MainDemo end

当修改 VM arguments 参数,加上-javaagent:"/Users/chenxinjie/GitHub/springboot-demo/agent-demo/target/agent-demo.jar=systemNo=main-demo" 运行 Java Application,得到的结果是:

1
2
3
4
5
premain(String agentArgs, Instrumentation inst)
参数:systemNo=main-demo
参数:sun.instrument.InstrumentationImpl@5caf905d

MainDemo end

统计方法执行时间

1
2
public java.lang.String cn.live.service.impl.DemoServiceImpl.hello(java.lang.String) hook :1004ms
public java.lang.String cn.live.controller.DemoController.hello() hook :3004ms

以上是 Java Agent 技术与 bytebuddy 工具,配合实现的统计方法执行时间功能。

agent-demo 工程

修改 premain() 方法,使用 bytebuddy 工具动态生产 Agent。

1
2
3
4
5
6
7
8
9
10
11
AgentBuilder.Transformer transformer = new AgentBuilder.Transformer() {
  @Override
  public Builder<?> transform(Builder<?> arg0, TypeDescription arg1, ClassLoader arg2,
      JavaModule arg3) {
    return arg0.method(ElementMatchers.any())
        .intercept(MethodDelegation.to(TimeInterceptor.class));
  }
};

new AgentBuilder.Default().type(ElementMatchers.nameStartsWith("cn.live"))
  .transform(transformer).installOn(inst);

新增 TimeInterceptor.java 类。

1
2
3
4
5
6
7
8
9
@RuntimeType
public static Object intercept(@Origin Method method, @SuperCall Callable<?> callable) throws Exception {
  long start = System.currentTimeMillis();
  try {
    return callable.call(); // 执行方法
  } finally {
    System.out.println(method + " hook :" + (System.currentTimeMillis() - start) + "ms");
  }
}
  • 记录方法运行之前的时间 start
  • 执行方法并返回
  • finally 中计算消耗的时间差并输出字符串

小结

感到万分尴尬,如此优秀的 Java Agent 从来都没有关注过!

本文主要通过两个案例来了解 Java Agent 技术,顺便提出两个问题:Java Agent、bytebuddy 到底是怎么样的一个实现原理?有什么优秀的开源案例?