Java Javassist
前言:CC2所需要学习的知识点以及学习yso源码所需要用到的
参考文章:http://www.javassist.org/
参考文章:https://www.cnblogs.com/rickiyang/p/11336268.html
参考文章:https://y4er.com/post/javassist-learn/
参考文章:https://blog.csdn.net/qq_41918771/article/details/121603885
什么是Javassist
Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态"AOP"框架。
关于java字节码的处理,有很多工具,如bcel,asm。不过这些都需要直接跟虚拟机指令打交道。如果你不想了解虚拟机指令,可以采用javassist。javassist是jboss的一个子项目,其主要的优点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。
首先大家都知道的Java 字节码以二进制的形式存储在 .class 文件中,每一个.class 文件包含一个 Java 类或接口。
Javassist 就是一个用来处理 Java 字节码的类库。它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解。同时也可以去生成一个新的类对象,通过完全手动的方式。
一句话来讲Javassist:Javassist允许Java程序可以在运行时定义一个新的class、在JVM加载时修改class文件。
Maven依赖:
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.22.0-GA</version>
</dependency>
字节码的读写
参考文章:https://blog.csdn.net/zixiao217/article/details/88803631
ClassPool对象:代表class文件的CtClass对象的容器,可以通过该对象来获取想要读取或者修改的类。
CtClass(compile-time class编译时的类):一个处理class文件的句柄。
class Point {
public Point(){
System.out.println(1);
}
}
class Rect extends Point {
public Rect(){
super();
System.out.println(2);
}
}
public class Test {
public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.get("com.java.javassist.test1.Rect");
ctClass.setSuperclass(classPool.get("com.java.javassist.test1.Point"));
ctClass.writeFile("javassistExample");
}
}
添加成员字段属性
ClassPool pool = ClassPool.getDefault(); //获取一个类池
// 动态添加一个字段
CtClass ctClass = pool.makeClass("com.java.javassist.pojo.People");
CtField ageField = new CtField(pool.get("java.lang.String"), "name", ctClass);
ageField.setModifiers(Modifier.PRIVATE); // 还可以设置访问权限
ctClass.addField(ageField);
// 动态添加getter setter方法
ctClass.addMethod(CtNewMethod.setter("setAge", ageField));
ctClass.addMethod(CtNewMethod.getter("getAge", ageField));
构造函数的创建
ClassPool pool = ClassPool.getDefault(); //获取一个类池
// 动态添加一个字段
CtClass ctClass = pool.makeClass("com.java.javassist.pojo.People");
CtField ageField = new CtField(pool.get("java.lang.String"), "name", ctClass);
ageField.setModifiers(Modifier.PRIVATE); // 还可以设置访问权限
ctClass.addField(ageField);
// 动态添加getter setter方法
ctClass.addMethod(CtNewMethod.setter("setAge", ageField));
ctClass.addMethod(CtNewMethod.getter("getAge", ageField));
// 添加无参构造函数
CtConstructor ctConstructor = new CtConstructor(new CtClass[]{}, ctClass);
ctConstructor.setBody("$0.name = \"池灵\";");
ctClass.addConstructor(ctConstructor);
// 添加有参构造函数
CtConstructor ctConstructor1 = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, ctClass);
ctConstructor1.setBody("$0.name = $1;");
ctClass.addConstructor(ctConstructor1);
当如果想要实现有参和无参构造函数方法的时候,$0 和 $1...$n ,这里的$0代表着这个类的this对象,$1则是当前方法的第一个参数,以此类推!
需要注意:在实现有参和无参构造的时候,之后一定要进行setbody,否则为报如下的错误,它会说存在方法是抽象或者不原生的信息!
添加成员方法
这里实现成员方法,然后进行保存字节码
ClassPool pool = ClassPool.getDefault(); //获取一个类池
// 动态添加一个字段
CtClass ctClass = pool.makeClass("com.java.javassist.pojo.People");
CtField ageField = new CtField(pool.get("java.lang.String"), "name", ctClass);
ageField.setModifiers(Modifier.PRIVATE); // 还可以设置访问权限
ctClass.addField(ageField);
// 动态添加getter setter方法
ctClass.addMethod(CtNewMethod.setter("setAge", ageField));
ctClass.addMethod(CtNewMethod.getter("getAge", ageField));
// 添加无参构造函数
CtConstructor ctConstructor = new CtConstructor(new CtClass[]{}, ctClass);
ctConstructor.setBody("$0.name = \"池灵\";");
ctClass.addConstructor(ctConstructor);
// 添加有参构造函数
CtConstructor ctConstructor1 = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, ctClass);
ctConstructor1.setBody("$0.name = $1;");
ctClass.addConstructor(ctConstructor1);
// 添加成员方法 如果没有setBody则是一个抽象方法
CtMethod ctMethod = new CtMethod(CtClass.voidType,"hello", new CtClass[]{pool.get("java.lang.String")}, ctClass);
ctMethod.setModifiers(Modifier.PUBLIC);
ctMethod.setBody("{System.out.println($0.name);}");
ctClass.addMethod(ctMethod);
ctClass.writeFile("javassistExample");
小总结
上面的代码就是我们学习的一个小例子,我们到这里就实现了一个简单的类,我们来观察下字节码文件的反编译内容,可以发现,该生成的代码与我们自己想要实现的完全是一样的!
-
在 Javassist 中,类Javaassit.CtClass表示class文件。
-
一个 GtClass (编译时类)对象可以处理一个class文件
-
ClassPool是CtClass对象的容器。它按需读取类文件来构造CtClass对象,并且保存CtClass对象以便以后使用。
需要注意的是ClassPool会在内存中维护所有被它创建过的CtClass,当 CtClass数量过多时会占用大量的内存,官方API中给出的解决方案是有意识的调用CtClass的detach()方法以释放内存。
关于ClassPool
ClassPool需要关注的方法:
-
getDefault: 返回默认的ClassPool 是单例模式的,一般通过该方法创建我们的ClassPool;
-
appendClassPath, insertClassPath: 将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬;
-
toClass: 将修改后的CtClass加载至当前线程的上下文类加载器中,CtClass的toClass方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的class。
-
get,getCtClass: 根据类路径名获取该类的CtClass对象,用于后续的编辑。
后面在对象的实例化中会讲到!
关于CtMethod
CtMethod继承CtBehavior,需要关注的方法:
- insertBefore:在方法的起始位置插入代码
- insterAfter:在方法的所有 return 语句前插入代码
- insertAt:在指定的位置插入代码
- setBody:将方法的内容设置为要写入的代码,当方法被abstract修饰时,该修饰符被移除
- make:创建一个新的方法
这里就不演示了,部分方法已经演示过了,其他方法其实都差不多的!
对象实例化
这里提供了三种方法,分别是反射方式调用,加载class文件和通过接口
反射方式调用
在前面的代码上继续进行补充,可以看到最后是进行保存了该class字节码,但是我们这里也可以来进行实例化
这里只展示了部分代码,完整代码和上面拼接下即可
// 添加成员方法 如果没有setBody则是一个抽象方法
CtMethod ctMethod = new CtMethod(CtClass.voidType,"hello", new CtClass[]{pool.get("java.lang.String")}, ctClass);
ctMethod.setModifiers(Modifier.PUBLIC);
ctMethod.setBody("{System.out.println($0.name);}");
ctClass.addMethod(ctMethod);
Object o = ctClass.toClass().newInstance();
Method method = o.getClass().getMethod("hello", String.class);
method.invoke(o, "hello");
可以发现成功打印了name属性
加载class文件
我们通过ctClass.writeFile("javassistExample");
来进行保存了文件,这里我们就可以进行派上用场!
下面是我们保存的字节码文件,我们通过这个字节码文件来进行实例化对象
public class Test2 {
public static void main(String[] args) throws NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath("C:\\Users\\dell\\Desktop\\ALL\\javaidea\\MyFirstTestMaven\\CollectionsSerializable\\javassistExample");
CtClass ctClass = classPool.get("com.java.javassist.pojo.People");
Object o = ctClass.toClass().newInstance();
Method method = o.getClass().getMethod("hello", String.class);
method.invoke(o, "hello");
}
}
这里有个注意点:如果当前web路径中也存在一个com.java.javassist.pojo.People,那么就需要用到insertClassPath,当java加载的时候首先找的是class字节码,而不是当前web路径中的类,如果你把insertClassPath改成了appendClassPath那么他就会加载失败!
通过接口
新建一个接口IPerson,将Person类的方法全部抽象出来
public class Test3 {
public static void main(String[] args) throws NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath("C:\\Users\\dell\\Desktop\\ALL\\javaidea\\MyFirstTestMaven\\CollectionsSerializable\\javassistExample");
CtClass IPerson = pool.get("com.java.javassist.pojo.IPeople");
CtClass Person = pool.get("com.java.javassist.pojo.People");
Person.defrost();
Person.setInterfaces(new CtClass[]{IPerson});
Object o = Person.toClass().newInstance();
Method method = o.getClass().getMethod("hello", String.class);
method.invoke(o, "hello");
}
}
AOP编程
直接拿日志记录来进行讲解
想要实现的效果是能够在hello方法的前后进行打印字符==================
通过上面的了解,我们可以下面两条来进行实现
insertBefore 在方法的起始位置插入代码
insterAfter 在方法的所有 return 语句前插入代码
要知道,class在没有进行toClass的之前,我们可以进行任意修改,我们就拿class字节码文件来继续进行修改
代码实现:
public class Aop1 {
public static void main(String[] args) throws NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath("C:\\Users\\dell\\Desktop\\ALL\\javaidea\\MyFirstTestMaven\\CollectionsSerializable\\javassistExample");
CtClass ctClass = classPool.get("com.java.javassist.pojo.People");
ctClass.defrost();
CtMethod ctMethod = ctClass.getDeclaredMethod("hello", new CtClass[]{classPool.get("java.lang.String")});
ctMethod.insertBefore("{System.out.println(\"------ hello before ------\");}");
ctMethod.insertAfter("{System.out.println(\"------ printName after ------\");}");
Object o = ctClass.toClass().newInstance();
Method hello = o.getClass().getMethod("hello",String.class);
hello.invoke(o, "1111");
}
}
20230227补充
我们平常会在代码中看到用javassist,大家有没有思考过一个问题,明明代码好像不用Javassist也可以实现,为什么我们还需要用到Javassist呢?
我不知道别人咋理解的,有一个好处就是比如你反序列化的时候想要写一些需要引用到第三方库的对象转换的字节码数组,但是这个时候就有一个问题了,如果你不用javassist的话还需要将对象生成的class文件手动编码为字符串,然后再以对应的解码方式的逻辑写在反序列化代码中,这样就比较繁琐,而直接通过javassist生成那么就直接一步到位了
在ysoserial中就可以看到,生成对应的字节码都是通过javassist来进行生成的
其中的几个坑点
-
一个是泛型,比如
Map<>
这种形式,如果在javassist中写的话那么就需要是Map/*<>*/
这种写法 -
在编写代码的时候遇到如下报错情况的时候,最好把要用到的类全部都用全限定名来进行编写,否则可能会出现一些问题,默认的话就只有java.lang包下的类编写是不用全限定名的,就比如java.lang.String类,可以直接写成String,但是推荐的还是全部都用全限定名的方式来进行编写,主要硬要写的还事先需要通过importPackage来进行导入包名先
将其修改为全限定名的情况,如下所示
- javassist.CannotCompileException的问题,这种情况是javassist是不支持可变参数的,就比如如下情况
这里就可以通过clazz.getDeclaredMethod("defineClass", new Class[]{byte[].class, Integer.TYPE, Integer.TYPE});来进行解决