前言
对于Java字节码,它是在Java类的编译过程产生的,即由.java源文件到.class二进制字节码文件的过程。而Java类的加载又是通过类的名字获取二进制字节流,然后在内存中将字节流生成类对象。所以动态修改类的时机在于修改.class文件,只要通过修改.class文件的字节码,即可达到修改类的目的。修改字节码可以通过ASM这个开源框架实现,ASM是一个Java字节码引擎的库,具体可以查看官网,它可以通过操作字节码实现修改类或者生成类。
介绍
Java字节码的执行操作主要是在虚拟机的栈执行,这个栈主要有局部变量表,操作数栈等几个部分。
(一)局部变量表
主要用来保存方法中的局部变量,基本的存储单位为slot(32位的存储空间),所以long double的数据类型需要两个slot, 当方法被调用时,参数会传递从0开始的局部变量表的索引位置上,所以局部变量最大的大小是在编译期就决定的,特别需要注意的是如果调用的是实例方法,局部变量第0个位置是实例对象的引用。
(二)操作数栈
主要用来当作字节码指令操作的出栈入栈的容器,例如变量的出栈入栈都是在操作数栈里面进行的。
(三)指令
指令主要是由操作码+操作数组成的,指令包括加载和存储指令,运算指令和类型转换指令,方法调用指令等等。指令所需要的操作,调用方法,赋值等,都是在操作数栈进行的。
过程
首先是导包,包的版本关系可以查看发布版本,这里我导入的是implementation "org.ow2.asm:asm:6.2"
。修改字节码主要需要以下这几个类:ClassReader, ClassWriter, ClassVisitor, MethodVisitor。各个类的作用如下:
- ClassReader: 读取类文件
- ClassWriter: 继承ClassVisitor 主要用来生成修改类之后的字节
- ClassVisitor: 用于访问修改类
- MethodVisitor: 用于访问修改类的方法
一般用法如下:
try {
String classPath = "asmdemo/ModifyInstanceClass";
ClassReader classReader = new ClassReader(classPath);
ClassWriter classWriter = new ClassWriter(classReader, 0);
ClassVisitor classVisitor = new ClassVisitorDemo(classWriter);
classReader.accept(classVisitor, 0);
File file = new File(ROOT_SUFFIX + "ClassDynamicLoader/ASMProject/build/classes/java/main/asmdemo/ModifyInstanceClass.class");
FileOutputStream output = new FileOutputStream(file);
output.write(classWriter.toByteArray());
output.close();
} catch (IOException e) {
e.printStackTrace();
}
private static class ClassVisitorDemo extends ClassVisitor {
ClassVisitorDemo(ClassVisitor classVisitor) {
super(Opcodes.ASM5, classVisitor);
}
@Override
public void visitEnd() {
cv.visitField(Opcodes.ACC_PRIVATE, "timer", Type.getDescriptor(long.class), null, null);
MethodVisitor methodVisitor = cv.visitMethod(Opcodes.ACC_PUBLIC, "newFunc","()V", null,null);
methodVisitor.visitInsn(Opcodes.RETURN);
methodVisitor.visitMaxs(0,1);
super.visitEnd();
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions);
if (name.equals("print") && desc.equals("()V")) {
methodVisitor = new MethodVisitorHub.FirstMethodVisitor(methodVisitor);
} else if(name.equals("print") && desc.equals("(Ljava/lang/String;)V")) {
methodVisitor = new MethodVisitorHub.SecondMethodVisitor(methodVisitor);
} else if (name.equals("connectStr")) {
methodVisitor = new MethodVisitorHub.ThirdMethodVisitor(methodVisitor);
}
return methodVisitor;
}
}
先利用ClassReader读取待修改的类文件,然后基于Reader创建了对应的ClassWriter,再基于ClassWriter创建了对应的ClassVisitor, 再接着ClassReader委托ClassVisitor去读取修改类,最后,创建文件输出流,利用ClassWriter生成的字节,将重新生成的字节码写回build目录生成的class文件,替换编译生成的class文件,这样就可以达到修改类的目的。