UP

预期目标

我们的预期目标:打印正在加载的类。

开发环境:

  • JDK 版本:Java 8
  • 编辑器:记事本(Windows)或 vi(Linux)

我们尽量使用简单的工具,来理解 Agent Jar 的生成过程。

代码目录结构:Code

java-agent-manual-01
└─── src
     ├─── lsieun
     │    ├─── agent
     │    │    └─── LoadTimeAgent.java
     │    └─── instrument
     │         └─── InfoTransformer.java
     ├─── manifest.txt
     └─── sample
          ├─── HelloWorld.java
          └─── Program.java

做一些准备工作(prepare01.sh):

DIR=java-agent-manual-01
mkdir ${DIR} && cd ${DIR}

mkdir -p src/sample
touch src/sample/{HelloWorld.java,Program.java}

mkdir -p src/lsieun/{agent,instrument}
touch src/lsieun/agent/LoadTimeAgent.java
touch src/lsieun/instrument/InfoTransformer.java
touch src/manifest.txt

Application

HelloWorld.java

package sample;

public class HelloWorld {
    public static int add(int a, int b) {
        return a + b;
    }

    public static int sub(int a, int b) {
        return a - b;
    }
}

Program.java

package sample;

import java.lang.management.ManagementFactory;
import java.util.Random;
import java.util.concurrent.TimeUnit;

public class Program {
    public static void main(String[] args) throws Exception {
        // (1) print process id
        String nameOfRunningVM = ManagementFactory.getRuntimeMXBean().getName();
        System.out.println(nameOfRunningVM);

        // (2) count down
        int count = 600;
        for (int i = 0; i < count; i++) {
            String info = String.format("|%03d| %s remains %03d seconds", i, nameOfRunningVM, (count - i));
            System.out.println(info);

            Random rand = new Random(System.currentTimeMillis());
            int a = rand.nextInt(10);
            int b = rand.nextInt(10);
            boolean flag = rand.nextBoolean();
            String message;
            if (flag) {
                message = String.format("a + b = %d", HelloWorld.add(a, b));
            }
            else {
                message = String.format("a - b = %d", HelloWorld.sub(a, b));
            }
            System.out.println(message);

            TimeUnit.SECONDS.sleep(1);
        }
    }
}

编译和运行

进行编译:

# 进行编译
$ mkdir out
$ javac src/sample/*.java -d out/

# 查看编译结果
$ find ./out/ -type f
./out/sample/HelloWorld.class
./out/sample/Program.class

运行结果:

$ cd out/
$ java sample.Program
5556@LenovoWin7
|000| 5556@LenovoWin7 remains 600 seconds
a - b = 6
|001| 5556@LenovoWin7 remains 599 seconds
a - b = -4
...

Agent Jar

manifest.txt

修改 manifest.txt 文件内容:

Premain-Class: lsieun.agent.LoadTimeAgent

注意:在 manifest.txt 文件的结尾处有一个空行。(make sure the last line in the file is a blank line)

LoadTimeAgent.java

package lsieun.agent;

import lsieun.instrument.InfoTransformer;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;

public class LoadTimeAgent {
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("Premain-Class: " + LoadTimeAgent.class.getName());
        ClassFileTransformer transformer = new InfoTransformer();
        inst.addTransformer(transformer);
    }
}

InfoTransformer.java

package lsieun.instrument;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.Formatter;

public class InfoTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader,
                            String className,
                            Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain,
                            byte[] classfileBuffer) throws IllegalClassFormatException {
        StringBuilder sb = new StringBuilder();
        Formatter fm = new Formatter(sb);
        fm.format("ClassName: %s%n", className);
        fm.format("    ClassLoader: %s%n", loader);
        fm.format("    ClassBeingRedefined: %s%n", classBeingRedefined);
        fm.format("    ProtectionDomain: %s%n", protectionDomain);
        System.out.println(sb.toString());

        return null;
    }
}

生成 Jar 包

编译:

# 切换目录
$ cd java-agent-manual-01/

# 找到所有 .java 文件
$ find ./src/lsieun/ -name "*.java" > sources.txt
$ cat sources.txt
./src/lsieun/agent/LoadTimeAgent.java
./src/lsieun/transformer/InfoTransformer.java

# 进行编译
$ javac -d out/ @sources.txt

生成 Jar 包:

# 复制 manifest.txt 文件
$ cp src/manifest.txt out/

# 切换目录
$ cd out/
$ ls
lsieun/  manifest.txt  sample/

# 进行打包(第一种方式)
            ┌─── f: TheAgent.jar
         ┌──┴──┐
$ jar -cvfm TheAgent.jar manifest.txt lsieun/
          └─────────┬────────┘
                    └─── m: manifest.txt
# 进行打包(第二种方式)
                   ┌─── f: TheAgent.jar
          ┌────────┴────────┐
$ jar -cvmf manifest.txt TheAgent.jar lsieun/
         └───┬──┘
             └─── m: manifest.txt

输出信息:

$ jar -cvfm TheAgent.jar manifest.txt lsieun/
已添加清单
正在添加: lsieun/(输入 = 0) (输出 = 0)(存储了 0%)
正在添加: lsieun/agent/(输入 = 0) (输出 = 0)(存储了 0%)
正在添加: lsieun/agent/LoadTimeAgent.class(输入 = 503) (输出 = 309)(压缩了 38%)
正在添加: lsieun/instrument/(输入 = 0) (输出 = 0)(存储了 0%)
正在添加: lsieun/instrument/InfoTransformer.class(输入 = 1214) (输出 = 639)(压缩了 47%)

运行

在使用 java 命令时,我们可以通过使用 -javaagent 选项来使用 Agent Jar:

$ java -javaagent:TheAgent.jar sample.Program

部分输出结果:

...
ClassName: sample/Program
    ClassLoader: sun.misc.Launcher$AppClassLoader@18b4aac2
    ClassBeingRedefined: null
    ProtectionDomain: ProtectionDomain  (file:/D:/tmp/myAgent/java-agent-manual-01/out/ <no signer certificates>)
 sun.misc.Launcher$AppClassLoader@18b4aac2
 <no principals>
 java.security.Permissions@4aa298b7 (
 ("java.io.FilePermission" "\D:\tmp\myAgent\java-agent-manual-01\out\-" "read")
 ("java.lang.RuntimePermission" "exitVM")
)
...

另外,在使用 java 命令时,可以添加 -verbose:class 选项,它可以显示每个已加载类的信息。

$ java -verbose:class sample.Program

总结

本文内容总结如下:

  • 第一点,本文的主要目的是对 Java Agent 有一个整体的印象,因此不需要理解技术细节。
  • 第二点,Agent Jar 当中有三个重要组成部分:manifest、Agent Class 和 ClassFileTransformer。
  • 第三点,使用 java 命令加载 Agent Jar 时,需要使用 -javaagent 选项。