查看源码很简单,一些常用IDE里如idea、eclipse都提供了查看class文件源码的功能,虽然跟源码有些出入(解语法糖等),但功能实现上是一致的,且比源码更贴近于JVM运行时的情况。
有时候我们需要修改源码以满足使用要求,对于java代码生成的字节码重新源码就比较简单了,一种方式是继承然后重写待修改的功能,另一种方式是直接创建一个同名类文件,把反编译的源码复制进去,修改后,将新生成的class文件替换原jar包中的class文件,但有些字节码是由其它语言生成的,反编译后的文件并不能满足java编译语法,也就无法编译成新的class文件,对于这种情况,有种通用的方式就是直接修改字节码来实现。
网上有很多修改字节码的文章,但大都是修改常量池来输出不同的值,本文使用jclasslib直接修改字节码中的源码逻辑,为方便演示,本文的demo可能比较简单,但是这种方法可以用到更复杂的类中,如有需要欢迎留言探讨。
1、java源码
package com.zhanghao.test.jclasslib;
public class JclasslibTest {
public static void main(String[] args) {
int a = 1;
int b = 2;
printMin(a, b);
}
private static void printMin(int a, int b) {
int min = a <= b ? a : b;
System.out.println(min);
}
}
输出两个参数中的较小值:1
目标:通过修改字节码的方式使printMin方法输出较大值:2
2、class文件反编译结果
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.zhanghao.test.jclasslib;
public class JclasslibTest {
public JclasslibTest() {
}
public static void main(String[] args) {
int a = 1;
int b = 2;
printMin(a, b);
}
private static void printMin(int a, int b) {
int min = a <= b ? a : b;
System.out.println(min);
}
}
可以看出,生成的class文件只是比java源码多加了一个默认的无参构造方法(因为demo比较简单,所以未体现编译优化的多种策略)
3、下载安装jclasslib
https://github.com/ingokegel/jclasslib/releases
4、使用jclasslib打开class文件
字节码各部分名称已经很明确类,这里不在过多解释,看我们需要修改的位置Methods->printMin
printMin方法对应的字节码一共为19行
0:将第一个参数入栈(参数a=1)
1:将第二个参数入栈(参数b=2)
2:比较栈顶两int型数值的大小,当结果大于0时跳转到第9行字节码指令(判断条件进入相应逻辑块)
5:将第一个参数入栈(选择a)
6:无条件跳转到第10行字节码指令
9:将第二个参数入栈(选择b)
10:将栈顶int型数值存入第三个本地变量(存储选择结果c)
11:获取指定类的静态域,并将其压入栈顶(获取待运行的实例及方法)
14:将第三个参数入栈(将c入栈以供11中的实例方法运行参数)
15:调用实例方法
18:从当前方法返回void
更多字节码指令可参考 虚拟机字节码指令表
参照虚拟机字节码指令表,如要实现将pringMin输出参数中的较大值,只要把第三条指令if_icmpgt修改成if_icmple即可。
助记符 | if_icmpgt | if_icmple |
---|---|---|
指令含义 | 比较栈顶两int型数值的大小,当结果大于0时跳转 | 比较栈顶两int型数值的大小,当结果小于或等于0时跳转 |
字节码 | 0xa3 | 0xa4 |
有符号型十进制数 | -93 | -92 |
5、修改字节码
package com.zhanghao.test.jclasslib;
import com.alibaba.fastjson.JSON;
import java.io.*;
import org.gjt.jclasslib.io.ClassFileWriter;
import org.gjt.jclasslib.structures.AttributeInfo;
import org.gjt.jclasslib.structures.ClassFile;
import org.gjt.jclasslib.structures.MethodInfo;
import org.gjt.jclasslib.structures.attributes.CodeAttribute;
public class JclasslibModify {
public static void main(String[] args) throws Exception {
String filePath = "/Users/zhanghao/Desktop/jclasslib/JclasslibTest.class";
FileInputStream fis = new FileInputStream(filePath);
DataInput di = new DataInputStream(fis);
ClassFile cf = new ClassFile();
cf.read(di);
System.out.println(JSON.toJSONString(cf));
MethodInfo[] methodInfos = cf.getMethods();
MethodInfo methodInfo = methodInfos[2];
AttributeInfo[] attributeInfos = methodInfo.getAttributes();
CodeAttribute codeAttribute = (CodeAttribute) attributeInfos[0];
byte[] bytes = codeAttribute.getCode();
bytes[2] = -92;
fis.close();
File f = new File(filePath);
ClassFileWriter.writeToFile(f, cf);
}
}
读取class文件,可通过debug或者输出json的方式查看ClassFile的类结构,修改类结构中字节码然后重写文件。
需要用到jclasslib.jar,下载链接:jclasslib.jar
6、查看修改后的结果
package com.zhanghao.test;
public class JclasslibTest {
public JclasslibTest() {
}
public static void main(String[] args) {
int a = 1;
int b = 2;
printMin(a, b);
}
private static void printMin(int a, int b) {
int min = a > b ? a : b;
System.out.println(min);
}
}
本文只是简单的阐述修改字节码的方法,对于实际项目中需要修改字节码时,情况会更为复杂,但是换汤不换药,可以先用java生成待修改部分的字节码,然后替换掉原字节码中相应的字节码块,其它文件也需要相应修改,比如常量池等