参考资料

ASM 系列详细教程

编译时,找不到asm依赖

用过[Arthas]的都知道,Arthas是alibaba开源的一个非常强大的Java诊断工具。

不管是线上还是线下,我们都可以用Arthas分析程序的线程状态、查看jvm的实时运行状态、打印方法的出入参和返回类型、收集方法中每个代码块耗时,

甚至可以监控类、方法的调用次数、成功次数、失败次数、平均响应时长、失败率等。

前几天学习java动态字节码技术时,突然想起这款java诊断工具的trace功能:打印方法中每个节点的调用耗时。简简单单的,正好拿来做动态字节码入门学习的demo。

程序结构

src
├── agent-package.bat
├── java
│ ├── asm
│ │ ├── MANIFEST.MF
│ │ ├── TimerAgent.java
│ │ ├── TimerAttach.java
│ │ ├── TimerMethodVisitor.java
│ │ ├── TimerTrace.java
│ │ └── TimerTransformer.java
│ └── demo
│ ├── MANIFEST.MF
│ ├── Operator.java
│ └── Test.java
├── run-agent.bat
├── target-package.bat
└── tools.jar

  

编写目标程序

代码

package com.gravel.demo.test.asm;

/**
* @Auther: syh
* @Date: 2020/10/12
* @Description:
*/
public class Test {
public static boolean runnable = true;
public static void main(String[] args) throws Exception {
while (runnable) {
test();
}
} // 目标:分析这个方法中每个节点的耗时
public static void test() throws Exception {
Operator.handler();
long time_wait = (long) ((Math.random() * 1000) + 2000);
Operator.callback();
Operator.pause(time_wait);
}
}

  

Operator.java

/**
* @Auther: syh
* @Date: 2020/10/28
* @Description: 辅助类,同样可用于分析耗时
*/
public class Operator { public static void handler() throws Exception {
long time_wait = (long) ((Math.random() * 10) + 20);
sleep(time_wait);
} public static void callback() throws Exception {
long time_wait = (long) ((Math.random() * 10) + 20);
sleep(time_wait);
} public static void pause(long time_wait) throws Exception {
sleep(time_wait);
} public static void stop() throws Exception {
Test.runnable = false;
System.out.println("business stopped.");
} private static void sleep(long time_wait) throws Exception {
Thread.sleep(time_wait);
}
}

  

MANIFEST.MF

编写MANIFEST.MF文件,指定main-class。注意:冒号后面加空格,结尾加两行空白行。

Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: syh
Created-By: Apache Maven
Build-Jdk: 1.8.0_202
Main-Class: com.gravel.demo.test.asm.Target

  

打包

偷懒写了bat批命令,生成target.jar

@echo off & setlocal
attrib -s -h -r -a /s /d demo
rd /s /q demo
rd /q target.jar
javac -encoding utf-8 -d . ./java/demo/*.java
jar cvfm target.jar ./java/demo/MANIFEST.MF demo
rd /s /q demo
pause
java -jar target.jar

  

java agent探针

instrument 是 JVM 提供的一个可以修改已加载类文件的类库。而要实现代码的修改,我们需要实现一个 instrument agent。

jdk1.5时,agent有个内定方法premain。是在类加载前修改。所以无法做到修改正在运行的类。
jdk1.6后,agent新增了agentmain方法。agentmain是在虚拟机启动以后加载的。所以可以做拦截、热部署等。

讲JAVA探针技术,实际上我自己也是半吊子。所以这里用的是边分析别人例子边摸索的思路来实现我的简单的trace功能。
例子使用的是ASM字节码生成框架

MANIFEST.MF

首先一个可用的jar,关键之一是MAINFEST.MF文件是吧。

Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Created-By: Apache Maven
Built-By: syh
Build-Jdk: 1.8.0_202
Agent-Class: asm.TimerAgent
Can-Retransform-Classes: true
Can-Redefine-Classes: true
Class-Path: ./tools.jar
Main-Class: asm.TimerAttach

  

我们从MANIFEST.MF中提取几个关键的属性

属性
 说明

Agent-Class

agentmain入口类

Premain-Class

premain入口类,与agent-class至少指定一个。

Can-Retransform-Classes

对于已经加载的类重新进行转换处理,即会触发重新加载类定义。

Can-Redefine-Classes

对已经加载的类不做转换处理,而是直接把处理结果(bytecode)直接给JVM

Class-Path

asm动态字节码技术依赖tools.jar,如果没有可以从jdk的lib目录下拷贝。

Main-Class

这里并不是agent的关键属性,为了方便,我把加载虚拟机的程序和agent合并了。

代码

然后我们来看看两个入口类,首先分析一个可执行jar的入口类Main-Class。

public class TimerAttach {

    public static void main(String[] args) throws Exception {
/**
* 启动jar时,需要指定两个参数:1目标程序的pid。 2 要修改的类路径及方法,格式 package.class#methodName
*/
if (args.length < 2) {
System.out.println("pid and class must be specify.");
return;
} if (!args[1].contains("#")) {
System.out.println("methodName must be specify.");
return;
} VirtualMachine vm = VirtualMachine.attach(args[0]);
// 这里为了方便我把 vm和agent整合在一个jar里面了, args[1]就是agentmain的入参。
vm.loadAgent("agent.jar", args[1]);
}
}

  

代码很简单,1:args入参校验;2:加载目标进程pid(args[0]);3:加载agent jar包(因为合并了,所以这个jar其实就是自己)。

其中vm.loadAgent(agent.jar, args[1])会调用agent-class中的agentmain方法,而args[1]就是agentmain的第一个入参。

public class TimerAgent {
public static void agentmain(String agentArgs, Instrumentation inst) {
String[] ownerAndMethod = agentArgs.split("#");
inst.addTransformer(new TimerTransformer(ownerAndMethod[1]), true);
try {
inst.retransformClasses(Class.forName(ownerAndMethod[0]));
System.out.println("agent load done.");
} catch (Exception e) {
e.printStackTrace();
System.out.println("agent load failed!");
}
}
}

  

在 agentmain 方法里,我们调用retransformClassess方法载入目标类,调用addTransformer方法加载TimerTransformer类实现对目标类的重新定义。

类转换器

public class TimerTransformer implements ClassFileTransformer {
private String methodName; public TimerTransformer(String methodName) {
this.methodName = methodName;
} @Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classFileBuffer) {
ClassReader reader = new ClassReader(classFileBuffer);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor classVisitor = new TimerTrace(Opcodes.ASM5, classWriter, methodName);
reader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
return classWriter.toByteArray();
}
}

  

对被匹配到的类中的方法进行修改

public class TimerTrace extends ClassVisitor implements Opcodes {
private String owner;
private boolean isInterface;
private String methodName; public TimerTrace(int i, ClassVisitor classVisitor, String methodName) {
super(i, classVisitor);
this.methodName = methodName;
} @Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
owner = name;
isInterface = (access & ACC_INTERFACE) != 0;
} @Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
// 匹配到指定methodName时,进行字节码修改
if (!isInterface && mv != null && name.equals(methodName)) { // System.out.println(" package.className:methodName()")
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false); mv.visitLdcInsn(" " + owner.replace("/", ".")
+ ":" + methodName + "() ");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append",
"(Ljava/lang/String;)Ljava/lang/StringBuilder;", false); mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); // 方法代码块耗时统计并打印
TimerMethodVisitor at = new TimerMethodVisitor(owner, access, name, descriptor, mv);
return at.getLocalVariablesSorter();
}
return mv;
} public static void main(String[] args) throws IOException {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
TraceClassVisitor tv = new TraceClassVisitor(cw, new PrintWriter(System.out));
TimerTrace addFiled = new TimerTrace(Opcodes.ASM5, tv, "test");
ClassReader classReader = new ClassReader("demo.Test");
classReader.accept(addFiled, ClassReader.EXPAND_FRAMES); File file = new File("out/production/asm-demo/demo/Test.class");
String parent = file.getParent();
File parent1 = new File(parent);
parent1.mkdirs();
file.createNewFile();
FileOutputStream fileOutputStream = new FileOutputStream(file);
fileOutputStream.write(cw.toByteArray());
}
}

  

要统计方法中每行代码耗时,只需要在每一行代码的前后加上当前时间戳然后相减即可。

所以我们的代码是这么写的。

public class TimerMethodVisitor extends MethodVisitor implements Opcodes {
private int start;
private int end;
private int maxStack;
private String lineContent;
public boolean instance = false;
private LocalVariablesSorter localVariablesSorter;
private AnalyzerAdapter analyzerAdapter; public TimerMethodVisitor(String owner, int access, String name, String descriptor, MethodVisitor methodVisitor) {
super(Opcodes.ASM5, methodVisitor);
this.analyzerAdapter = new AnalyzerAdapter(owner, access, name, descriptor, this);
localVariablesSorter = new LocalVariablesSorter(access, descriptor, this.analyzerAdapter);
} public LocalVariablesSorter getLocalVariablesSorter() {
return localVariablesSorter;
} /**
* 进入方法后,最先执行
* 所以我们可以在这里定义一个最开始的时间戳, 然后创建一个局部变量var_end
* Long var_start = System.nanoTime();
* Long var_end;
*/
@Override
public void visitCode() {
mv.visitCode();
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", false);
start = localVariablesSorter.newLocal(Type.LONG_TYPE);
mv.visitVarInsn(ASTORE, start); end = localVariablesSorter.newLocal(Type.LONG_TYPE); maxStack = 4;
} /**
* 在每行代码后面增加以下代码
* var_end = System.nanoTime();
* System.out.println("[" + String.valueOf((var_end.doubleValue() - var_start.doubleValue()) / 1000000.0D) + "ms] " + "package.className:methodName() #lineNumber");
* var_start = var_end;
* @param lineNumber
* @param label
*/
@Override
public void visitLineNumber(int lineNumber, Label label) {
super.visitLineNumber(lineNumber, label);
if (instance) {
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", false);
mv.visitVarInsn(ASTORE, end); // System.out
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); // new StringBuilder();
mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false); mv.visitLdcInsn(" -[");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append",
"(Ljava/lang/String;)Ljava/lang/StringBuilder;", false); mv.visitVarInsn(ALOAD, end);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "doubleValue", "()D", false);
mv.visitVarInsn(ALOAD, start);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "doubleValue", "()D", false);
mv.visitInsn(DSUB);
mv.visitLdcInsn(new Double(1000 * 1000));
mv.visitInsn(DDIV);
// String.valueOf((end - start)/1000000)
mv.visitMethodInsn(INVOKESTATIC, "java/lang/String", "valueOf", "(D)Ljava/lang/String;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append",
"(Ljava/lang/String;)Ljava/lang/StringBuilder;", false); mv.visitLdcInsn("ms] ");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append",
"(Ljava/lang/String;)Ljava/lang/StringBuilder;", false); // .append("owner:methodName() #line")
mv.visitLdcInsn(this.lineContent + "#" + lineNumber);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append",
"(Ljava/lang/String;)Ljava/lang/StringBuilder;", false); // stringBuilder.toString()
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false); // println stringBuilder.toString()
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); // start = end
mv.visitVarInsn(ALOAD, end);
mv.visitVarInsn(ASTORE, start); maxStack = Math.max(analyzerAdapter.stack.size() + 4, maxStack);
}
instance = true;
} /**
* 拼接字节码内容
* @param opcode
* @param owner
* @param methodName
* @param descriptor
* @param isInterface
*/
@Override
public void visitMethodInsn(int opcode, String owner, String methodName, String descriptor, boolean isInterface) {
super.visitMethodInsn(opcode, owner, methodName, descriptor, isInterface);
if (!isInterface && opcode == Opcodes.INVOKESTATIC) {
this.lineContent = owner.replace("/", ".")
+ ":" + methodName + "() ";
}
} @Override
public void visitMaxs(int maxStack, int maxLocals) {
super.visitMaxs(Math.max(maxStack, this.maxStack), maxLocals);
}
}

  

如果初学者不会改字节码。可以利用idea自带的asm插件做参考。

这样,一个可执行的agent jar就写完了,然后打包

@echo off
attrib -s -h -r -a /s /d asm
rd /s /q asm
rd /q agent.jar
javac -XDignore.symbol.file=true -encoding utf-8 -d . ./java/asm/*.java
jar cvfm agent.jar ./java/asm/MANIFEST.MF asm
rd /s /q asm
exit

  

测试

运行目标程序 target.jar

java -jar target.jar

  

打印Test.test中每个节点耗时

java -jar agent.jar [pid] demo.Test#test

  

结果

打印Operator.handler方法每个节点耗时

使用java动态字节码技术简单实现arthas的trace功能。的更多相关文章

  1. Java 动态字节码技术

    对 Debug 的好奇 初学 Java 时,我对 IDEA 的 Debug 非常好奇,不止是它能查看断点的上下文环境,更神奇的是我可以在断点处使用它的 Evaluate 功能直接执行某些命令,进行一些 ...

  2. 【转】动态字节码技术跟踪Java程序

    Whats is Java Agent?   .. java.lang.instrument.Instrumentation 之前有写 基于AOP的日志调试 讨论一种跟踪Java程序的方法, 但不是很 ...

  3. 动态字节码技术Javassist

    字节码技术可以动态改变某个类的结构(添加/删除/修改  新的属性/方法) 关于字节码的框架有javassist,asm,bcel等 引入依赖 <dependency> <groupI ...

  4. JAVA的字节码技术

    1.什么是字节码? 字节码 byteCode JVM能够解释执行的.java程序的归宿,但是从规范上来讲和Java已没有任何关系了.一些动态语言也可以编译成字节码在JVM上运行.字节码就相当于JVM上 ...

  5. Java之字节码(3) - 简单介绍

    转载来自 首先了解一下理论知识: 字节码: Class文件是8位字节流,按字节对齐.之所以称为字节码,是因为每条指令都只占据一个字节,所有的操作码和操作数都是按字节对齐的.如:0×03表示iconst ...

  6. 字节码技术---------动态代理,lombok插件底层原理。类加载器

    字节码技术应用场景 AOP技术.Lombok去除重复代码插件.动态修改class文件等 字节技术优势  Java字节码增强指的是在Java字节码生成之后,对其进行修改,增强其功能,这种方式相当于对应用 ...

  7. 【java虚拟机系列】从java虚拟机字节码执行引擎的执行过程来彻底理解java的多态性

    我们知道面向对象语言的三大特点之一就是多态性,而java作为一种面向对象的语言,自然也满足多态性,我们也知道java中的多态包括重载与重写,我们也知道在C++中动态多态是通过虚函数来实现的,而虚函数是 ...

  8. JVM性能优化--字节码技术

    一.字节码技术应用场景 AOP技术.Lombok去除重复代码插件.动态修改class文件等 二.字节技术优势 Java字节码增强指的是在Java字节码生成之后,对其进行修改,增强其功能,这种方式相当于 ...

  9. JVM探针与字节码技术

    JVM探针是自jdk1.5以来,由虚拟机提供的一套监控类加载器和符合虚拟机规范的代理接口,结合字节码指令能够让开发者实现无侵入的监控功能.如:监控生产环境中的函数调用情况或动态增加日志输出等等.虽然在 ...

随机推荐

  1. Python 中的是那种路径

    1.1 操作系统文件绝对路径 django 静态文件查找, 模板查找(第一种)   # 去配置好的 文件夹 中查找指定的文件 BASE_DIR = os.path.dirname(os.path.di ...

  2. 微服务实战系列(十)-网关高可用之中间件Keepalived

    1.场景描述 因为要做网关的高可用,用到了keepalived+nginx,来保证nginx的高可用,如下图: 安装了keepavlived,走了一些弯路,记录下吧,nginx的安装就不多说了,博客已 ...

  3. STM32的CCM RAM

    STM32F407ZGT6的Flash大小为1MB,SRAM大小为(128KB+64KB). 这里SRAM之所以分开表示是因为在芯片内部前面的128KB和后面的64KB地址不是连续的,后面的64KB在 ...

  4. DIV垂直滚动效果源码

    <div id="demo" style="width: 300; overflow: hidden; line-height:24px; height: 100p ...

  5. 获取url中带的参数

    本文目前只针对url中一个参数的 function getQueryString(name) { var reg = new RegExp("(^|&)" + name + ...

  6. shell-脚本开发基本规范及习惯

    1.shell-脚本开发基本规范及习惯 1.开头指定脚本解析器 #!/bin/sh 或#!/bin/bash 2.开头加版本版权等信息 #Date: 2018/3/26 #Author: zhangs ...

  7. JavaScript常用对象介绍

    目录 对象(object) 对象的创建方式 点语法 括号表示法 内置对象 Array 数组创建方式 检测数组 转换方法 分割字符串 栈方法 队列方法 重排序方法 操作方法 位置方法 迭代方法 Stri ...

  8. Mybatis老手复习文档

    Mybatis学习笔记 再次学习Mybatis,日后,有时间会把这个文档更新,改的越来越好,然后,改成新手老手通用的文档 1.我的认识 Mybatis 是一个持久层框架,(之前 我虽然学了这个myba ...

  9. 基于Python的接口自动化实战-基础篇之读写配置文件

    引言 在编写接口自动化测试脚本时,有时我们需要在代码中定义变量并给变量固定的赋值.为了统一管理和操作这些固定的变量,咱们一般会将这些固定的变量以一定规则配置到指定的配置文件中,后续需要用到这些变量和变 ...

  10. Mysql索引(一篇就够le)

    我想很多人对mysql的认知可能就是CRUD(代表创建(Create).更新(Update).读取(Retrieve)和删除(Delete)操作),也不敢说自己会用和熟悉mysql,当然我就是其中一个 ...