Java之Javassist动态编程
Java之Javassist动态编程
动态编程与javassist
动态编程是相对于静态编程而言的,那二者有什么明显的区别呢?简单的说就是在静态编程中,类型检查是在编译时完成的,而动态编程中类型检查是在运行时完成的。所谓动态编程就是绕过编译过程在运行时进行操作的技术
那么动态编程的出现是为了解决哪些问题呢?个人感觉比如Spring的依赖注入,用到了动态编程,虽然说不用依赖注入也可以,但是会很繁琐,比如动态编程来的便捷,那动态编程感觉就是为了解决某些场景中只使用静态编程显得比较臃肿和笨拙的地方。
而之前接触到的比如反射,动态代理(运行时动态插入代码)都有点动态编程的影子,
下面看下Javassist。
Javassist
是一个开源的分析、编辑和创建Java字节码的类库,Java 字节码存储在称为类文件的二进制文件中。每个类文件包含一个 Java 类或接口。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。其主要的优点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。而个人感觉在安全中最重要的就是在使用Javassist
时我们可以像写Java代码一样直接插入Java代码片段,让我们不再需要关注Java底层的字节码的和栈操作,仅需要学会如何使用Javassist
的API即可实现字节码编辑,类似于可以达到任意代码执行的效果。
Javassist使用
在Javassist中最为重要的是ClassPool
,CtClass
,CtMethod
以及 CtField
这几个类。
ClassPool:一个基于HashMap实现的CtClass对象容器,其中键是类名称,值是表示该类的CtClass对象。默认的ClassPool使用与底层JVM相同的类路径,因此在某些情况下,可能需要向ClassPool添加类路径或类字节。
CtClass:表示一个类,这些CtClass对象可以从ClassPool获得。
CtMethods:表示类中的方法。
CtFields :表示类中的字段。
ClassPool
ClassPool:一个基于HashMap实现的CtClass对象容器,其中键是类名称,值是表示该类的CtClass对象。默认的ClassPool使用与底层JVM相同的类路径,因此在某些情况下,可能需要向ClassPool添加类路径或类字节。
常用方法:
ClassPool getDefault() 返回默认的类池。
ClassPath insertClassPath(String pathname) 在搜索路径的开头插入目录或jar(或zip)文件。
ClassPath insertClassPath(ClassPath cp) ClassPath在搜索路径的开头插入一个对象。
java.lang.ClassLoader getClassLoader() 获取类加载器
CtClass get(java.lang.String classname) 从源中读取类文件,并返回对CtClass 表示该类文件的对象的引用。
ClassPath appendClassPath(ClassPath cp) 将ClassPath对象附加到搜索路径的末尾。
CtClass makeClass(java.lang.String classname) 创建一个新的public类
CtClass
CtClass:表示一个类,一个CtClass
(编译时类)对象可以处理一个class
文件,这些CtClass对象可以从ClassPool获得。
void setSuperclass(CtClass clazz) 更改超类,除非此对象表示接口。
java.lang.Class<?> toClass(java.lang.invoke.MethodHandles.Lookup lookup)
将此类转换为java.lang.Class对象。
byte[] toBytecode() 将该类转换为类文件。
void writeFile() 将由此CtClass 对象表示的类文件写入当前目录。
void writeFile(java.lang.String directoryName) 将由此CtClass 对象表示的类文件写入本地磁盘。
CtConstructor makeClassInitializer() 制作一个空的类初始化程序(静态构造函数)。
CtMethod
CtMethod:表示类中的方法。超类为CtBehavior,很多有用的方法都在CtBehavior
void insertBefore (java.lang.String src)
在正文的开头插入字节码。
void insertAfter (java.lang.String src)
在正文的末尾插入字节码。
void setBody (CtMethod src, ClassMap map)
从另一个方法复制方法体。
CtConstructor
CtConstructor的实例表示一个构造函数。它可能代表一个静态构造函数。
void setBody(java.lang.String src)
设置构造函数主体。
void setBody(CtConstructor src, ClassMap map)
从另一个构造函数复制一个构造函数主体。
CtMethod toMethod(java.lang.String name, CtClass declaring)
复制此构造函数并将其转换为方法。
CtField
CtFields :表示类中的字段。
动态生成类
大致有如下几个步骤
-
获取默认类池
ClassPool classPool = ClassPool.getDefault();
-
创建一个自定义类
CtClass ctClass = classPool.makeClass();
-
添加实现接口or属性or构造方法or普通方法
-
添加接口
ctClass.setInterfaces(new CtClass[]{classPool.makeInterface("java.io.Serializable")});
-
添加属性
//新建一个int类型名为id的成员变量 CtField id = new CtField(CtClass.intType, "id", ctClass); //将id设置为public id.setModifiers(AccessFlag.PUBLIC); //将该id属性"赋值"给ClassDemo ctClass.addField(id);
-
添加构造方法(有参)
//添加有参构造方法 CtConstructor ctConstructor1 = CtNewConstructor.make("public ClassDemo(int id){this.id = id;}", ctClass); ctClass.addConstructor(ctConstructor1);
-
添加方法
CtMethod ctMethod = CtNewMethod.make("public void calcDemo(){java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");}", ctClass); ctClass.addMethod(ctMethod);
-
-
写入磁盘
这里写入磁盘可以用如下两种方法
- javassist自带的
ctClass.writeFile();
可指定绝对路径写入 - 也可转换为byte流通过
FileOutputStream
等写入磁盘
- javassist自带的
-
进行验证:调用方法or属性赋值
-
tips:
- 这里注意
javassist.CannotCompileException
异常: 因为同个 Class 是不能在同个 ClassLoader 中加载两次的,所以在输出 CtClass 的时候需要注意下,可以使用javassist
自带的classloader解决此问题 - 反射时
newInstance()
抛出了java.lang.InstantiationException
异常可能是因为没有写无参构造 - 如果已经加载了通过javassist生成的类,即便是通过反射(如
class.forName()
)或者new
都不是加载一个"新类"
,只有换一个ClassLoader加载才会是生成一个"新类"
- 这里注意
package javassisttest;
import javassist.*;
import javassist.bytecode.AccessFlag;
import java.io.File;
import java.io.FileOutputStream;
public class JavassistDemo01 {
public static void main(String[] args) {
JavassistDemo01 a = new JavassistDemo01();
a.makeClass0();
}
public void makeClass0(){
//获取默认类池
ClassPool classPool = ClassPool.getDefault();
//创建一个类ClassDemo
CtClass ctClass = classPool.makeClass("javassisttest.ClassDemo");
//让该类实现序列化接口
ctClass.setInterfaces(new CtClass[]{classPool.makeInterface("java.io.Serializable")});
try {
//新建一个int类型名为id的成员变量
CtField id = new CtField(CtClass.intType, "id", ctClass);
//将id设置为public
id.setModifiers(AccessFlag.PUBLIC);
//将该id属性"赋值"给ClassDemo
ctClass.addField(id);
//添加无参构造方法
CtConstructor ctConstructor = CtNewConstructor.make("public ClassDemo(){};", ctClass);
ctClass.addConstructor(ctConstructor);
//添加有参构造方法
CtConstructor ctConstructor1 = CtNewConstructor.make("public ClassDemo(int id){this.id = id;}", ctClass);
ctClass.addConstructor(ctConstructor1);
//添加普通方法1
CtMethod ctMethod = CtNewMethod.make("public void calcDemo(){java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");}", ctClass);
ctClass.addMethod(ctMethod);
//添加普通方法2
CtMethod ctMethod1 = CtNewMethod.make("public void hello(){System.out.println(\"Hello Javassist!!!\");}", ctClass);
ctClass.addMethod(ctMethod1);
//将class文件写入磁盘
//转换成字节流
byte[] bytes = ctClass.toBytecode();
//写入磁盘
File classPath = new File(new File(System.getProperty("user.dir"), "/src/main/java/javassisttest/"), "ClassDemo.class");
FileOutputStream fos = new FileOutputStream(classPath);
fos.write(bytes);
fos.close();
//验证-调用方法
//注意这里可能会抛javassist.CannotCompileException异常因为同个 Class 是不能在同个 ClassLoader 中加载两次的,所以在输出 CtClass 的时候需要注意下
//需要通过一个未加载该class的classloader加载即可,为此javassist内置了一个classloader
//获取javassist的classloader
ClassLoader loader = new Loader(classPool);
System.out.println("loading");
//通过该classloader加载才是新的一个class
Class<?> clazz = loader.loadClass("javassisttest.ClassDemo");
//反射调用hello
clazz.getDeclaredMethod("hello").invoke(clazz.newInstance());
//反射调用calc
clazz.getDeclaredMethod("calcDemo").invoke(clazz.newInstance());
} catch (Exception e){
System.out.println(e);
}
}
}
动态获取类方法
- 获取默认类池
ClassPool classPool = ClassPool.getDefault();
- 获取目标类
CtClass cc = cp.get();
- 获取类的方法
CtMethod m = cc.getDeclaredMethod();
- 插入任意代码
m.insertBefore("{java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");}");
- 转换为class对象
Class c = cc.toClass();
- 反射调用对象
JavassistDemo j= (JavassistDemo)c.newInstance();
- 执行方法
j.hello();
JavassistDemo.java
package javassisttest;
public class JavassistDemo {
public void hello(){
System.out.println("hello calc!!!");
}
}
JavassistDemoto01
public static void main(String[] args) {
JavassistDemo01 a = new JavassistDemo01();
try {
a.toGetClass();
} catch (Exception e) {
e.printStackTrace();
}
}
public void toGetClass() throws Exception {
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get("javassisttest.JavassistDemo");
CtMethod m = cc.getDeclaredMethod("hello"); /
m.insertBefore("{java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");}");
Class c = cc.toClass();
JavassistDemo j= (JavassistDemo)c.newInstance();
j.hello();
}
tips:
- 如果目标类未加载过,可以直接调用
toClass()
方法之后new
一个该类的对象即可调用该类。 - 如果目标类已加载过,就需要用上面的方法,通过javassist的ClassLoader去加载后进行调用。
Javassist特殊参数
在动态修改类时,可能会碰到如下代码,这里的$1
代表方法中第一个行参。setBody
会用参数中写的方法体覆盖掉原有的方法
public void makePool(){
ClassPool classPool = ClassPool.getDefault();
try {
CtClass ctClass = classPool.get("javassisttest.Person");
CtMethod hello = ctClass.getDeclaredMethod("hello", new CtClass[]{classPool.get("java.lang.String")});
hello.setBody("{" + "System.out.println(\"你好:\" + $1);" + "}");
ctClass.writeFile();
ctClass.toClass();
new Person().hello("CoLoo");
} catch (NotFoundException e) {
e.printStackTrace();
} catch (CannotCompileException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
其余的参数可参照如下
标识符 | 作用 |
---|---|
0、0、0、1、$2、 3 、 3、 3、… | this和方法参数(1-N是方法参数的顺序) |
$args | 方法参数数组,类型为Object[] |
$$ | 所有方法参数,例如:m($$)相当于m(1,1,1,2,…) |
$cflow(…) | control flow 变量 |
$r | 返回结果的类型,在强制转换表达式中使用。 |
$w | 包装器类型,在强制转换表达式中使用。 |
$_ | 返回的结果值 |
$sig | 类型为java.lang.Class的参数类型对象数组 |
$type | 类型为java.lang.Class的返回值类型 |
$class | 类型为java.lang.Class的正在修改的类 |
参考于:http://www.javassist.org/tutorial/tutorial2.html
获取类信息
public void makePool2(){
ClassPool classPool = ClassPool.getDefault();
try {
CtClass ctClass = classPool.get("javassisttest.Person");
byte[] bytes = ctClass.toBytecode();
System.out.println(bytes.length);
System.out.println(ctClass.getName());
System.out.println(ctClass.getSimpleName());
System.out.println(ctClass.getSuperclass().getName());
System.out.println(Arrays.toString(ctClass.getInterfaces()));
for (CtConstructor constructor : ctClass.getConstructors()) {
System.out.println(constructor);
}
for (CtMethod method : ctClass.getMethods()) {
System.out.println(method);
}
} catch (NotFoundException e) {
e.printStackTrace();
} catch (CannotCompileException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
结语
无论是动态生成类还是动态获取类中某方法并插入任意代码都是很令人眼红的操作,在cc链中也有用到此机制。
ps:弹计算器确实比找女朋友有意思多了。
Reference
https://www.javassist.org/html/index.html
https://juejin.cn/post/6952765170544279566
https://www.cnblogs.com/baiqiantao/p/10235049.html