查看源碼很簡單,一些常用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生成待修改部分的字節碼,然後替換掉原字節碼中相應的字節碼塊,其它文件也需要相應修改,比如常量池等