预期目标
我们的预期目标:打印方法接收的参数值和返回值,借助于 Maven 管理依赖和进行编译,避免手工打 Jar 包的麻烦。
本文内容虽然很多,但是我们静下心来想一想,它有一个简单的目标:生成一个 Agent Jar。因此,在过程当中的内容细节,都是为 TheAgent.jar
做一定的铺垫。
新建一个 Maven 项目,取名为 java-agent-maven
,代码目录结构:Code
java-agent-maven
├─── pom.xml
└─── src
└─── main
└─── java
├─── lsieun
│ ├─── agent
│ │ ├─── DynamicAgent.java
│ │ └─── LoadTimeAgent.java
│ ├─── asm
│ │ ├─── adapter
│ │ │ └─── PrintMethodInfoStdAdapter.java
│ │ ├─── cst
│ │ │ └─── Const.java
│ │ └─── visitor
│ │ ├─── MethodInfo.java
│ │ └─── PrintMethodInfoVisitor.java
│ ├─── instrument
│ │ └─── ASMTransformer.java
│ └─── Main.java
├─── run
│ ├─── DynamicInstrumentation.java
│ ├─── LoadTimeInstrumentation.java
│ └─── PathManager.java
└─── sample
├─── HelloWorld.java
└─── Program.java
问题:为什么没有 manifest.txt
文件呢?
回答:因为 META-INF/MANIFEST.MF
的信息由 pom.xml
文件中 maven-jar-plugin
提供。
生成 Jar 文件,我们有三种选择:
- 第一种,
maven-jar-plugin
+maven-dependency-plugin
- 第二种,
maven-assembly-plugin
- 第三种,
maven-shade-plugin
pom.xml
在 Maven 项目当中,一个非常重要的配置就是 pom.xml
文件。
properties
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<asm.version>9.0</asm.version>
</properties>
dependencies
<dependencies>
</dependencies>
ASM
在这里不再使用 JDK 内置的 ASM 类库,因为内置的版本比较低。
我们想使用更高的 ASM 版本,也就能够支持更高版本 .class
文件操作。
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>${asm.version}</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-util</artifactId>
<version>${asm.version}</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-commons</artifactId>
<version>${asm.version}</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-tree</artifactId>
<version>${asm.version}</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-analysis</artifactId>
<version>${asm.version}</version>
</dependency>
tools.jar
在 tools.jar
文件当中,包含了 com.sun.tools.attach.VirtualMachine
类,会在 DynamicInstrumentation
类当中用到。
在 Java 9 之后的版本,引入了模块化系统,com.sun.tools.attach
包位于 jdk.attach
模块。
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>8</version>
<scope>system</scope>
<systemPath>${env.JAVA_HOME}/lib/tools.jar</systemPath>
</dependency>
plugins
<build>
<finalName>TheAgent</finalName>
<plugins>
</plugins>
</build>
compiler-plugin
下面的 maven-compiler-plugin
插件主要关注 compilerArgs
下的三个参数:
-g
: 生成所有调试信息-parameters
: 生成 属性-XDignore.symbol.file
: 在编译过程中,进行 link 时,不使用lib/ct.sym
,而是直接使用rt.jar
文件。
<!-- Java Compiler -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<fork>true</fork>
<compilerArgs>
<arg>-g</arg>
<arg>-parameters</arg>
<arg>-XDignore.symbol.file</arg>
</compilerArgs>
</configuration>
</plugin>
jar-plugin
下面的maven-jar-plugin
插件主要做以下两件事情:
- 第一,设置
META-INF/MANIFEST.MF
中的信息。 - 第二,确定在 jar 包当中包含哪些文件。
关于 <archive>
的配置,可以参考 Apache Maven Archiver。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifest>
<mainClass>lsieun.Main</mainClass>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
<addDefaultImplementationEntries>false</addDefaultImplementationEntries>
<addDefaultSpecificationEntries>false</addDefaultSpecificationEntries>
</manifest>
<manifestEntries>
<Premain-Class>lsieun.agent.LoadTimeAgent</Premain-Class>
<Agent-Class>lsieun.agent.DynamicAgent</Agent-Class>
<Launcher-Agent-Class>lsieun.agent.LauncherAgent</Launcher-Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>
</manifestEntries>
<addMavenDescriptor>false</addMavenDescriptor>
</archive>
<includes>
<include>lsieun/**</include>
</includes>
</configuration>
</plugin>
如果我们想使用的配置文件,可以使用 manifestFile
:
<configuration>
<archive>
<manifestFile>src/main/resources/manifest.mf</manifestFile>
</archive>
</configuration>
dependency-plugin
下面的 maven-dependency-plugin
插件主要目的:将依赖的 jar 包复制到 lib
目录下。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<id>lib-copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<excludeArtifactIds>tools</excludeArtifactIds>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
<overWriteReleases>false</overWriteReleases>
<overWriteSnapshots>false</overWriteSnapshots>
<overWriteIfNewer>true</overWriteIfNewer>
</configuration>
</execution>
</executions>
</plugin>
assembly-plugin
下面的 maven-assembly-plugin
插件主要目的:生成一个 jar 文件,它包含了依赖的 jar 包。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifest>
<mainClass>lsieun.Main</mainClass>
<addDefaultEntries>false</addDefaultEntries>
</manifest>
<manifestEntries>
<Premain-Class>lsieun.agent.LoadTimeAgent</Premain-Class>
<Agent-Class>lsieun.agent.DynamicAgent</Agent-Class>
<Launcher-Agent-Class>lsieun.agent.LauncherAgent</Launcher-Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>
</manifestEntries>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
shade-plugin
下面的 maven-shade-plugin
插件主要目的:生成一个 jar 文件,它包含了依赖的 jar 包,可以进行精简。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<configuration>
<minimizeJar>true</minimizeJar>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>run/*</exclude>
<exclude>sample/*</exclude>
</excludes>
</filter>
</filters>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Main-Class>lsieun.Main</Main-Class>
<Premain-Class>lsieun.agent.LoadTimeAgent</Premain-Class>
<Agent-Class>lsieun.agent.DynamicAgent</Agent-Class>
<Launcher-Agent-Class>lsieun.agent.LauncherAgent</Launcher-Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>
</manifestEntries>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
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 相关
Const.java
package lsieun.asm.cst;
import org.objectweb.asm.Opcodes;
public class Const {
public static final int ASM_VERSION = Opcodes.ASM9;
}
PrintMethodInfoStdAdapter.java
package lsieun.asm.adapter;
import lsieun.asm.cst.Const;
import lsieun.asm.visitor.MethodInfo;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import java.util.Objects;
import java.util.Set;
public class PrintMethodInfoStdAdapter extends MethodVisitor implements Opcodes {
private final String owner;
private final int methodAccess;
private final String methodName;
private final String methodDesc;
private final Set<MethodInfo> flags;
public PrintMethodInfoStdAdapter(MethodVisitor methodVisitor,
String owner, int methodAccess, String methodName, String methodDesc,
Set<MethodInfo> flags) {
super(Const.ASM_VERSION, methodVisitor);
Objects.requireNonNull(flags);
this.owner = owner;
this.methodAccess = methodAccess;
this.methodName = methodName;
this.methodDesc = methodDesc;
this.flags = flags;
}
@Override
public void visitCode() {
if (mv != null) {
if (flags.contains(MethodInfo.NAME_AND_DESC)) {
String line = String.format("Method Enter: %s.%s:%s", owner, methodName, methodDesc);
printMessage(line);
}
if (flags.contains(MethodInfo.PARAMETER_VALUES)) {
int slotIndex = (methodAccess & Opcodes.ACC_STATIC) != 0 ? 0 : 1;
Type methodType = Type.getMethodType(methodDesc);
Type[] argumentTypes = methodType.getArgumentTypes();
for (Type t : argumentTypes) {
printParameter(slotIndex, t);
int size = t.getSize();
slotIndex += size;
}
}
if (flags.contains(MethodInfo.CLASSLOADER)) {
printClassLoader();
}
if (flags.contains(MethodInfo.THREAD_INFO)) {
printThreadInfo();
}
if (flags.contains(MethodInfo.STACK_TRACE)) {
printStackTrace();
}
}
super.visitCode();
}
@Override
public void visitInsn(int opcode) {
if (flags.contains(MethodInfo.RETURN_VALUE)) {
Type t = Type.getMethodType(methodDesc);
Type returnType = t.getReturnType();
if (opcode == Opcodes.ATHROW) {
String line = String.format("Method throws Exception: %s.%s:%s", owner, methodName, methodDesc);
printMessage(line);
String message = " abnormal return";
printMessage(message);
printMessage("=================================================================================");
}
else if (opcode == Opcodes.RETURN) {
String line = String.format("Method Return: %s.%s:%s", owner, methodName, methodDesc);
printMessage(line);
String message = " return void";
printMessage(message);
printMessage("=================================================================================");
}
else if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.ARETURN) {
String line = String.format("Method Return: %s.%s:%s", owner, methodName, methodDesc);
printMessage(line);
printReturnValue(returnType);
printMessage("=================================================================================");
}
else {
assert false : "should not be here";
}
}
super.visitInsn(opcode);
}
private void printMessage(String message) {
super.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
super.visitLdcInsn(message);
super.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
private void printParameter(int slotIndex, Type t) {
super.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
super.visitTypeInsn(NEW, "java/lang/StringBuilder");
super.visitInsn(DUP);
super.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
super.visitLdcInsn(" ");
super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
if (slotIndex >= 0 && slotIndex <= 5) {
super.visitInsn(ICONST_0 + slotIndex);
}
else {
super.visitIntInsn(BIPUSH, slotIndex);
}
super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(I)Ljava/lang/StringBuilder;", false);
super.visitLdcInsn(": ");
super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
int opcode = t.getOpcode(Opcodes.ILOAD);
super.visitVarInsn(opcode, slotIndex);
int sort = t.getSort();
String descriptor;
if (sort == Type.SHORT) {
descriptor = "(I)Ljava/lang/StringBuilder;";
}
else if (sort >= Type.BOOLEAN && sort <= Type.DOUBLE) {
descriptor = "(" + t.getDescriptor() + ")Ljava/lang/StringBuilder;";
}
else {
descriptor = "(Ljava/lang/Object;)Ljava/lang/StringBuilder;";
}
super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", descriptor, false);
super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
super.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
private void printThreadInfo() {
super.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
super.visitTypeInsn(NEW, "java/lang/StringBuilder");
super.visitInsn(DUP);
super.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
super.visitLdcInsn("Thread Id: ");
super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
super.visitMethodInsn(INVOKESTATIC, "java/lang/Thread", "currentThread", "()Ljava/lang/Thread;", false);
super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Thread", "getName", "()Ljava/lang/String;", false);
super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
super.visitLdcInsn("@");
super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
super.visitMethodInsn(INVOKESTATIC, "java/lang/Thread", "currentThread", "()Ljava/lang/Thread;", false);
super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Thread", "getId", "()J", false);
super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
super.visitLdcInsn("(");
super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
super.visitMethodInsn(INVOKESTATIC, "java/lang/Thread", "currentThread", "()Ljava/lang/Thread;", false);
super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Thread", "isDaemon", "()Z", false);
super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Z)Ljava/lang/StringBuilder;", false);
super.visitLdcInsn(")");
super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
super.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
private void printClassLoader() {
super.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
super.visitTypeInsn(NEW, "java/lang/StringBuilder");
super.visitInsn(DUP);
super.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
super.visitLdcInsn("ClassLoader: ");
super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
super.visitLdcInsn(Type.getObjectType(owner));
super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getClassLoader", "()Ljava/lang/ClassLoader;", false);
super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/Object;)Ljava/lang/StringBuilder;", false);
super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
super.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
private void printStackTrace() {
super.visitTypeInsn(NEW, "java/lang/Exception");
super.visitInsn(DUP);
super.visitTypeInsn(NEW, "java/lang/StringBuilder");
super.visitInsn(DUP);
super.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
super.visitLdcInsn("Exception from ");
super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
super.visitLdcInsn(Type.getObjectType(owner));
super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getName", "()Ljava/lang/String;", false);
super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
super.visitMethodInsn(INVOKESPECIAL, "java/lang/Exception", "<init>", "(Ljava/lang/String;)V", false);
super.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Exception", "printStackTrace", "(Ljava/io/PrintStream;)V", false);
}
private void printReturnValue(Type returnType) {
int size = returnType.getSize();
if (size == 1) {
super.visitInsn(DUP);
}
else if (size == 2) {
super.visitInsn(DUP2);
}
else {
assert false : "should not be here";
}
printValueOnStack(returnType);
}
private void printValueOnStack(Type t) {
super.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
int size = t.getSize();
if (size == 1) {
super.visitInsn(SWAP);
}
else if (size == 2) {
super.visitInsn(DUP_X2);
super.visitInsn(POP);
}
else {
assert false : "should not be here";
}
super.visitTypeInsn(NEW, "java/lang/StringBuilder");
super.visitInsn(DUP);
super.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
super.visitLdcInsn(" ");
super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
if (size == 1) {
super.visitInsn(SWAP);
}
else {
super.visitInsn(DUP_X2);
super.visitInsn(POP);
}
int sort = t.getSort();
String descriptor;
if (sort == Type.SHORT) {
descriptor = "(I)Ljava/lang/StringBuilder;";
}
else if (sort >= Type.BOOLEAN && sort <= Type.DOUBLE) {
descriptor = "(" + t.getDescriptor() + ")Ljava/lang/StringBuilder;";
}
else {
descriptor = "(Ljava/lang/Object;)Ljava/lang/StringBuilder;";
}
super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", descriptor, false);
super.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
super.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
}
MethodInfo.java
package lsieun.asm.visitor;
import java.util.EnumSet;
public enum MethodInfo {
NAME_AND_DESC,
PARAMETER_VALUES,
RETURN_VALUE,
CLASSLOADER,
STACK_TRACE,
THREAD_INFO;
public static final EnumSet<MethodInfo> ALL = EnumSet.allOf(MethodInfo.class);
}
PrintMethodInfoVisitor.java
package lsieun.asm.visitor;
import lsieun.asm.adapter.PrintMethodInfoStdAdapter;
import lsieun.asm.cst.Const;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import java.util.Set;
public class PrintMethodInfoVisitor extends ClassVisitor {
private static final String ALL = "*";
private String owner;
private final String methodName;
private final String methodDesc;
private final Set<MethodInfo> flags;
public PrintMethodInfoVisitor(ClassVisitor classVisitor, Set<MethodInfo> flags) {
this(classVisitor, ALL, ALL, flags);
}
public PrintMethodInfoVisitor(ClassVisitor classVisitor, String methodName, String methodDesc, Set<MethodInfo> flags) {
super(Const.ASM_VERSION, classVisitor);
this.methodName = methodName;
this.methodDesc = methodDesc;
this.flags = flags;
}
@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) return mv;
boolean isAbstract = (access & Opcodes.ACC_ABSTRACT) != 0;
boolean isNative = (access & Opcodes.ACC_NATIVE) != 0;
if (isAbstract || isNative) return mv;
if (name.equals("<init>") || name.equals("<clinit>")) return mv;
boolean process = false;
if (ALL.equals(methodName) && ALL.equals(methodDesc)) {
process = true;
}
else if (name.equals(methodName) && ALL.equals(methodDesc)) {
process = true;
}
else if (name.equals(methodName) && descriptor.equals(methodDesc)) {
process = true;
}
if (process) {
String line = String.format("---> %s.%s:%s", owner, name, descriptor);
System.out.println(line);
mv = new PrintMethodInfoStdAdapter(mv, owner, access, name, descriptor, flags);
}
return mv;
}
}
Agent Jar
LoadTimeAgent.java
package lsieun.agent;
import lsieun.instrument.ASMTransformer;
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());
System.out.println("Can-Redefine-Classes: " + inst.isRedefineClassesSupported());
System.out.println("Can-Retransform-Classes: " + inst.isRetransformClassesSupported());
System.out.println("Can-Set-Native-Method-Prefix: " + inst.isNativeMethodPrefixSupported());
System.out.println("========= ========= =========");
ClassFileTransformer transformer = new ASMTransformer("sample/HelloWorld");
inst.addTransformer(transformer, false);
}
}
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) {
System.out.println("Agent-Class: " + DynamicAgent.class.getName());
System.out.println("Can-Redefine-Classes: " + inst.isRedefineClassesSupported());
System.out.println("Can-Retransform-Classes: " + inst.isRetransformClassesSupported());
System.out.println("Can-Set-Native-Method-Prefix: " + inst.isNativeMethodPrefixSupported());
System.out.println("========= ========= =========");
ClassFileTransformer transformer = new ASMTransformer("sample/HelloWorld");
inst.addTransformer(transformer, true);
try {
Class<?> targetClass = Class.forName("sample.HelloWorld");
inst.retransformClasses(targetClass);
} catch (Exception ex) {
ex.printStackTrace();
}
finally {
inst.removeTransformer(transformer);
}
}
}
LauncherAgent.java
package lsieun.agent;
import java.lang.instrument.Instrumentation;
public class LauncherAgent {
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("Launcher-Agent-Class: " + LauncherAgent.class.getName());
System.out.println("Can-Redefine-Classes: " + inst.isRedefineClassesSupported());
System.out.println("Can-Retransform-Classes: " + inst.isRetransformClassesSupported());
System.out.println("Can-Set-Native-Method-Prefix: " + inst.isNativeMethodPrefixSupported());
System.out.println("========= ========= =========");
}
}
ASMTransformer.java
package lsieun.instrument;
import lsieun.asm.visitor.*;
import org.objectweb.asm.*;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.*;
public class ASMTransformer implements ClassFileTransformer {
public static final List<String> ignoredPackages = Arrays.asList("com/", "com/sun/", "java/", "javax/", "jdk/", "lsieun/", "org/", "sun/");
private final String internalName;
public ASMTransformer(String internalName) {
Objects.requireNonNull(internalName);
this.internalName = internalName.replace(".", "/");
}
@Override
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
if (className == null) return null;
for (String name : ignoredPackages) {
if (className.startsWith(name)) {
return null;
}
}
System.out.println("candidate class: " + className);
if (className.equals(internalName)) {
System.out.println("transform class: " + className);
ClassReader cr = new ClassReader(classfileBuffer);
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
Set<MethodInfo> flags = EnumSet.of(
MethodInfo.NAME_AND_DESC,
MethodInfo.PARAMETER_VALUES,
MethodInfo.RETURN_VALUE);
ClassVisitor cv = new PrintMethodInfoVisitor(cw, flags);
int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
cr.accept(cv, parsingOptions);
return cw.toByteArray();
}
return null;
}
}
Main.java
package lsieun;
public class Main {
public static void main(String[] args) {
System.out.println("This is a Java Agent Jar");
}
}
Run
LoadTimeInstrumentation.java
package run;
import java.util.Formatter;
public class LoadTimeInstrumentation {
public static void main(String[] args) {
usage();
}
public static void usage() {
String jarPath = PathManager.getJarPath();
StringBuilder sb = new StringBuilder();
Formatter fm = new Formatter(sb);
fm.format("Usage:%n");
fm.format(" java -javaagent:/path/to/TheAgent.jar sample.Program%n");
fm.format("Example:%n");
fm.format(" java -cp ./target/classes/ -javaagent:./target/TheAgent.jar sample.Program%n");
fm.format(" java -cp ./target/classes/ -javaagent:%s sample.Program", jarPath);
String result = sb.toString();
System.out.println(result);
}
}
DynamicInstrumentation.java
package run;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
public class DynamicInstrumentation {
public static void main(String[] args) throws Exception {
String agent = PathManager.getJarPath();
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());
vm.getSystemProperties();
System.out.println("Load Agent");
vm.loadAgent(agent);
System.out.println("Detach");
vm.detach();
}
}
}
}
PathManager.java
package run;
import java.io.File;
import java.net.URISyntaxException;
public class PathManager {
public static String getJarPath() {
String filepath = null;
try {
filepath = new File(PathManager.class.getProtectionDomain().getCodeSource().getLocation().toURI()).getPath();
} catch (URISyntaxException ex) {
ex.printStackTrace();
}
if (filepath == null || !filepath.endsWith(".jar")) {
filepath = System.getProperty("user.dir") + File.separator + "target/TheAgent.jar";
}
return filepath.replace(File.separator, "/");
}
}
生成 Jar 包和运行
生成 Jar 包:
mvn clean package
上述命令执行完成之后,会在 target
文件夹下生成 TheAgent.jar
,其内容如下:
TheAgent.jar
├─── META-INF/MANIFEST.MF
├─── lsieun/agent/DynamicAgent.class
├─── lsieun/agent/LoadTimeAgent.class
├─── lsieun/asm/adapter/PrintMethodInfoStdAdapter.class
├─── lsieun/asm/cst/Const.class
├─── lsieun/asm/visitor/MethodInfo.class
├─── lsieun/asm/visitor/PrintMethodInfoVisitor.class
├─── lsieun/instrument/ASMTransformer.class
└─── lsieun/Main.class
运行 Load-Time Instrumentation:
# 相对路径
$ java -cp ./target/classes/ -javaagent:./target/TheAgent.jar sample.Program
# 绝对路径(\\)
$ java -cp ./target/classes/ -javaagent:D:\\git-repo\\java-agent-maven\\target\\TheAgent.jar sample.Program
# 绝对路径(/)
$ java -cp ./target/classes/ -javaagent:D:/git-repo/java-agent-maven/target/TheAgent.jar sample.Program
运行 Dynamic Instrumentation:
# Windows
$ java -cp "%JAVA_HOME%/lib/tools.jar";./target/classes/ run.DynamicInstrumentation
# Linux
$ java -cp "${JAVA_HOME}/lib/tools.jar":./target/classes/ run.DynamicInstrumentation
# MINGW64
$ java -cp "${JAVA_HOME}/lib/tools.jar"\;./target/classes/ run.DynamicInstrumentation
如果是 Java 9 及以上的版本,不需要引用 tools.jar
文件,可以直接运行:
$ java -cp ./target/classes/ run.DynamicInstrumentation
总结
本文内容总结如下:
- 第一点,使用 Maven 会提供很大的方便,但是 Agent Jar 的核心三要素没有发生变化,包括 manifest、Agent Class 和 ClassFileTransformer,三者缺一不可。
- 第二点,使用 ASM 修改字节码(bytecode)的内容是属于 Java Agent 的“辅助部分”。如果我们熟悉其它的字节码操作类库(例如,Javassist、ByteBuddy),可以将 ASM 替换掉。
- 第三点,细节之处的把握。
- 在
pom.xml
文件中,对${env.JAVA_HOME}/lib/tools.jar
进行了依赖,是因为我们用到com.sun.tools.attach.VirtualMachine
类。 - 在
pom.xml
文件中,maven-jar-plugin
部分提供的与 manifest 相关的信息,会转换到META-INF/MANIFEST.MF
文件中去。
- 在