对于Java字节码,它是在Java类的编译过程产生的,即由.java源文件到.class二进制字节码文件的过程。而Java类的加载又是经过类的名字获取二进制字节流,而后在内存中将字节流生成类对象。因此动态修改类的时机在于修改.class文件,只要经过修改.class文件的字节码,便可达到修改类的目的。修改字节码能够经过ASM这个开源框架实现,ASM是一个Java字节码引擎的库,具体能够查看官网,它能够经过操做字节码实现修改类或者生成类。html
Java字节码的执行操做主要是在虚拟机的栈执行,这个栈主要有局部变量表,操做数栈等几个部分。java
主要用来保存方法中的局部变量,基本的存储单位为slot(32位的存储空间),因此long double的数据类型须要两个slot, 当方法被调用时,参数会传递从0开始的局部变量表的索引位置上,因此局部变量最大的大小是在编译期就决定的,特别须要注意的是若是调用的是实例方法,局部变量第0个位置是实例对象的引用。 ###(二)操做数栈 主要用来看成字节码指令操做的出栈入栈的容器,例如变量的出栈入栈都是在操做数栈里面进行的。 ###(三)指令 指令主要是由操做码+操做数组成的,指令包括加载和存储指令,运算指令和类型转换指令,方法调用指令等等。指令所须要的操做,调用方法,赋值等,都是在操做数栈进行的。git
首先是导包,包的版本关系能够查看发布版本,这里我导入的是implementation "org.ow2.asm:asm:6.2"
。修改字节码主要须要如下这几个类:ClassReader, ClassWriter, ClassVisitor, MethodVisitor。各个类的做用以下:github
通常用法以下:数组
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文件,这样就能够达到修改类的目的。bash
flowchat
st=>start: 开始
e=>end: 文件输出流将字节码写回类生成的build路径,替换
op1=>operation: Reader读取类
op2=>operation: MethodVisitor修改类
op3=>operation: 获取Writer修改以后的字节码
st->op1->op2->op3->e
复制代码
对于类的修改,主要关注ClassVisitor和MethodVisitor这两个类便可,ClassVistor能够实现成员变量和方法的增长,MethodVisitor用于修改类方法的实现。在修改类方法的时候,我是先经过把原先的方法修改成预期的方法,而后经过javap命令对预期的方法产生的类文件进行反编译,查看编译器产生的字节码。命令以下:javap -v .class文件路径。 经过反编译以后能够获得修改后的类的操做数栈和局部变量表的最大大小,还有具体的字节码指令。下面开始看具体的使用。框架
MethodVIsitor通常经过实现visitCode visitInsan visitMaxs方法来实现类的修改。visitCode是方法的访问开始;visitInsn能够访问方法的操做指令,通常应用于在return指令以前插入代码;vistiMax则用于复写操做数栈和局部变量表的大小,由于类被修改,因此所需的栈和变量表大小可能会增长。下面是几个具体的例子:ide
1. 在print()空方法中插入一行输出 System.out.print("Hello World");
ui
首先利用javap -v 编译修改前的print方法,以下

接着在print()方法增长 ` System.out.print("Hello World");`再执行javap -c反编译
复制代码
public static class FirstMethodVisitor extends MethodVisitor {
public FirstMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM5, mv);
}
/**
* 进入方法 插入System.out.print("hello world")这行代码
*/
@Override
public void visitCode() {
super.visitCode();
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("hello world");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "print", "(Ljava/lang/String;)V", false);
}
@Override
public void visitMaxs(int maxStack, int maxLocals) {
mv.visitMaxs(2,1);
}
}
复制代码
上面的代码主要覆写了visitMaxs,stack local数值是经过反编译获得的,visitCode则是添加了三个指令。分析System.out.print可知,实际上是经过System这个类获取out这个变量,而后经过out调用print这个方法输出“hello world”这个变量。 因此首先要获取out,out是一个静态变量,第一个指令是visitFieldInsn,顾名思义就是访问成员的指令,第一个参数是操做码,第二个参数是调用成员的类,第三个参数是成员的名称,第四个参数是成员的类型,对号入座第一个指令,操做码是获取静态变量,调用类是“java/lang/System”, 成员名是“out”, 类型经过反编译可知是“Ljava/io/PrintStream;”。因此得出结论,第一个指令是经过System这个类获取out这个静态变量而且把变量入栈。 接着第二个指令visitLdcInsn是把常量推到操做数栈,这里是把“hello world”入栈, 最后就是第三个指令visitMethodInsn,仍是顾名思义是访问方法的指令,第一个参数是操做码,第二个参数是调用方法等的类,第三个参数是方法名,第四个参数是方法的返回类型和参数类型,第五个参数是调用方法的类是不是接口,对号入座,Opcodes.INVOKEVIRTUAL指的是调用的是实例方法,调用的类是out即“java/io/PrintStream”这个类,方法名是print,返回值是void对应“V”,参数是String对应“Ljava/lang/String; ”, 这些参数的对应类型均可以从反编译获得。第三个指令须要两个操做数,一个是执行方法的主体即out,第二个是参数即“hello world”,使用visitMethodInsn指令的时候,out “”hello world“依次从操做数栈出栈,刚恰好对应指令调用的参数顺序。 拦截方法的入口在ClassVisitor,以下:this
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;
}
}
复制代码
在visitMethod中判断方法名为print,则进行拦截注入本身建立的MethodVisitor便可。 到这里已经分析完成,能够自信满满地运行代码了,可是要切记,不能在修改以前使用该类,若是使用了以后,类已经被加载,那么修改以后的类不会被再次加载,也就没法发挥做用了。
2. 在print(String s )空方法中插入一行输出 System.out.print(s)
分析的方法和上面的同样,这里的关键是读取参数的值,反编译以后能够发现使用了ALOAD这个指令,这个指令的做用是从局部变量表读取变量入栈,指令代码以下:
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitVarInsn(Opcodes.ALOAD, 1);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "print", "(Ljava/lang/String;)V", false);
复制代码
visitVarInsn是读取参数的指令,操做码是ALOAD,后面的参数是指变量表的索引,上面也提到,若是是实例方法,局部变量表的0索引是实例对象,因此这里取了索引1。
**3. 在connectStr()空方法打印执行消耗时间 ** 修改前代码以下:
public void connectStr() {
String s = "";
for (int i = 0; i < 10000; i ++) {
s += i;
}
}
复制代码
修改后代码以下:
public void connectStr() {
this.timer = -System.currentTimeMillis();
String s = "";
for(int i = 0; i < 10000; ++i) {
s = s + i;
}
this.timer += System.currentTimeMillis();
System.out.println(this.timer);
}
复制代码
这里的关键是在return前插入代码, 还有增长变量timer。具体的反编译过程就不展现了,直接上代码:
public static class ThirdMethodVisitor extends MethodVisitor {
public ThirdMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM5, mv);
}
/**
* 进入方法
*/
@Override
public void visitCode() {
super.visitCode();
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitInsn(Opcodes.LNEG);
mv.visitFieldInsn(Opcodes.PUTFIELD, "asmdemo/ModifyInstanceClass", "timer", "J");
}
/**
* return前插入代码
*/
@Override
public void visitInsn(int opcode) {
if (opcode == Opcodes.RETURN) {
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitInsn(Opcodes.DUP);
mv.visitFieldInsn(Opcodes.GETFIELD, "asmdemo/ModifyInstanceClass", "timer", "J");
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitInsn(Opcodes.LADD);
mv.visitFieldInsn(Opcodes.PUTFIELD, "asmdemo/ModifyInstanceClass", "timer", "J");
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitFieldInsn(Opcodes.GETFIELD, "asmdemo/ModifyInstanceClass", "timer", "J");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(J)V", false);
}
super.visitInsn(opcode);
}
@Override
public void visitMaxs(int maxStack, int maxLocals) {
mv.visitMaxs(5, 3);
}
}
复制代码
首先看visitCode方法, 作的事情就是this.timer = System.currentTimeMillis(),对这行代码进行拆分,就是获取时间戳赋值给timer,对应底下的指令mv.visitVarInsn(Opcodes.ALOAD, 0) 先将实例对象入栈即咱们用的变量this,接着访问方法获取系统时间戳而后执行LNEG取反入栈,最后在执行访问方法的指令PUTFIELD把值赋给timer,须要的参数是时间戳和this变量,this变量用于访问timer,时间戳则是赋值的变量。 接着看visitInsn方法,visitInsn能够拦截方法执行的指令作一些插入操做,在这里咱们须要作的事在return以前插入时间戳的计算和打印, 代码比较长以下:
if (opcode == Opcodes.RETURN) {
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitInsn(Opcodes.DUP);
mv.visitFieldInsn(Opcodes.GETFIELD, "asmdemo/ModifyInstanceClass", "timer", "J");
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitInsn(Opcodes.LADD);
mv.visitFieldInsn(Opcodes.PUTFIELD, "asmdemo/ModifyInstanceClass", "timer", "J");
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitVarInsn(Opcodes.ALOAD, 0);
mv.visitFieldInsn(Opcodes.GETFIELD, "asmdemo/ModifyInstanceClass", "timer", "J");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(J)V", false);
}
复制代码
老规矩,拆解代码 this.timer += System.currentTimeMillis(), 须要取出timer的值,获取时间戳,进行加法操做,而后结果赋值到timer,这里须要用到两个this变量,由于要访问timer两次,因此能够看到一个新的指令,DUP,DUP的意思就是复制栈顶变量而后入栈,也就是说拷贝多一份this变量,底下的指令已经分析过了,就再也不赘述。 到这里,还没完成,由于timer变量还没生成呢,类变量的生成就要依赖ClassVisitor了, 拦截ClassVisitor的visitEnd方法,动态增长变量,以下:
@Override
public void visitEnd() {
cv.visitField(Opcodes.ACC_PRIVATE, "timer", Type.getDescriptor(long.class), null, null);
super.visitEnd();
}
复制代码
代码已经上传 AsmDemo
这里介绍的只是动态修改类的冰山一角,动态生成类的应用场景不少,像市面上的路由框架,热修复框架,不少都是利用了动态修改类的方式进行代码的注入,因此路还很长,还需更加努力。