本文最后编辑于 前,其中的内容可能需要更新。
字节码操作
实践
ASM
对于需要手动操纵字节码的需求,可以使用ASM,它可以直接生产 .class字节码文件,也可以在类被加载入JVM之前动态修改类行为
ASM API
有Core API和Tree API。 Core API可以类比SAX的方式解析XML,Tree API可以类比DOM方式解析XML。一般情况下都使用Core API
它有下面几个关键类:
ClassReader:用于读取已经编译好的.class文件。
ClassWriter:用于重新构建编译后的类,如修改类名、属性以及方法,也可以生成新的类的字节码文件。
各种Visitor类:如上所述,CoreAPI根据字节码从上到下依次处理,对于字节码文件中不同的区域有不同的Visitor,比如用于访问方法的MethodVisitor、用于访问类变量的FieldVisitor、用于访问注解的AnnotationVisitor等。为了实现AOP,重点要使用的是MethodVisitor。
ClassReader和ClassWriter都是继承ClassVisitor
一个Base类
1 2 3 4 5 6 7 8
| public class Base { public Base() { }
public void process() { System.out.println("process"); } }
|
目标:通过ASM增强后在process前输出start、后输出end。
首先需要定义两个类,一个是继承ClassVisitor的MyClassVisitor类,用于对字节码的visit以及修改;
另一个是Generator类,在这个类中定义ClassReader和ClassWriter,classReader读取字节码,然后交给MyClassVisitor类处理,处理完成后由ClassWriter写字节码并将旧的字节码替换掉。

Generator:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| package asm; import jdk.internal.org.objectweb.asm.ClassReader; import jdk.internal.org.objectweb.asm.ClassVisitor; import jdk.internal.org.objectweb.asm.ClassWriter;
import java.io.File; import java.io.FileOutputStream;
public class Generator { public static void main(String[] args) throws Exception { ClassReader classReader = new ClassReader("asm/Base"); ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); ClassVisitor classVisitor = new MyClassVisitor(classWriter); classReader.accept(classVisitor, ClassReader.SKIP_DEBUG); byte[] data = classWriter.toByteArray(); File f = new File("target/classes/asm/Base.class"); FileOutputStream fout = new FileOutputStream(f); fout.write(data); fout.close(); System.out.println("now generator cc success!!!!!"); } }
|
MyClassVisitor
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| package asm;
import jdk.internal.org.objectweb.asm.ClassVisitor; import jdk.internal.org.objectweb.asm.MethodVisitor; import jdk.internal.org.objectweb.asm.Opcodes;
public class MyClassVisitor extends ClassVisitor implements Opcodes { public MyClassVisitor( ClassVisitor classVisitor) { super(ASM5, classVisitor); }
@Override public MethodVisitor visitMethod(int i, String s, String s1, String s2, String[] strings) { MethodVisitor methodVisitor = cv.visitMethod(i, s, s1, s2, strings); if (s.equals("<init>") || s.equals("sayHello")) { return methodVisitor; } return new MyMethodVisitor(methodVisitor); }
class MyMethodVisitor extends MethodVisitor implements Opcodes{
public MyMethodVisitor(MethodVisitor methodVisitor) { super(ASM5, methodVisitor); }
@Override public void visitCode() { mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitLdcInsn("start"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); super.visitCode(); }
@Override public void visitInsn(int opcode) { if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) { mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitLdcInsn("end"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); } super.visitInsn(opcode); } } }
|
逻辑理解起来也不难。
Base里有两个方法,构造函数和process。process是我们要增强的方法。
visitMethod方法判断当前读到哪个方法,跳过构造方法 <init>
后,将需要被增强的方法交给内部类MyMethodVisitor来进行处理。

主要的操作就在这一片
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Override public void visitCode() { super.visitCode(); mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitLdcInsn("start"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); } @Override public void visitInsn(int opcode) { if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) { mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitLdcInsn("end"); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); } mv.visitInsn(opcode); }
|
MethodVisitor的visitCode方法,它会在ASM开始访问某一个方法的Code区时被调用,所以可以在这里添加前置操作
每当ASM访问到无参数指令时,都会调用MyMethodVisitor中的visitInsn方法,判断了当前指令是否为无参数的“return”指令,如果是就在它的前面添加一些指令。我们写void方法时一般没有return。这里其实涉及到在字节码层面,void方法会执行return指令。
具体实现就是visitXXXXInsn,中间的XXXX就是字节码指令,比如mv.visitLdcInsn(“end”)对应的操作码就是ldc “end”,即将字符串“end”压入栈。
运行Generator.main之后查看编译好的Base.class内容:

写一个Test测试

ASM工具
ASM ByteCode Outline
算是一个辅助工具,利用ASM手写字节码时,需要利用一系列visitXXXXInsn()方法来写对应的助记符,所以需要先将每一行源代码转化为一个个的助记符,然后通过ASM的语法转换为visitXXXXInsn()这种写法。第一步将源码转化为助记符就已经够麻烦了,不熟悉字节码操作集合的话,需要我们将代码编译后再反编译,才能得到源代码对应的助记符。第二步利用ASM写字节码时,如何传参也很令人头疼。ASM社区也知道这两个问题,所以提供了这个工具
在idea里可以直接安装。
工具教程
https://blog.csdn.net/ForwardSailing/article/details/106494116
是个非常非常好用的工具
Javassist
比asm操作简单一点框架。ASM是在指令层次上操作字节码,而Javassist更强调源码层次
利用Javassist实现字节码增强时,可以无须关注字节码刻板的结构,其优点就在于编程简单。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构或者动态生成类。其中最重要的是ClassPool、CtClass、CtMethod、CtField这四个类:
- CtClass(compile-time class):编译时类信息,它是一个class文件在代码中的抽象表现形式,可以通过一个类的全限定名来获取一个CtClass对象,用来表示这个类文件。
- ClassPool:从开发视角来看,ClassPool是一张保存CtClass信息的HashTable,key为类名,value为类名对应的CtClass对象。当我们需要对某个类进行修改时,就是通过pool.getCtClass(“className”)方法从pool中获取到相应的CtClass。
- CtMethod、CtField:这两个比较好理解,对应的是类中的方法和属性。
跟之前一样,在process前面和后面插入start和end,实现起来更简单直观易懂。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import javassist.*;
import java.io.IOException;
public class JavassistTest { public static void main(String[] args) throws Exception { ClassPool cp = ClassPool.getDefault(); CtClass cc = cp.get("asm.Base"); CtMethod m = cc.getDeclaredMethod("process"); m.insertBefore("{ System.out.println(\"start\"); }"); m.insertAfter("{ System.out.println(\"end\"); }"); Class c = cc.toClass(); cc.writeFile("/Users/fmyyy/Myself/yso/untitled"); Base h = (Base)c.newInstance(); h.process(); } }
|
参考
https://tech.meituan.com/2019/09/05/java-bytecode-enhancement.html