之前使用到了Instrumentation来做字节码修改,用到了javaassist,顺便做个笔记,记录一下。
对于动态扩展现有类或接口的二进制字节码,有比较成熟的开源项目提供支持,如CGLib、ASM、Javassist等。其中,CGLib的底层基于ASM实现,是一个高效高性能的生成库;而ASM是一个轻量级的类库,但需要涉及到JVM的操作和指令;相比而言,Javassist要简单的多,完全是基于Java的API,但其性能相比前二者要差一些。
一个简单的示例,如下的代码是动态创建Java类二进制字节码并通过反射调用的示例,可供参考:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 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" ); 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()); Object o = Class.forName("cn.ibm.com.TestClass" ).newInstance(); Method getter = o.getClass().getMethod("getName" ); System.out.println(getter.invoke(o)); Method setter = o.getClass().getMethod("setName" , new Class [] {String.class}); setter.invoke(o, "Adam" ); System.out.println(getter.invoke(o)); 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
读取和输出字节码1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ClassPool pool = ClassPool.getDefault();pool.appendClassPath(new LoaderClassPath (Thread.currentThread().getContextClassLoader())); CtClass cl = null ;try { cl = pool.get("com.zero.test.TestMainInJar" ); byte [] byteArr = cl.toBytecode(); cl.writeFile("/Users/zero/git/xxx/src/main/java/" ); } catch (Exception e) { e.printStackTrace(); } finally { if (cl != null ) { cl.detach(); } }
2.插入source 文本在方法体前或者后 CtMethod 和CtConstructor 提供了 insertBefore()、insertAfter()和 addCatch()方法,它们可以插入一个souce文本到存在的方法的相应的位置。javassist 包含了一个简单的编译器解析这souce文本成二进制插入到相应的方法体里。 Javassist提供了一些特殊的变量来代表方法参数:$1,$2,$args…
2.1 $0, $1, $2, … $0代码的是this,$1代表方法参数的第一个参数、$2代表方法参数的第二个参数,以此类推,$N代表是方法参数的第N个。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void move (int dx, int dy) CtMethod m = cc.getDeclaredMethod("move" );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 \$\$  \$\$是所有方法参数的简写,主要用在方法调用上。例如: ```java 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代表 异常值。比如:
1 2 3 CtMethod m = ...;CtClass etype = ClassPool.getDefault().get("java.io.IOException" );m.addCatch("{ System.out.println($e); throw $e; }" , etype);
实际代码如下:
1 2 3 4 5 6 7 try { the original method body } catch (java.io.IOException e) { System.out.println(e); throw e; }
3、修改方法体 CtMethod 和CtConstructor 提供了 setBody() 的方法,可以替换方法或者构造函数里的所有内容。注意 $_变量不支持。
4、新增一个方法或者field Javassist 允许开发者一个新的方法或者构造方法。新增一个方法,例如:
1 2 3 4 5 CtClass point = ClassPool.getDefault().get("Point" );CtMethod m = CtNewMethod.make( "public int xmove(int dx) { x += dx; }" , point); point.addMethod(m);
在方法中调用其他方法,例如:
1 2 3 4 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
1 2 3 4 5 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);
或者:
1 2 3 CtClass point = ClassPool.getDefault().get("Point" );CtField f = CtField.make("public int z = 0;" , point);point.addField(f);
移除方法或者field,调用removeField()或者removeMethod()即可。
简单示例,统计方法执行时间:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 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); } 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\");" ); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 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) { parmAndValue.put(attr.variableName(i), "$" + (i + 1 )); } else { 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反编译结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 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); } }