阅读完需:约 16 分钟
ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。
ASM — Maven
<!-- ASM -->
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-commons</artifactId>
<version>6.2.1</version>
<exclusions>
<exclusion>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-analysis</artifactId>
</exclusion>
<exclusion>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-tree</artifactId>
</exclusion>
</exclusions>
</dependency>
认识ASM
例1:HelloWorld
首先来看几个简单的例子:
public class HelloWorld {
public static void main(String[] var0) {
System.out.println("Hello World");
}
}
HelloWorld一般我们都是这么写的,但是用 javap -c
反编译一下就会发现完全不一样啦
public class org.itstack.demo.test.HelloWorld {
public org.itstack.demo.test.HelloWorld();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
指令 | 描述 |
---|---|
getstatic | 获取静态字段的值 |
ldc | 常量池中的常量值入栈 |
invokevirtual | 运行时方法绑定调用方法 |
return | void函数返回 |
这几个是上述反编译的字节码指令
当HelloWorld用ASM写出来的时候就会变的非常不一样
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
private static byte[] generate() {
ClassWriter classWriter = new ClassWriter(0);
// 定义对象头;版本号、修饰符、全类名、签名、父类、实现的接口
classWriter.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC, "org/itstack/demo/asm/AsmHelloWorld", null, "java/lang/Object", null);
// 添加方法;修饰符、方法名、描述符、签名、异常
MethodVisitor methodVisitor = classWriter.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
// 执行指令;获取静态属性
methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
// 加载常量 load constant
methodVisitor.visitLdcInsn("Hello World");
// 调用方法
methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
// 返回
methodVisitor.visitInsn(Opcodes.RETURN);
// 设置操作数栈的深度和局部变量的大小
methodVisitor.visitMaxs(2, 1);
// 方法结束
methodVisitor.visitEnd();
// 类完成
classWriter.visitEnd();
// 生成字节数组
return classWriter.toByteArray();
}
以上的代码都是来自于 ASM
框架的代码,这里面所有的操作与我们使用使用 javap -c XXX
所反解析出的字节码是一样的,只不过是反过来使用指令来编写代码。
这里有几个比较关键的内容:
- 定义一个类的生成
ClassWriter
- 设定版本、修饰符、全类名、签名、父类、实现的接口,其实也就是那句;
public class HelloWorld
- 接下来开始创建方法,方法同样需要设定;修饰符、方法名、描述符等。这里面有几个固定标识;
- 类型描述符
- | Java 类型 | 类型描述符 | |:—|:—| | boolean | Z | | char | C | | byte | B | | short | S | | int | I | | float | F | | long | J | | double | D | | Object | Ljava/lang/Object; | | int[] | [I | | Object[][] | [[Ljava/lang/Object; |
- 方法描述符
- | 源文件中的方法声明 | 方法描述符 | |:—|:—| | void m(int i, float f) | (IF)V | | int m(Object o) | (Ljava/lang/Object;)I | | int[] m(int i, String s) | (ILjava/lang/String;)[I | | Object m(int[] i) | ([I)Ljava/lang/Object; |
([Ljava/lang/String;)V
== void main(String[] args)
- 类型描述符
- 执行指令;获取静态属性。主要是获得
System.out
- 加载常量 load constant,输出我们的HelloWorld
methodVisitor.visitLdcInsn("Hello World");
- 最后是调用输出方法并设置空返回,同时在结尾要设置操作数栈的深度和局部变量的大小
但是单纯的写指令很令人很费解,难以下手所以有一些好用的工具可以帮助我们查看代码的字节码。


当我们开发的时候只要对着这个字节码来编写ASM,基本问题都可以解决。
例2:两数之和计算
我们的目标方法
public class demo {
public static void main(String[] args) {
int sum = new demo().sum(1, 1);
System.out.println(sum);
}
public int sum(int i, int m) {
return i + m;
}
}
ASM编写的内容
public static byte[] generate2(){
ClassWriter classWriter = new ClassWriter(0);
MethodVisitor methodVisitor_init = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
methodVisitor_init.visitCode();
methodVisitor_init.visitVarInsn(Opcodes.ALOAD, 0);
methodVisitor_init.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
methodVisitor_init.visitInsn(Opcodes.RETURN);
methodVisitor_init.visitMaxs(1, 1);
methodVisitor_init.visitEnd();
classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "com/enmalvi/ASM/demoauto", null, "java/lang/Object", null);
MethodVisitor methodVisitor = classWriter.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
methodVisitor.visitTypeInsn(Opcodes.NEW, "com/enmalvi/ASM/demoauto");
methodVisitor.visitInsn(Opcodes.DUP);
methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, "com/enmalvi/ASM/demoauto","<init>","()V");
methodVisitor.visitInsn(Opcodes.ICONST_1);
methodVisitor.visitInsn(Opcodes.ICONST_1);
methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "com/enmalvi/ASM/demoauto", "sum", "(II)I");
methodVisitor.visitVarInsn(Opcodes.ISTORE,1);
methodVisitor.visitFieldInsn(Opcodes.GETSTATIC,"java/lang/System","out","Ljava/io/PrintStream;");
methodVisitor.visitVarInsn(Opcodes.ILOAD,1);
methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL,"java/io/PrintStream","println","(I)V");
methodVisitor.visitInsn(Opcodes.RETURN);
methodVisitor.visitMaxs(3,2);
methodVisitor.visitEnd();
// 添加方法;修饰符、方法名、描述符、签名、异常
MethodVisitor methodVisitor_sum = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "sum", "(II)I", null, null);
methodVisitor_sum.visitVarInsn(Opcodes.ILOAD, 1);
methodVisitor_sum.visitVarInsn(Opcodes.ILOAD, 2);
methodVisitor_sum.visitInsn(Opcodes.IADD);
// 返回
methodVisitor_sum.visitInsn(Opcodes.IRETURN);
// 设置操作数栈的深度和局部变量的大小
methodVisitor_sum.visitMaxs(2, 3);
methodVisitor_sum.visitEnd();
// 类完成
classWriter.visitEnd();
// 生成字节数组
return classWriter.toByteArray();
}
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Class<?> clazz = new generate().defineClass("com.enmalvi.ASM.demoauto", generate2(), 0, generate2().length);
// 反射获取 main 方法
Method main = clazz.getMethod("main", String[].class);
// 调用 main 方法
main.invoke(null, new Object[]{new String[]{}});
}
看似很难很复杂,其实都是模版化的套路
-
ClassWriter
创建一个类 -
MethodVisitor
生成一个方法的访问中者,写入方法的内容最后methodVisitor.visitEnd()
结束方法执行闭环,最后classWriter.visitEnd()
类完成创建完成,classWriter.toByteArray()
生成字节数组。 - 通过反射调用创建的类里的方法即可

其中的methodVisitor
也是分好几段来创建的和插件展示的字节码是一致的,而ClassWriter
只创建一个即可。
例3:在原有方法上字节码增强监控耗时
原有的方法:
public class MyMethod {
public String queryUserInfo(String uid) {
System.out.println("xxxx");
System.out.println("xxxx");
System.out.println("xxxx");
System.out.println("xxxx");
return uid;
}
}
目标实现的方法:
public class ApiTest {
public String queryUserInfo(String uid) {
long var2 = System.nanoTime();
System.out.println("xxxx");
System.out.println("xxxx");
System.out.println("xxxx");
System.out.println("xxxx");
System.out.println("方法执行耗时(纳秒)->queryUserInfo:" + (System.nanoTime() - var2));
return uid;
}
}
ASM编写
public class TestMonitor extends ClassLoader {
public static void main(String[] args) throws IOException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
// 读取类
ClassReader cr = new ClassReader(MyMethod.class.getName());
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
// 初始化
{
MethodVisitor methodVisitor = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
methodVisitor.visitInsn(Opcodes.RETURN);
methodVisitor.visitMaxs(1, 1);
methodVisitor.visitEnd();
}
// 插入信息
ClassVisitor cv = new ProfilingClassAdapter(cw, MyMethod.class.getSimpleName());
// 组合字节码信息
cr.accept(cv, ClassReader.EXPAND_FRAMES);
byte[] bytes = cw.toByteArray();
outputClazz(bytes);
Class<?> clazz = new TestMonitor().defineClass("com.enmalvi.ASM.ASM02.MyMethod", bytes, 0, bytes.length);
Method queryUserInfo = clazz.getMethod("queryUserInfo", String.class);
Object obj = queryUserInfo.invoke(clazz.newInstance(), "10001");
System.out.println("测试结果:" + obj);
}
static class ProfilingClassAdapter extends ClassVisitor {
public ProfilingClassAdapter(final ClassVisitor cv, String innerClassName) {
super(ASM5, cv);
}
// 关键是这里 插入自己想要的内容
@Override
public MethodVisitor visitMethod(int access,
String name,
String desc,
String signature,
String[] exceptions) {
System.out.println("access:" + access);
System.out.println("name:" + name);
System.out.println("desc:" + desc);
if (!"queryUserInfo".equals(name)) return null;
// 基本固定写法
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
return new ProfilingMethodVisitor(mv, access, name, desc);
}
}
static class ProfilingMethodVisitor extends AdviceAdapter {
private String methodName = "";
protected ProfilingMethodVisitor(MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(ASM5, methodVisitor, access, name, descriptor);
this.methodName = name;
}
// 在方法输入
@Override
protected void onMethodEnter() {
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
mv.visitVarInsn(LSTORE, 2);
mv.visitVarInsn(ALOAD, 1);
}
// 关于方法退出
@Override
protected void onMethodExit(int opcode) {
if ((IRETURN <= opcode && opcode <= RETURN) || opcode == ATHROW) {
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("方法执行耗时(纳秒)->" + methodName+":");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
mv.visitVarInsn(LLOAD, 2);
mv.visitInsn(LSUB);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)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);
}
}
}
private static void outputClazz(byte[] bytes) {
// 输出类字节码
FileOutputStream out = null;
try {
String pathName = TestMonitor.class.getResource("/").getPath() + "AsmTestMonitor.class";
out = new FileOutputStream(new File(pathName));
System.out.println("ASM类输出路径:" + pathName);
out.write(bytes);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != out) try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
- 整体的代码块有点大,我们可以分为块来看,如下;
-
ClassReader cr = new ClassReader(MyMethod.class.getName());
读取原有类,也是字节码增强的开始 -
ClassVisitor cv = new ProfilingClassAdapter(cw, MyMethod.class.getSimpleName());
开始增强字节码 -
onMethodEnter
,onMethodExit
,在方法进入和方法退出时添加耗时执行的代码。
-
结果:
access:1
name:<init>
desc:()V
access:1
name:queryUserInfo
desc:(Ljava/lang/String;)Ljava/lang/String;
ASM类输出路径:/Users/xujiahui/Me/IDEA/enmalvi/case-study/target/classes/AsmTestMonitor.class
xxxx
xxxx
xxxx
xxxx
方法执行耗时(纳秒)->queryUserInfo:44083
测试结果:10001
例4:方法加上TryCatch捕获异常并输出
在字节码增强方面有三个框架;ASM、Javassist、ByteCode,各有优缺点按需选择。
通过 ASM
字节码增强技术,使用指令码将方法修改为我们想要的效果。这部分原本需要使用 JavaAgent
技术,在工程启动加载时候进行修改字节码。这里为了将关于字节码核心内容展示出来,通过加载类名称获取字节码进行修改。
这是修改之前的方法
public Integer strToNumber(String str) {
return Integer.parseInt(str);
}
这是修改之后的方法
public Integer strToNumber(String str) throws JsonProcessingException {
try {
Integer var2 = Integer.parseInt(str);
point("com.enmalvi.ASM.ASM03.strToNumber", (Object)var2);
return var2;
} catch (Exception var3) {
point("com.enmalvi.ASM.ASM03.strToNumber", (Throwable)var3);
throw var3;
}
}
从修改前到修改后,可以看到。有如下几点修改;
- 返回值赋值给新的参数,并做了输出
- 把方法包裹在一个
TryCatch
中,并将异常也做了输出
ASM编写
public class MethodTest extends ClassLoader {
// 测试的方法
public Integer strToNumber(String str) {
return Integer.parseInt(str);
}
public static void main(String[] args) throws Exception {
byte[] bytes = new MethodTest().getBytes(MethodTest.class.getName());
// 输出方法
// outputClazz(bytes, MethodTest.class.getSimpleName());
// 测试方法
Class<?> clazz = new MethodTest().defineClass("com.enmalvi.ASM.ASM03.MethodTest", bytes, 0, bytes.length);
Method queryUserInfo = clazz.getMethod("strToNumber", String.class);
// 正确入参
Object obj01 = queryUserInfo.invoke(clazz.newInstance(), "123");
System.out.println("01 测试结果:" + obj01);
// 异常入参
Object obj02 = queryUserInfo.invoke(clazz.newInstance(), "abc");
System.out.println("02 测试结果:" + obj02);
}
/**
* 字节码增强获取新的字节码
*/
private byte[] getBytes(String className) throws IOException {
/**
* 首先他会分别创建 ClassReader、ClassWriter,用于对类的加载和写入,这里的加载方式在构造方法中也提供的比较丰富。
* 可以通过类名、字节码或者流的方式进行处理。
*/
ClassReader cr = new ClassReader(className);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
cr.accept(new ClassVisitor(ASM5, cw) {
/**
* 接下来是对方法的访问 MethodVisitor ,基本所有使用 ASM 技术的监控系统,都会在这里来实现字节码的注入。
* @param access
* @param name
* @param descriptor
* @param signature
* @param exceptions
* @return
*/
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
// 方法过滤
if (!"strToNumber".equals(name)) {
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
return new AdviceAdapter(ASM5, mv, access, name, descriptor) {
private Label from = new Label(),
to = new Label(),
target = new Label();
/**
* onMethodEnter 方法进入时设置一些基本内容,比如当前纳秒用于后续监控方法的执行耗时。
* 还有就是一些 Try 块的开始。
*/
@Override
protected void onMethodEnter() {
//标志:try块开始位置
visitLabel(from);
visitTryCatchBlock(from,
to,
target,
"java/lang/Exception");
}
/**
* visitMaxs 这个是在方法结束前,用于添加 Catch 块。到这也就可以将整个方法进行包裹起来了。
* @param maxStack
* @param maxLocals
*/
@Override
public void visitMaxs(int maxStack, int maxLocals) {
//标志:try块结束
mv.visitLabel(to);
//标志:catch块开始位置
mv.visitLabel(target);
mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]{"java/lang/Exception"});
// 异常信息保存到局部变量
int local = newLocal(Type.LONG_TYPE);
mv.visitVarInsn(ASTORE, local);
// 输出信息
mv.visitLdcInsn(className + "." + name); // 类名.方法名
mv.visitVarInsn(ALOAD, local);
mv.visitMethodInsn(INVOKESTATIC, Type.getInternalName(MethodTest.class), "point", "(Ljava/lang/String;Ljava/lang/Throwable;)V", false);
// 抛出异常
mv.visitVarInsn(ALOAD, local);
mv.visitInsn(ATHROW);
super.visitMaxs(maxStack, maxLocals);
}
/**
* onMethodExit 最后是这个方法退出时,用于 RETURN 之前,可以注入结尾的字节码加强,比如调用外部方法输出监控信息。
* @param opcode
*/
@Override
protected void onMethodExit(int opcode) {
// this.nextLocal,获取局部变量的索引值。这个值就让局部变量最后的值,也就是存放 ARETURN 的值(ARETURN,是返回对象类型,如果是返回 int 则需要使用 IRETURN)。
if ((IRETURN <= opcode && opcode <= RETURN) || opcode == ATHROW) {
int nextLocal = this.nextLocal;
mv.visitVarInsn(ASTORE, nextLocal); // 将栈顶引用类型值保存到局部变量indexbyte中。
mv.visitVarInsn(ALOAD, nextLocal); // 从局部变量indexbyte中装载引用类型值入栈。
mv.visitLdcInsn(className + "." + name); // 类名.方法名
mv.visitVarInsn(ALOAD, nextLocal); // ALOAD,将异常信息加载到操作数栈用,用于输出。
// INVOKESTATIC,调用静态方法。调用方法除了这个指令外还有;invokespecial、invokevirtual、invokeinterface。
mv.visitMethodInsn(INVOKESTATIC, Type.getInternalName(MethodTest.class), "point", "(Ljava/lang/String;Ljava/lang/Object;)V", false);
}
}
};
}
}, ClassReader.EXPAND_FRAMES);
return cw.toByteArray();
}
/**
* 输出字节码
*
* @param bytes 字节码
* @param className 类名称
*/
private static void outputClazz(byte[] bytes, String className) {
// 输出类字节码
FileOutputStream out = null;
try {
String pathName = MethodTest.class.getResource("/").getPath() + className + "SQM.class";
out = new FileOutputStream(new File(pathName));
System.out.println("ASM字节码增强后类输出路径:" + pathName + "\r\n");
out.write(bytes);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != out) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void point(String methodName, Throwable throwable) {
System.out.println("系统监控 :: [方法名称:" + methodName + " 异常信息:" + throwable.getMessage() + "]\r\n");
}
public static void point(String methodName, Object response) throws JsonProcessingException {
System.out.println("系统监控 :: [方法名称:" + methodName + " 输出信息:" + new ObjectMapper().writeValueAsString(response) + "]\r\n");
}
}
以上这段代码就是 ASM
用于处理字节码增强的模版代码块。首先他会分别创建 ClassReader
、ClassWriter
,用于对类的加载和写入,这里的加载方式在构造方法中也提供的比较丰富。可以通过类名、字节码或者流的方式进行处理。
接下来是对方法的访问 MethodVisitor
,基本所有使用 ASM
技术的监控系统,都会在这里来实现字节码的注入。这里面目前用到了三个方法的,如下;
-
onMethodEnter
方法进入时设置一些基本内容,比如当前纳秒用于后续监控方法的执行耗时。还有就是一些Try
块的开始。 -
visitMaxs
这个是在方法结束前,用于添加Catch
块。到这也就可以将整个方法进行包裹起来了。 -
onMethodExit
最后是这个方法退出时,用于RETURN
之前,可以注入结尾的字节码加强,比如调用外部方法输出监控信息。
基本上所有的 ASM
字节码增强操作,都离不开这三个方法。
结果:
系统监控 :: [方法名称:com.enmalvi.ASM.ASM03.MethodTest.strToNumber 输出信息:123]
01 测试结果:123
系统监控 :: [方法名称:com.enmalvi.ASM.ASM03.MethodTest.strToNumber 异常信息:For input string: "abc"]
我们常用的非入侵的监控系统,全链路监控,以及一些反射框架中,其实都用到了 ASM
,只是还没有注意到而已。