目录
1. 预期目标
- 使用java agent对运行的程序进行监控,当某个方法被调用时,能够打印类名、方法名、入参、返回值、方法耗时
2. 新建一个工程
- 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>org.example</groupId>
<artifactId>my-agent</artifactId>
<version>1.0-SNAPSHOT</version>
<name>my-agent</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-commons</artifactId>
<version>9.3</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
3. MANIFEST.MF
Manifest-Version: 1.0
Premain-Class: org.example.agent.MyAgent
Agent-Class: org.example.agent.MyAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Can-Set-Native-Method-Prefix: true
4. premain方式
package org.example.agent;
import java.lang.instrument.Instrumentation;
public class MyAgent {
/**
* jvm参数形式调用premain方法,在java程序的main方法执行之前执行
*/
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("[MyAgent][premain]: " + agentArgs);
System.out.println("Premain-Class: " + MyAgent.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("========= ========= =========");
inst.addTransformer(new MonitorTransformer());
}
}
4.1 监控类
- 这个类在ASM字节码插桩:打印方法名、入参、返回值、方法耗时中做过介绍,这里稍微修改
package org.example.agent;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;
/**
* MonitorTransformer
*
* @author Fisher
* @date 2022/9/15 11:59
**/
public class MonitorTransformer implements ClassFileTransformer, Opcodes {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
// 需要排除 JDK 自带的类
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/example")) {
ClassReader cr = new ClassReader(classfileBuffer);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
cr.accept(new MyClassVisitor(ASM9, cw, className), ClassReader.EXPAND_FRAMES);
return cw.toByteArray();
}
return null;
}
static class MyClassVisitor extends ClassVisitor {
String className;
protected MyClassVisitor(int api, ClassVisitor classVisitor, String className) {
super(api, classVisitor);
this.className = className;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
if (methodVisitor != null) {
// 排除抽象方法和本地方法
boolean isAbstractMethod = (access & ACC_ABSTRACT) != 0;
boolean isNativeMethod = (access & ACC_NATIVE) != 0;
if (!isAbstractMethod && !isNativeMethod) {
// 排除构造方法
if (!"<init>".equals(name)) {
methodVisitor = new MyMethodVisitor(api, methodVisitor, access, name, descriptor, className);
}
}
}
return methodVisitor;
}
}
static class MyMethodVisitor extends AdviceAdapter {
String str = ",方法名->" + super.getName() + ",方法描述符->" + super.methodDesc;
// 开始时间在局部变量表中的位置
int start = 0;
protected MyMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor,
String className) {
super(api, methodVisitor, access, name, descriptor);
str = "类名->" + className + str;
}
@Override
protected void onMethodEnter() {
// 记录开始时间
super.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
// 在局部变量表中nextLocal位置存放long类型的数值,nextLocal表示当前已存数据的下一个位置的索引
// 下面执行完visitVarInsn(LSTORE, start)后,nextLocal会根据存入的数据类型长度,后移一位或两位
start = nextLocal;
// 将栈顶的数值放入局部变量表中start位置
super.visitVarInsn(LSTORE, start);
// 进入方法时,先打印一句话 Enter: xxx
printText("Enter: " + str);
// 取出方法所有的入参类型
Type[] argumentTypes = getArgumentTypes();
for (int i = 0; i < argumentTypes.length; i++) {
Type argumentType = argumentTypes[i];
// 将方法的入参从局部变量表中取出,压入到操作数栈中
loadArg(i);
// 对操作数栈顶的数据按照argumentType类型进行包装,并用包装好的值替换原来栈顶的这个数值,而且数据类型也是一致的
box(argumentType);
// 打印操作数栈顶的这个值,就实现了对方法入参的循环打印
printObject("入参类型:");
}
}
@Override
protected void onMethodExit(int opcode) {
// 退出方法时,打印一句话 Exit: xxx
printText("Exit: " + str);
// throw 与 return 指令没有返回值,这里手动将希望打印到控制台的字符串压入到操作数栈顶
if (opcode == ATHROW) {
super.visitLdcInsn("有异常抛出了");
} else if (opcode == RETURN) {
super.visitLdcInsn("void方法,没有返回值");
} else if (opcode == ARETURN) {
// 复制操作数栈顶的1个数值,并将复制结果压入操作数栈顶,此时操作数栈上有2个连续相同的数值
// 复制的目的是,多出来的这个数值用来打印到控制台,原来栈顶的数值不受影响
dup();
} else if (opcode == LRETURN || opcode == DRETURN) {
// 因为double和long类型(64bit)占2个slot,所以要复制操作数栈顶的2个数值,并将其压入操作数栈顶
dup2();
// 对栈顶的数据按照返回值类型进行包装,并用包装好的值替换原来栈顶的这个数值
// double类型会用Double.valueOf()进行包装,long类型会用Long.valueOf()进行包装
box(getReturnType());
} else {
dup();
// 这里排除上面几种返回值类型,这里的opcode应该是 FRETURN 和 IRETURN
// 对相应类型的数据进行Float.valueOf()或者Integer.valueOf()包装
box(getReturnType());
}
// 因为这里打印时,需要参数是Object类型,所以上面的2个box(getReturnType())必须有,目的是将基本数据类型转成包装类
// 否则打印时,传的是基本数据类型,不是Object一定会报错
// 前面2个if没有返回值,所以不需要按照返回值数据类型进行包装,直接传入String类型数据给printObject方法进行打印
// 第3个if是Object类型返回值,复制一份压到栈顶即可,不需要再包装了
printObject("返回值类型:");
// 打印耗时
printSpendTime();
}
private void printText(String str) {
// 将str从常量池中取出,压入操作数栈顶
super.visitLdcInsn(str);
// 从操作数栈顶取出一个数据,作为入参调用PrintUtils的public static void printText(String str)方法
super.visitMethodInsn(INVOKESTATIC, Type.getInternalName(PrintUtils.class), "printText",
"(Ljava/lang/String;)V", false);
}
private void printObject(String name) {
// 将name压入栈顶
super.visitLdcInsn(name);
// printObject方法入参是name和value,从栈顶取参数时,从后往前输入
// 所以要先拿到Object类型的value再拿String类型的name,但此时栈顶是name,name下面是value的包装类
// 所以要调用swap方法,将栈顶最顶端的两个数值互换(数值不能是long或double类型)
swap();
// 从操作数栈顶取出一个数据,作为入参调用PrintUtils的public static void printObject(String name, Object value)方法
super.visitMethodInsn(INVOKESTATIC, Type.getInternalName(PrintUtils.class), "printObject",
"(Ljava/lang/String;Ljava/lang/Object;)V", false);
}
private void printSpendTime() {
// 方法名压入栈顶
super.visitLdcInsn(super.getName());
// 将开始时间从局部变量表start位置压入栈顶
super.visitVarInsn(LLOAD, start);
super.visitMethodInsn(INVOKESTATIC, Type.getInternalName(PrintUtils.class), "printSpendTime",
"(Ljava/lang/String;J)V", false);
}
}
}
4.2 打印类
package org.example.agent;
import java.util.Arrays;
public class PrintUtils {
public static void printText(String str) {
System.out.println(str);
}
public static void printObject(String name, Object value) {
if (value == null) {
System.out.println("null");
} else {
if (value instanceof Object[]) {
System.out
.println(name + value.getClass().getSimpleName() + ",参数值:" + Arrays.toString((Object[])value));
} else {
System.out.println(name + value.getClass().getSimpleName() + ",参数值:" + value);
}
}
}
public static void printSpendTime(String methodName, long startTime) {
System.out.println(methodName + " 耗时:" + (System.currentTimeMillis() - startTime) + " ms");
System.out.println("*************************************************");
}
}
4.3 打包
- 使用maven插件,点击assembly:assembly
4.4 创建一个springboot工程
- 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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>maven-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>maven-demo</name>
<description>maven-demo</description>
<properties>
<java.version>8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
<configuration>
<includeSystemScope>true</includeSystemScope>
<mainClass>com.example.mavendemo.MavenDemoApplication</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
- 启动类
package com.example.mavendemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MavenDemoApplication {
public static void main(String[] args) {
System.out.println("hello world!");
SpringApplication.run(MavenDemoApplication.class, args);
System.out.println("hahaha");
}
}
- 测试接口
package com.example.mavendemo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* HelloController
*
* @author Fisher
* @date 2022/9/15 14:59
**/
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(String message) {
return message;
}
}
4.5 验证
- 在idea的VM options中添加-javaagent:xxx.jar参数
- 启动程序,查看打印
- 调用测试接口
http://localhost:8080/hello?message=fisher
,查看打印
5. agentmain方式
5.1 指定程序名称方式
- 方法参考Java Agent入门教程
package org.example.attach;
import java.io.IOException;
import java.util.List;
import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
public class Attacher {
public static void main(String[] args)
throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
// 查询所有运行的java程序
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor virtualMachineDescriptor : list) {
String displayName = virtualMachineDescriptor.displayName();
if (displayName.equals("maven-demo-0.0.1-SNAPSHOT.jar")) {
System.out.println(virtualMachineDescriptor.id());
VirtualMachine attach = VirtualMachine.attach(virtualMachineDescriptor);
attach.loadAgent("/Users/fisher/Documents/code/gitee/myAgent/target/my-agent-1.0-SNAPSHOT-jar-with-dependencies.jar",
"Hello VirtualMachineDescriptor");
attach.detach();
}
}
}
}
5.2 agentmain方法
- 如果要修改的类已经在jvm加载完成,则需要使用retransformClasses(Class<?>… classes)添加要修改的类,让对应的类可以重新转换
- 这里只添加MyAgent中agentmain方法即可,其他类中的代码无需修改
package org.example.agent;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.util.ArrayList;
import java.util.List;
public class MyAgent {
/**
* jvm参数形式调用premain方法,在java程序的main方法执行之前执行
*/
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("[MyAgent][premain]: " + agentArgs);
System.out.println("Premain-Class: " + MyAgent.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("========= ========= =========");
inst.addTransformer(new MonitorTransformer());
}
/**
* attach方式调用agentmain方法,在java程序启动后执行
*/
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("[MyAgent][agentmain]: " + agentArgs);
System.out.println("========= ========= =========");
List<Class> candidates = new ArrayList<>();
MonitorTransformer transformer = new MonitorTransformer();
inst.addTransformer(transformer, true);
// 获取所有已经加载的类
Class[] classes = inst.getAllLoadedClasses();
for (Class c : classes) {
String className = c.getName();
// 排除JDK自带的类
if (className.startsWith("java")) {
continue;
}
if (className.startsWith("javax")) {
continue;
}
if (className.startsWith("jdk")) {
continue;
}
if (className.startsWith("sun")) {
continue;
}
if (className.startsWith("com.sun")) {
continue;
}
// 只保留需要修改的类
boolean isModifiable = inst.isModifiableClass(c);
// 匹配对应项目的包名前缀
boolean isCandidate = className.startsWith("com.example");
if (isModifiable && isCandidate) {
candidates.add(c);
}
String message = String.format("[DEBUG] Loaded Class: %s ---> Modifiable: %s, Candidate: %s", className,
isModifiable, isCandidate);
System.out.println(message);
}
if (!candidates.isEmpty()) {
try {
int size = candidates.size();
// 添加所有需要修改的类
inst.retransformClasses(candidates.toArray(new Class[size]));
} catch (UnmodifiableClassException e) {
throw new RuntimeException(e);
}
String message = String.format("[DEBUG] candidates size: %d, %s", candidates.size(), candidates);
System.out.println(message);
}
}
}
5.3 验证
- 启动程序,
java -jar maven-demo-0.0.1-SNAPSHOT.jar
- 执行attach操作,
java -jar attach-demo-1.0-SNAPSHOT-jar-with-dependencies.jar
- 调用测试接口,
http://localhost:8080/hello?message=fisher
- 查看日志打印