之前使用到了Instrumentation來做字節碼修改,用到了javaassist,順便做個筆記,記錄一下。
對於動態擴展現有類或接口的二進制字節碼,有比較成熟的開源項目提供支持,如CGLib、ASM、Javassist等。其中,CGLib的底層基於ASM實現,是一個高效高性能的生成庫;而ASM是一個輕量級的類庫,但需要涉及到JVM的操作和指令;相比而言,Javassist要簡單的多,完全是基於Java的API,但其性能相比前二者要差一些。
一個簡單的示例,如下的代碼是動態創建Java類二進制字節碼並通過反射調用的示例,可供參考:
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtField;
import javassist.CtNewMethod;
import javassist.Modifier;
import javassist.NotFoundException;
import javassist.CtField.Initializer;
public class JavassistGenerator {
public static void main(String[] args) throws CannotCompileException, NotFoundException, InstantiationException, IllegalAccessException, ClassNotFoundException, SecurityException, NoSuchMethodException, IllegalArgumentException, InvocationTargetException {
// 創建類
ClassPool pool = ClassPool.getDefault();
CtClass cls = pool.makeClass("cn.ibm.com.TestClass");
// 添加私有成員name及其getter、setter方法
CtField param = new CtField(pool.get("java.lang.String"), "name", cls);
param.setModifiers(Modifier.PRIVATE);
cls.addMethod(CtNewMethod.setter("setName", param));
cls.addMethod(CtNewMethod.getter("getName", param));
cls.addField(param, Initializer.constant(""));
// 添加無參的構造體
CtConstructor cons = new CtConstructor(new CtClass[] {}, cls);
cons.setBody("{name = \"Brant\";}");
cls.addConstructor(cons);
// 添加有參的構造體
cons = new CtConstructor(new CtClass[] {pool.get("java.lang.String")}, cls);
cons.setBody("{$0.name = $1;}");
cls.addConstructor(cons);
// 打印創建類的類名
System.out.println(cls.toClass());
// 通過反射創建無參的實例,並調用getName方法
Object o = Class.forName("cn.ibm.com.TestClass").newInstance();
Method getter = o.getClass().getMethod("getName");
System.out.println(getter.invoke(o));
// 調用其setName方法
Method setter = o.getClass().getMethod("setName", new Class[] {String.class});
setter.invoke(o, "Adam");
System.out.println(getter.invoke(o));
// 通過反射創建有參的實例,並調用getName方法
o = Class.forName("cn.ibm.com.TestClass").getConstructor(String.class).newInstance("Liu Jian");
getter = o.getClass().getMethod("getName");
System.out.println(getter.invoke(o));
}
}
參考:https://www.cnblogs.com/sunfie/p/5154246.html
- 讀取和輸出字節碼
ClassPool pool = ClassPool.getDefault();
pool.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));
CtClass cl = null;
try {
// 從classpath中查詢該類
cl = pool.get("com.zero.test.TestMainInJar");
// ...
// 獲取二進制格式
byte[] byteArr = cl.toBytecode();
//輸出.class文件到該目錄中
cl.writeFile("/Users/zero/git/xxx/src/main/java/");
} catch (Exception e) {
e.printStackTrace();
} finally {
if (cl != null) {
//把CtClass object 從ClassPool中移除
cl.detach();
}
}
2.插入source 文本在方法體前或者後
CtMethod 和CtConstructor 提供了 insertBefore()、insertAfter()和 addCatch()方法,它們可以插入一個souce文本到存在的方法的相應的位置。javassist 包含了一個簡單的編譯器解析這souce文本成二進制插入到相應的方法體裏。
Javassist提供了一些特殊的變量來代表方法參數:$1,args…
2.1 $0, $1, $2, …
$0代碼的是this,$1代表方法參數的第一個參數、$2代表方法參數的第二個參數,以此類推,$N代表是方法參數的第N個。例如:
//實際方法
void move(int dx, int dy)
//javassist
CtMethod m = cc.getDeclaredMethod("move");
//打印dx,和dy
m.insertBefore("{ System.out.println($1); System.out.println($2); }");
注意:如果javassist改變了$1的值,那實際參數值也會改變。
2.2 $args
$args 指的是方法所有參數的數組,類似Object[],如果參數中含有基本類型,則會轉成其包裝類型。需要注意的時候,$args[0]對應的是$1,而不是$0,$0!=$args[0],$0=this。
2.3 $$
$$是所有方法參數的簡寫,主要用在方法調用上。例如:
//原方法
move(String a,String b)
move($$) 相當於move($1,$2)
如果新增一個方法,方法含有move的所有參數,則可以這些寫:exMove($$, context)
相當於exMove($1, $2, context)
2.4 $_
$_代表的是方法的返回值。
2.5 $sig
$sig指的是方法參數的類型(Class)數組,數組的順序爲參數的順序。
2.6 $class
$class 指的是this的類型(Class)。也就是$0的類型。
2.7 addCatch()
addCatch() 指的是在方法中加入try catch 塊,需要主要的是,必須在插入的代碼中,加入return 值。$e代表 異常值。比如:
CtMethod m = ...;
CtClass etype = ClassPool.getDefault().get("java.io.IOException");
m.addCatch("{ System.out.println($e); throw $e; }", etype);
實際代碼如下:
try {
the original method body
}
catch (java.io.IOException e) {
System.out.println(e);
throw e;
}
3、修改方法體
CtMethod 和CtConstructor 提供了 setBody() 的方法,可以替換方法或者構造函數裏的所有內容。注意 $_變量不支持。
4、新增一個方法或者field
Javassist 允許開發者一個新的方法或者構造方法。新增一個方法,例如:
CtClass point = ClassPool.getDefault().get("Point");
CtMethod m = CtNewMethod.make(
"public int xmove(int dx) { x += dx; }",
point);
point.addMethod(m);
在方法中調用其他方法,例如:
CtClass point = ClassPool.getDefault().get("Point");
CtMethod m = CtNewMethod.make(
"public int ymove(int dy) { $proceed(0, dy); }",
point, "this", "move");
其效果如下:public int ymove(int dy) { this.move(0, dy); }
新增field
CtClass point = ClassPool.getDefault().get("Point");
CtField f = new CtField(CtClass.intType, "z", point);
f.setModifiers(Modifier.PRIVATE | Modifier.STATIC | Modifier.FINAL);
point.addField(f);
//point.addField(f, "0"); // initial value is 0.
或者:
CtClass point = ClassPool.getDefault().get("Point");
CtField f = CtField.make("public int z = 0;", point);
point.addField(f);
移除方法或者field,調用removeField()或者removeMethod()即可。
簡單示例,統計方法執行時間:
public class TestMain {
public static void main(String[] args) {
ClassPool pool = ClassPool.getDefault();
pool.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));
CtClass cl = null;
try {
cl = pool.get("com.zero.test.TestMainInJar");
CtMethod[] methods = cl.getDeclaredMethods();
for (CtMethod method : methods) {
doMethod(method);
}
// byte[] byteArr = cl.toBytecode();
cl.writeFile("/Users/zero/git/javaagenttest/src/main/java/");
} catch (Exception e) {
e.printStackTrace();
} finally {
if (cl != null) {
cl.detach();
}
}
}
private static void doMethod(CtMethod ctMethod) throws NotFoundException, CannotCompileException {
String methodName = ctMethod.getName();
if(methodName.equals("main")){
return;
}
ctMethod.addLocalVariable("startTimeAgent", CtClass.longType);
ctMethod.insertBefore("startTimeAgent = System.currentTimeMillis();");
String systemPrintStr = null;
LinkedHashMap<String, String> parmAndValue = AssistUtil.getParmAndValue(ctMethod);
if (parmAndValue != null) {
systemPrintStr = AssistUtil.parmSystemPrint(parmAndValue);
}
System.out.println(systemPrintStr);
if (systemPrintStr != null) {
ctMethod.insertAfter("System.out.println(\"cost:\" + (System.currentTimeMillis() - startTimeAgent) + \"ms, \" + " + systemPrintStr + ");");
}
else {
ctMethod.insertAfter("System.out.println(\"cost:\" + (System.currentTimeMillis() - startTimeAgent) + \"ms\");");
}
}
}
public class AssistUtil {
public static String replaceClassName(String className) {
return className.replace("/", ".");
}
public static LinkedHashMap<String, String> getParmAndValue(CtMethod ctMethod) throws NotFoundException {
int parmLength = ctMethod.getParameterTypes().length;
if (parmLength == 0) {
return null;
}
boolean isStatic = Modifier.isStatic(ctMethod.getModifiers());
MethodInfo methodInfo = ctMethod.getMethodInfo();
CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
if (attr == null) {
throw new NotFoundException("LocalVariableAttribute not found in " + ctMethod.getName());
}
LinkedHashMap<String, String> parmAndValue = new LinkedHashMap<>();
String[] paramNames = new String[parmLength];
for (int i = 0; i < paramNames.length; i++) {
if (isStatic) {
// 靜態方法第一個 $0 是 null
parmAndValue.put(attr.variableName(i), "$" + (i + 1));
}
else {
// 非靜態方法第一個 $0 是 this
parmAndValue.put(attr.variableName(i + 1), "$" + (i + 1));
}
}
return parmAndValue;
}
public static String parmSystemPrint(LinkedHashMap<String, String> parmAndValue) {
List<String> formatStringlines = new ArrayList<>();
parmAndValue.forEach((parm, value) ->
formatStringlines.add("\"" + parm + ": \" + " + value)
);
StringBuilder stringBuilder = new StringBuilder(32);
stringBuilder.append(String.join(" + \", \" + ", formatStringlines));
return stringBuilder.toString();
}
}
執行後生成的.class反編譯結果:
package com.zero.test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class TestMainInJar {
private static final Logger LOGGER = LoggerFactory.getLogger(TestMainInJar.class);
public TestMainInJar() {
}
public static void main(String[] args) throws InterruptedException {
boolean var1 = false;
while(true) {
Thread.sleep(1000L);
int number = 3;
test(number);
(new TestMainInJar()).test2();
(new TestMainInJar()).test3(3L, "zero");
}
}
public static void test(int a) throws InterruptedException {
long startTimeAgent = System.currentTimeMillis();
Thread.sleep(1000L);
LOGGER.info("test a");
Object var4 = null;
System.out.println("cost:" + (System.currentTimeMillis() - startTimeAgent) + "ms, " + "a: " + a);
}
public void test2() throws InterruptedException {
long startTimeAgent = System.currentTimeMillis();
System.out.println("test2");
Object var4 = null;
System.out.println("cost:" + (System.currentTimeMillis() - startTimeAgent) + "ms");
}
public void test3(long a, String b) throws InterruptedException {
long startTimeAgent = System.currentTimeMillis();
System.out.println("test3: --- " + a + "," + b);
Object var7 = null;
System.out.println("cost:" + (System.currentTimeMillis() - startTimeAgent) + "ms, " + "a: " + a + ", " + "b: " + b);
}
}