前言
在写序列化数据生成工具时,TemplatesImpl里的bytes[][]往往都是由javassist动态生成的。javassist依赖相较于asm依赖,使用起来更加简单,因为javassist更像是在操作类,而asm就是在操作字节码了。这里介绍一下javassist的基本用法,另外自己在使用javassist时,遇到了不少坑,这里也来总结一下。
基本使用
直接看代码,我写注释了:
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 58 59 60 61 62 63 64 65
| package com.idiot9;
import javassist.*;
public class CreateMyTest { public String address;
public CreateMyTest() { System.out.println("ctConstrutor"); }
public static void main(String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass MyTest = pool.makeClass("MyTest");
CtField name = new CtField(pool.get("java.lang.String"), "name", MyTest); name.setModifiers(Modifier.PRIVATE); MyTest.addField(name, CtField.Initializer.constant("1diot9"));
CtField address = CtField.make("public String address;", MyTest); MyTest.addField(address);
CtConstructor ctConstructor = new CtConstructor(new CtClass[]{}, MyTest); ctConstructor.setBody("System.out.println(\"ctConstrutor\");"); MyTest.addConstructor(ctConstructor);
CtMethod hello = new CtMethod(CtClass.voidType, "hello", new CtClass[]{}, MyTest); hello.setModifiers(Modifier.PUBLIC); hello.setBody("System.out.println(\"hello\" + name);"); MyTest.addMethod(hello);
CtMethod tmp = CtMethod.make(" public void tmp(){\n" + " System.out.println(\"tmp\");\n" + " }", MyTest); MyTest.addMethod(tmp);
CtConstructor staticBlock = MyTest.makeClassInitializer(); staticBlock.setBody("System.out.println(\"staticBlock\");");
String s = "System.out.println(\"staticBlock2\");"; MyTest.makeClassInitializer().insertAfter(s);
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"); MyTest.setSuperclass(superClass);
MyTest.writeFile("./src/main/java/");
}
public void tmp(){ System.out.println("tmp"); } }
|
这里介绍了几种常用的方法:创建字段,创建方法,创建构造方法,创建静态代码块,继承父类
类搜索相关
javassist必须要找到对应的类才能进行动态创建,所以搞清楚它的默认搜索路径和怎么添加路径很重要。下面来简单介绍一下。
首先,会加载rt.jar中的核心包,包括:java.lang包的所有类,java.util,java.net,java.math,java.nio,java.swing,java.awt,java.lang.reflect,java.lang.invoke。注意,这上面的都是加载单个包下的类,不包括子包里的内,这一点很重要。
然后介绍一下第三方依赖的搜索情况。
如果你在pom里导入了依赖,那么你可以通过 pool.get(“org.apache.commons.beanutils.BeanComparator”); 的方式直接获取到类。但是,如果你没有在pom里导入依赖,就需要手动添加类搜索路径,比如添加一个jar包路径:pool.insertClassPath(“D:\repository\commons-beanutils\commons-beanutils\1.9.4\commons-beanutils-1.9.4.jar”); 这样javassist就能找到这个包中的所有类。
除了在get类的时候有搜索问题,在动态创建方法时也会有问题。首先看一下下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class CreateOutPackage { public static void main(String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); pool.insertClassPath("D:\\repository\\commons-beanutils\\commons-beanutils\\1.9.4\\commons-beanutils-1.9.4.jar"); CtClass ctClass = pool.makeClass("Test");
String staticBlock = "BeanComparator beanComparator = new BeanComparator();"; ctClass.makeClassInitializer().insertAfter(staticBlock);
} }
|
是不是觉得没有任何问题?但是运行后会发现报错:
报错原因为:找不到BeanComparator类。这里你可能会想,上面不都insertClassPath了吗,为什么还找不到呢?这是因为,我们没有写全限定名。举个例子,存在两个同名类,com.A和net.A,那么,如果你只写A,javassist怎么知道是哪一个呢?所以我们修改代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class CreateOutPackage { public static void main(String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); pool.insertClassPath("D:\\repository\\commons-beanutils\\commons-beanutils\\1.9.4\\commons-beanutils-1.9.4.jar");
ctClass = pool.makeClass("Test");
String staticBlock = "org.apache.commons.beanutils.BeanComparator beanComparator = new org.apache.commons.beanutils.BeanComparator();"; ctClass.makeClassInitializer().insertAfter(staticBlock);
} }
|
这样一来就没有报错了。
不过,每次都要写全限定名是不是觉得很麻烦?所以,javassist还提供了另外一个方法:pool.importPackage(“org.apache.commons.beanutils”); 这个的效果就相当于每次在类最顶上进行import,回忆一下,当我们import后,是不是只需要写SimpleName就行了?这里importPackage的效果是一样的。
于是,我们可以再修改代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class CreateOutPackage { public static void main(String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); pool.insertClassPath("D:\\repository\\commons-beanutils\\commons-beanutils\\1.9.4\\commons-beanutils-1.9.4.jar"); pool.importPackage("org.apache.commons.beanutils");
CtClass ctClass = pool.makeClass("Test");
String staticBlock = "BeanComparator beanComparator = new BeanComparator();"; ctClass.makeClassInitializer().insertAfter(staticBlock);
}
}
|
这样看起来是不是简洁很多?
遇到的坑
在写SpringInterceptor内存马时,遇到一个很奇怪的点。我定义了一个command方法,并在preHandle方法中调用:
但是这样会报错:
当我把最底下的command方法删除后就行了。我一开始以为是command方法的问题,但是后来发现直接照搬方法体到preHandle里竟然可以,于是也不是command方法的问题。
后来不知怎么灵机一动,把command调用上面的String command改成了String cmd,就好了:
原来这里同一个类里的变量名和方法名不能一样。
参考
https://drun1baby.top/2023/12/07/Java-Agent-%E5%86%85%E5%AD%98%E9%A9%AC%E5%AD%A6%E4%B9%A0/#Javassist