预期目标
我们的预期目标:借助于 JDK 内置的 ASM 打印出方法接收的参数,使用 Dynamic Instrumentation 的方式实现。
开发环境:
- JDK 版本:Java 8
- 编辑器:记事本(Windows)或
vi
(Linux)
代码目录结构:Code
java-agent-manual-03
└─── src
├─── attach
│ └─── VMAttach.java
├─── lsieun
│ ├─── agent
│ │ └─── DynamicAgent.java
│ ├─── asm
│ │ ├─── adapter
│ │ │ └─── MethodInfoAdapter.java
│ │ ├─── cst
│ │ │ └─── Const.java
│ │ └─── visitor
│ │ └─── MethodInfoVisitor.java
│ ├─── instrument
│ │ └─── ASMTransformer.java
│ └─── utils
│ └─── ParameterUtils.java
├─── manifest.txt
└─── sample
├─── HelloWorld.java
└─── Program.java
做一些准备工作(prepare03.sh
):
DIR=java-agent-manual-03
mkdir ${DIR} && cd ${DIR}
mkdir -p src/sample
touch src/sample/{HelloWorld.java,Program.java}
mkdir -p src/lsieun/{agent,asm,instrument,utils}
mkdir -p src/lsieun/asm/{adapter,cst,visitor}
touch src/lsieun/agent/DynamicAgent.java
touch src/lsieun/instrument/ASMTransformer.java
touch src/lsieun/asm/adapter/MethodInfoAdapter.java
touch src/lsieun/asm/cst/Const.java
touch src/lsieun/asm/visitor/MethodInfoVisitor.java
touch src/lsieun/utils/ParameterUtils.java
touch src/manifest.txt
mkdir -p src/attach
touch src/attach/VMAttach.java
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);
}
}
}
ASM 相关
在这个部分,我们要借助于 JDK 内置的 ASM 类库(jdk.internal.org.objectweb.asm
),来实现打印方法参数的功能。
ParameterUtils.java
package lsieun.utils;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
public class ParameterUtils {
private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(
() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
);
public static void printValueOnStack(boolean value) {
System.out.println(" " + value);
}
public static void printValueOnStack(byte value) {
System.out.println(" " + value);
}
public static void printValueOnStack(char value) {
System.out.println(" " + value);
}
public static void printValueOnStack(short value) {
System.out.println(" " + value);
}
public static void printValueOnStack(int value) {
System.out.println(" " + value);
}
public static void printValueOnStack(float value) {
System.out.println(" " + value);
}
public static void printValueOnStack(long value) {
System.out.println(" " + value);
}
public static void printValueOnStack(double value) {
System.out.println(" " + value);
}
public static void printValueOnStack(Object value) {
if (value == null) {
System.out.println(" " + value);
}
else if (value instanceof String) {
System.out.println(" " + value);
}
else if (value instanceof Date) {
System.out.println(" " + formatter.get().format(value));
}
else if (value instanceof char[]) {
System.out.println(" " + Arrays.toString((char[]) value));
}
else if (value instanceof Object[]) {
System.out.println(" " + Arrays.toString((Object[]) value));
}
else {
System.out.println(" " + value.getClass() + ": " + value.toString());
}
}
public static void printText(String str) {
System.out.println(str);
}
public static void printStackTrace() {
Exception ex = new Exception();
ex.printStackTrace(System.out);
}
}
Const.java
package lsieun.asm.cst;
import jdk.internal.org.objectweb.asm.Opcodes;
public class Const {
public static final int ASM_VERSION = Opcodes.ASM5;
}
MethodInfoAdapter.java
package lsieun.asm.adapter;
import jdk.internal.org.objectweb.asm.MethodVisitor;
import jdk.internal.org.objectweb.asm.Opcodes;
import jdk.internal.org.objectweb.asm.Type;
import lsieun.asm.cst.Const;
public class MethodInfoAdapter extends MethodVisitor {
private final String owner;
private final int methodAccess;
private final String methodName;
private final String methodDesc;
public MethodInfoAdapter(MethodVisitor methodVisitor, String owner,
int methodAccess, String methodName, String methodDesc) {
super(Const.ASM_VERSION, methodVisitor);
this.owner = owner;
this.methodAccess = methodAccess;
this.methodName = methodName;
this.methodDesc = methodDesc;
}
@Override
public void visitCode() {
if (mv != null) {
String line = String.format("Method Enter: %s.%s:%s", owner, methodName, methodDesc);
printMessage(line);
int slotIndex = (methodAccess & Opcodes.ACC_STATIC) != 0 ? 0 : 1;
Type methodType = Type.getMethodType(methodDesc);
Type[] argumentTypes = methodType.getArgumentTypes();
for (Type t : argumentTypes) {
int sort = t.getSort();
int size = t.getSize();
int opcode = t.getOpcode(Opcodes.ILOAD);
super.visitVarInsn(opcode, slotIndex);
if (sort >= Type.BOOLEAN && sort <= Type.DOUBLE) {
String desc = t.getDescriptor();
printValueOnStack("(" + desc + ")V");
}
else {
printValueOnStack("(Ljava/lang/Object;)V");
}
slotIndex += size;
}
}
super.visitCode();
}
private void printMessage(String str) {
super.visitLdcInsn(str);
super.visitMethodInsn(Opcodes.INVOKESTATIC, "lsieun/utils/ParameterUtils", "printText", "(Ljava/lang/String;)V", false);
}
private void printValueOnStack(String descriptor) {
super.visitMethodInsn(Opcodes.INVOKESTATIC, "lsieun/utils/ParameterUtils", "printValueOnStack", descriptor, false);
}
private void printStackTrace() {
super.visitMethodInsn(Opcodes.INVOKESTATIC, "lsieun/utils/ParameterUtils", "printStackTrace", "()V", false);
}
}
MethodInfoVisitor.java
package lsieun.asm.visitor;
import jdk.internal.org.objectweb.asm.ClassVisitor;
import jdk.internal.org.objectweb.asm.MethodVisitor;
import lsieun.asm.adapter.MethodInfoAdapter;
import lsieun.asm.cst.Const;
public class MethodInfoVisitor extends ClassVisitor {
private String owner;
public MethodInfoVisitor(ClassVisitor classVisitor) {
super(Const.ASM_VERSION, classVisitor);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
this.owner = name;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
if (mv != null && !name.equals("<init>") && !name.equals("<clinit>")) {
mv = new MethodInfoAdapter(mv, owner, access, name, descriptor);
}
return mv;
}
}
Agent Jar
manifest.txt
Agent-Class: lsieun.agent.DynamicAgent
Can-Retransform-Classes: true
注意:在 manifest.txt
文件的结尾处有一个空行。(make sure the last line in the file is a blank line)
DynamicAgent.java
package lsieun.agent;
import lsieun.instrument.ASMTransformer;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
public class DynamicAgent {
public static void agentmain(String agentArgs, Instrumentation inst) {
ClassFileTransformer transformer = new ASMTransformer();
try {
inst.addTransformer(transformer, true);
Class<?> targetClass = Class.forName("sample.HelloWorld");
inst.retransformClasses(targetClass);
} catch (Exception ex) {
ex.printStackTrace();
}
finally {
inst.removeTransformer(transformer);
}
}
}
ASMTransformer.java
package lsieun.instrument;
import jdk.internal.org.objectweb.asm.ClassReader;
import jdk.internal.org.objectweb.asm.ClassVisitor;
import jdk.internal.org.objectweb.asm.ClassWriter;
import lsieun.asm.visitor.MethodInfoVisitor;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class ASMTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
if (className == null) return null;
if (className.startsWith("java")) return null;
if (className.startsWith("javax")) return null;
if (className.startsWith("jdk")) return null;
if (className.startsWith("sun")) return null;
if (className.startsWith("org")) return null;
if (className.startsWith("com")) return null;
if (className.startsWith("lsieun")) return null;
System.out.println("candidate className: " + className);
if (className.equals("sample/HelloWorld")) {
ClassReader cr = new ClassReader(classfileBuffer);
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor cv = new MethodInfoVisitor(cw);
int parsingOptions = 0;
cr.accept(cv, parsingOptions);
return cw.toByteArray();
}
return null;
}
}
JVM Attach
VMAttach.java
在下面的代码中,我们要用到 com.sun.tools.attach
里定义的类,因此编译的时候需要用到 tools.jar
文件。
package attach;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
public class VMAttach {
public static void main(String[] args) throws Exception {
String agent = "TheAgent.jar";
System.out.println("Agent Path: " + agent);
List<VirtualMachineDescriptor> vmds = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : vmds) {
if (vmd.displayName().equals("sample.Program")) {
VirtualMachine vm = VirtualMachine.attach(vmd.id());
System.out.println("Load Agent");
vm.loadAgent(agent);
System.out.println("Detach");
vm.detach();
}
}
}
}
编译和运行
编译
编译 Application:
# 切换目录
$ cd java-agent-manual-03/
# 输出目录
$ mkdir out
# 编译
$ javac src/sample/*.java -d out/
编译和生成 Agent Jar:
# 切换目录
$ cd java-agent-manual-03/
# 找到所有 .java 文件
$ find ./src/lsieun/ -name "*.java" > sources.txt
$ cat sources.txt
./src/lsieun/agent/DynamicAgent.java
./src/lsieun/asm/adapter/MethodInfoAdapter.java
./src/lsieun/asm/cst/Const.java
./src/lsieun/asm/visitor/MethodInfoVisitor.java
./src/lsieun/instrument/ASMTransformer.java
./src/lsieun/utils/ParameterUtils.java
# 错误的编译
$ javac -d out/ @sources.txt
# 正确的编译
$ javac -XDignore.symbol.file -d out/ @sources.txt
# 复制 manifest.txt 文件
$ cp src/manifest.txt out/
# 切换目录
$ cd out/
$ ls
lsieun/ manifest.txt sample/
# 进行打包
$ jar -cvfm TheAgent.jar manifest.txt lsieun/
编译 VM Attach:
# 切换目录
$ cd java-agent-manual-03/
# 编译 VMAttach.java(Windows)
$ javac -cp "%JAVA_HOME%/lib/tools.jar";. -d out/ src/attach/VMAttach.java
# 编译 VMAttach.java(Linux)
$ javac -cp "${JAVA_HOME}/lib/tools.jar":. -d out/ src/attach/VMAttach.java
# 编译 VMAttach.java(MINGW64)
$ javac -cp "${JAVA_HOME}/lib/tools.jar"\;. -d out/ src/attach/VMAttach.java
运行
运行:
# 切换目录
$ cd java-agent-manual-03/
$ cd out/
# 编译 VMAttach.java(Windows)
$ java -cp "%JAVA_HOME%/lib/tools.jar";. attach.VMAttach
# 编译 VMAttach.java(Linux)
$ java -cp "${JAVA_HOME}/lib/tools.jar":. attach.VMAttach
# 编译 VMAttach.java(MINGW64)
$ java -cp "${JAVA_HOME}/lib/tools.jar"\;. attach.VMAttach
总结
本文内容总结如下:
- 第一点,本文的主要目的是对 Java Agent 有一个整体的印象,因此不需要理解技术细节。
- 第二点,Java Agent 的 Jar 包当中有三个重要组成部分:manifest、Agent Class 和 ClassFileTransformer。
- 第三点,当使用
javac
命令编译时,如果在程序当中使用到了jdk.*
或sun.*
当中的类,要添加-XDignore.symbol.file
选项。 - 第四点,当运行 Dynamic Instrumentation 时,需要在
CLASSPATH
当中引用JAVA_HOME/lib/tools.jar
。