java统计类方法耗时类库_手把手教你实现一个方法耗时统计的 java agent

AAffA0nNPuCLAAAAAElFTkSuQmCC

前面有两篇铺垫博文,在博文《200303-如何优雅的在 java 中统计代码块耗时》,其最后提到了根据利用 java agent 来统计方法耗时

本篇博文将进入 java agent 的实战,手把手教你如何是实现一个统计方法耗时的 java agent

1. 基本姿势点

上面两节虽然手把手教你实现了一个 hello world 版 agent,然而实际上对 java agent 依然是一脸茫然,所以我们得先补齐一下基础知识

首先来看 agent 的两个方法中的参数 Instrumentation,我们先看一下它的接口定义

/**

* 注册一个Transformer,从此之后的类加载都会被Transformer拦截。

* Transformer可以直接对类的字节码byte[]进行修改

*/

void addTransformer(ClassFileTransformer transformer);

/**

* 对JVM已经加载的类重新触发类加载。使用的就是上面注册的Transformer。

* retransformation可以修改方法体,但是不能变更方法签名、增加和删除方法/类的成员属性

*/

void retransformClasses(Class>... classes) throws UnmodifiableClassException;

/**

* 获取一个对象的大小

*/

long getObjectSize(Object objectToSize);

/**

* 将一个jar加入到bootstrap classloader的 classpath里

*/

void appendToBootstrapClassLoaderSearch(JarFile jarfile);

/**

* 获取当前被JVM加载的所有类对象

*/

Class[] getAllLoadedClasses();

前面两个方法比较重要,addTransformer 方法配置之后,后续的类加载都会被 Transformer 拦截。对于已经加载过的类,可以执行 retransformClasses 来重新触发这个 Transformer 的拦截。类加载的字节码被修改后,除非再次被 retransform,否则不会恢复。

通过上面的描述,可知

可以通过Transformer修改类

类加载时,会被触发 Transformer 拦截

2. 实现

我们需要统计方法耗时,所以想到的就是在方法的执行前,记录一个时间,执行完之后统计一下时间差,即为耗时

直接修改字节码有点麻烦,因此我们借助神器javaassist来修改字节码

实现自定义的ClassFileTransformer,代码如下

public class CostTransformer implements ClassFileTransformer {

@Override

public byte[] transform(ClassLoader loader, String className, Class> classBeingRedefined,

ProtectionDomain protectionDomain, byte[] classfileBuffer) {

// 这里我们限制下,只针对目标包下进行耗时统计

if (!className.startsWith("com/git/hui/java/")) {

return classfileBuffer;

}

CtClass cl = null;

try {

ClassPool classPool = ClassPool.getDefault();

cl = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));

for (CtMethod method : cl.getDeclaredMethods()) {

// 所有方法,统计耗时;请注意,需要通过`addLocalVariable`来声明局部变量

method.addLocalVariable("start", CtClass.longType);

method.insertBefore("start = System.currentTimeMillis();");

String methodName = method.getLongName();

method.insertAfter("System.out.println(\"" + methodName + " cost: \" + (System" +

".currentTimeMillis() - start));");

}

byte[] transformed = cl.toBytecode();

return transformed;

} catch (Exception e) {

e.printStackTrace();

}

return classfileBuffer;

}

}

然后稍微改一下 agent

/**

* Created by @author yihui in 16:39 20/3/15.

*/

public class SimpleAgent {

/**

* jvm 参数形式启动,运行此方法

*

* manifest需要配置属性Premain-Class

*

* @param agentArgs

* @param inst

*/

public static void premain(String agentArgs, Instrumentation inst) {

System.out.println("premain");

customLogic(inst);

}

/**

* 动态 attach 方式启动,运行此方法

*

* manifest需要配置属性Agent-Class

*

* @param agentArgs

* @param inst

*/

public static void agentmain(String agentArgs, Instrumentation inst) {

System.out.println("agentmain");

customLogic(inst);

}

/**

* 统计方法耗时

*

* @param inst

*/

private static void customLogic(Instrumentation inst) {

inst.addTransformer(new CostTransformer(), true);

}

}

到此 agent 完毕,打包和上面的过程一样,接下来进入测试环节

创建一个 DemoClz, 里面两个方法

public class DemoClz {

public int print(int i) {

System.out.println("i: " + i);

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

return i + 2;

}

public int count(int i) {

System.out.println("cnt: " + i);

try {

Thread.sleep(50);

} catch (InterruptedException e) {

e.printStackTrace();

}

return i + 1;

}

}

然后对应的 main 方法如下

public class BaseMain {

public static void main(String[] args) throws InterruptedException {

DemoClz demoClz = new DemoClz();

int cnt = 0;

for (int i = 0; i < 20; i++) {

if (++cnt % 2 == 0) {

i = demoClz.print(i);

} else {

i = demoClz.count(i);

}

}

}

}

选择 jvm 参数指定 agent 方式运行(具体操作和上面一样),输出如下

AAffA0nNPuCLAAAAAElFTkSuQmCC

虽然我们的应用程序中并没有方法的耗时统计,但是最终的输出却完美的打印了每个方法的调用耗时,实现了无侵入的耗时统计功能

到这里本文的 java agent 的扫盲 + 实战(开发一个方法耗时统计)都已经完成了,是否就宣告着可以小结了,并不是,下面介绍一下在实现上面的 demo 过程中遇到的一个问题

3. Exception in thread “main” java.lang.VerifyError: Expecting a stack map frame

在演示方法耗时的 agent 的示例中,并没有借助最开始的测试用例,而是新建了一个DemoClz来做的,那么为什么这样选择呢,如果直接用第二节的测试用例会怎样呢?

public class BaseMain {

public int print(int i) {

System.out.println("i: " + i);

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

return i + 2;

}

public void run() {

int i = 1;

while (true) {

i = print(i);

}

}

public static void main(String[] args) {

BaseMain main = new BaseMain();

main.run();

}

依然通过 jvm 参数指定 agent 的方式,运行上面的代码,会发现抛异常,无法正常运行了

AAffA0nNPuCLAAAAAElFTkSuQmCC

指出了在 run 方法这里,存在字节码的错误,我们统计耗时的 Agent,主要就是在方法开始前和结束后各自新增了一行代码,我们直接补充在 run 方法中,则相当于下面的代码

AAffA0nNPuCLAAAAAElFTkSuQmCC

上面的提示很明显的告诉了,最后一行语句永远不可能达到,编译就存在异常了;那么问题来了,作为一个 java agent 的提供者,我哪知道使用者有没有写这种死循环的方法,如果应用中有这么个死循环的任务存在,把我的 agent 一挂载上去,导致应用都起不来,这个锅算谁的????

下面提供解决方案,也很简单,在 jvm 参数中,添加一个-noverify (请注意不同的 jdk 版本,参数可能不一样,我的本地是 jdk8,用这个参数;如果是 jdk7 可以试一下-XX:-UseSplitVerifier)

在 IDEA 开发环境下,如下配置即可

AAffA0nNPuCLAAAAAElFTkSuQmCC

再次运行,正常了

AAffA0nNPuCLAAAAAElFTkSuQmCC

4. 小结

本篇为实战项目,首先明确方法参数Instrumentation它的接口定义,通过它来实现 java 字节码的修改

我们通过实现自定义的ClassFileTransformer,借助 javassist 来修改字节码,为每个方法的第一行和最后一行注入耗时统计的代码,从而实现方法耗时统计

最后留一个小问题,上面的实现中,当方法内部抛出异常时,我们注入的最后一行统计耗时会不会如期输出,如果不会,应该怎么修改,欢迎各位大佬留言指出解决方案

(具体解决方案可以在源码中获取哦,还有配套的测试 case,求支持,求赞,求关注 ❀)

II. 其他

0. 相关

相关博文

相关源码

一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

2. 声明

尽信书则不如,已上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现 bug 或者有更好的建议,欢迎批评指正,不吝感激

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java Agent 是一种 Java 应用程序的增强方式,可以在不修改应用程序源代码的情况下,对其进行一些特定的操作,例如监控、日志记录、性能调优等。 Micrometer 是一款度量指标库,它提供了一种统一的 API,用于在 Java 应用程序中收集和展示各种型的度量指标,例如计数器、计时器、直方图等。 在利用 Java Agent 监控 HTTP 请求的处理过程时,可以借助 Micrometer 来收集和展示各种型的度量指标。以下是实现方法: 1. 在 Java Agent 中,使用 Instrumentation API 找到 HTTP 请求处理的字节码,然后对其进行增强,添加 Micrometer 相关的代码。 2. 在 HTTP 请求处理中,使用 Micrometer API 创建并初始化度量指标,例如计数器和计时器。 3. 在 HTTP 请求处理方法中,根据请求的不同情况,使用 Micrometer API 更新度量指标的值,例如增加计数器的值或记录计时器的耗时。 下面是一个简单的示例,演示如何利用 Java Agent 和 Micrometer 监控 HTTP 请求的处理过程: 1. 创建一个 Java Agent,代码如下: ```java public class HttpAgent { public static void premain(String agentArgs, Instrumentation inst) { Class<?> httpClass = null; for (Class<?> clazz : inst.getAllLoadedClasses()) { if (clazz.getName().equals("com.example.HttpHandler")) { httpClass = clazz; break; } } if (httpClass != null) { inst.addTransformer(new HttpTransformer()); } } } ``` 2. 创建一个字节码转换器,代码如下: ```java public class HttpTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { if (className.equals("com/example/HttpHandler")) { ClassReader cr = new ClassReader(classfileBuffer); ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS); ClassVisitor cv = new HttpClassVisitor(cw); cr.accept(cv, 0); return cw.toByteArray(); } return classfileBuffer; } } ``` 3. 创建一个访问器,代码如下: ```java public class HttpClassVisitor extends ClassVisitor { public HttpClassVisitor(ClassWriter cw) { super(Opcodes.ASM5, cw); } @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); if (name.equals("handle")) { mv = new HttpMethodVisitor(mv); } return mv; } } ``` 4. 创建一个方法访问器,代码如下: ```java public class HttpMethodVisitor extends MethodVisitor { public HttpMethodVisitor(MethodVisitor mv) { super(Opcodes.ASM5, mv); } @Override public void visitCode() { super.visitCode(); mv.visitFieldInsn(Opcodes.GETSTATIC, "io/micrometer/core/instrument/Metrics", "counter", "Lio/micrometer/core/instrument/Counter;"); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "io/micrometer/core/instrument/Counter", "increment", "()V", false); mv.visitLdcInsn("http.requests"); mv.visitFieldInsn(Opcodes.GETSTATIC, "io/micrometer/core/instrument/Metrics", "timer", "Lio/micrometer/core/instrument/Timer;"); mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "io/micrometer/core/instrument/Timer", "record", "(Ljava/time/Duration;)V", false); } @Override public void visitInsn(int opcode) { if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) { mv.visitFieldInsn(Opcodes.GETSTATIC, "io/micrometer/core/instrument/Metrics", "timer", "Lio/micrometer/core/instrument/Timer;"); mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false); mv.visitInsn(Opcodes.DUP2); mv.visitInsn(Opcodes.LSUB); mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/time/Duration", "ofNanos", "(J)Ljava/time/Duration;", false); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "io/micrometer/core/instrument/Timer", "record", "(Ljava/time/Duration;)V", false); } super.visitInsn(opcode); } } ``` 5. 在 HTTP 请求处理中,使用 Micrometer API 创建并初始化度量指标,例如计数器和计时器,代码如下: ```java public class HttpHandler { private final Counter requests = Metrics.counter("http.requests"); private final Timer requestTimer = Metrics.timer("http.requestTimer"); public void handle(HttpRequest request, HttpResponse response) { requests.increment(); Timer.Sample sample = Timer.start(); try { // 处理请求 } finally { sample.stop(requestTimer); } } } ``` 6. 在 HTTP 请求处理方法中,根据请求的不同情况,使用 Micrometer API 更新度量指标的值,例如增加计数器的值或记录计时器的耗时,代码如上所示。 通过以上步骤,就可以利用 Java Agent 和 Micrometer 监控 HTTP 请求的处理过程,实时收集和展示各种型的度量指标,例如请求总数、请求响应时间、请求处理时间等等。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值