通过java agent监控程序执行

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 监控类

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 指定程序名称方式

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

在这里插入图片描述

  • 查看日志打印

在这里插入图片描述

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值