字节码编程javassist之结合javaagent监控方法执行耗时

发布于:2024-07-12 ⋅ 阅读:(137) ⋅ 点赞:(0)

写在前面

源码
本文看下javassist如何结合javaagent来实现监控方法执行时长功能。

1:程序

首先来写premain入口程序:

package com.dahuyou.javaagent.and.javassist;

import java.lang.instrument.Instrumentation;

/**
 * javaagent入口类
 */
public class MyJavaAgent {

    public static void premain(String agentArgs, Instrumentation instrumentation) {
        // 打印javaagent指定的参数
        System.out.println("agent args is: " + agentArgs);
        // 添加字节码转换器,转换为插桩后的字节码
        MyWasteTimeTransformer myWasteTimeTransformer = new MyWasteTimeTransformer();
        instrumentation.addTransformer(myWasteTimeTransformer);
    }
}

接着定义类文件转换器:

package com.dahuyou.javaagent.and.javassist;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtBehavior;
import javassist.CtClass;
import javassist.expr.ExprEditor;
import javassist.expr.MethodCall;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.HashSet;
import java.util.Set;

/**
 * 自定义字节码文件转换器
 */
public class MyWasteTimeTransformer implements ClassFileTransformer {
    /*
    要增强的类的集合
     */
    private static final Set<String> classNameSet = new HashSet<>();

    static {
        classNameSet.add("com.dahuyou.javaagent.and.javassist.MyApiTest");
    }

    /**
     * 转换方法
     *
     * @param loader
     * @param className           类名称 注意是/格式的
     * @param classBeingRedefined
     * @param protectionDomain
     * @param classfileBuffer     原始的解码文件,即需要插桩的程序对应的字节码文件
     * @return
     * @throws IllegalClassFormatException
     */
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        try {
            String fullQualifiedClazzName = className.replace("/", ".");
            if (!classNameSet.contains(fullQualifiedClazzName)) {
                return null;
            }
            System.out.println("start transfer class: " + fullQualifiedClazzName);

            // javassist 增强开始啦
            ClassPool pool = ClassPool.getDefault();
            CtClass ctClass = pool.get(fullQualifiedClazzName);
            // 获取所有的方法,并增加方法耗时代码
            CtBehavior[] declaredBehaviors = ctClass.getDeclaredBehaviors();
            for (CtBehavior method : declaredBehaviors) {
                // 增强
                enhanceMethod(method);
            }
            // 返回插桩增强后的字节码
            return ctClass.toBytecode();
        } catch (Exception e) {
            e.printStackTrace();
        }


//        return new byte[0];
        return null;
    }

    /*
    插桩方法执行耗时
     */
    private void enhanceMethod(CtBehavior method) throws Exception {
        String methodName = "";
        if (method.isEmpty() || "main".equalsIgnoreCase(methodName = method.getName())) {
            return;
        }
        final StringBuilder source = new StringBuilder();
        // 前置增强: 打入时间戳
        // 保留原有的代码处理逻辑
        source.append("{")
                .append("long start = System.nanoTime();\n") //前置增强: 打入时间戳
                .append("$_ = $proceed($$);\n") //调用原有代码, 类似于method();($$)表示所有的参数(这个就高级了啊!)
                .append("System.out.print(\"method:[")
                .append(methodName).append("]\");").append("\n")
                .append("System.out.println(\" cost:[\" +(System.nanoTime() - start)+ \"ns]\");") // 后置增强,计算输出方法执行耗时
                .append("}");
        // 方法编辑
        ExprEditor editor = new ExprEditor() {
            @Override
            public void edit(MethodCall methodCall) throws
                    CannotCompileException {
                methodCall.replace(source.toString());
            }
        };
        method.instrument(editor);
    }
}

其中最重要的就是方法enhanceMethod,使用javassist来插桩方法耗时代码,$_ = $proceed($$);用来执行方法原有逻辑,其前和其后就是方法前增强逻辑和方法后增强逻辑了。

接着打agent jar包:
在这里插入图片描述

最后,写个测试类:

package com.dahuyou.javaagent.and.javassist;

/**
 * 要插桩增强的类
 */
public class MyApiTest {
    public static void main(String[] args) {
        new MyApiTest().sayHiMan();
    }

    public void sayHiMan() {
        System.out.println("hello from MyApiTest");
    }
}

还需要在VM option中配置javaagent:
在这里插入图片描述
运行:

agent args is: agent参数啊
start transfer class: com.dahuyou.javaagent.and.javassist.MyApiTest
hello from MyApiTest
method:[sayHiMan] cost:[108000ns]

Process finished with exit code 0

写在后面

如果是有基于字节码插桩的监控需求的话,真正的开发其实也就是这个套路了,无非就是不同的场景字节码框架的技术选型不同而已,也就是javaagent+字节码框架来实现无侵入的监控这样。

参考文章列表

maven项目设置manifest清单文件插件配置


网站公告

今日签到

点亮在社区的每一天
去签到