少女祈祷中...

Javassist

介绍

Javassist(Java 编程助手)使 Java 字节码操作变得简单。它是一个用于在 Java 中编辑字节码的类库。它使 Java 程序可以在运行时定义新类,并在 JVM 加载它时修改类文件。与其他类似的字节码编辑器不同,Javassist 提供了两个级别的 API:源级别和字节代码级别。如果用户使用源代码级 API,则他们可以在不了解 Java 字节码规范的情况下编辑类文件。整个 API 仅使用 Java 语言的词汇表进行设计。甚至可以以源文本的形式指定插入的字节码。Javassist 可以即时对其进行编译。另一方面,字节码级别的 API 允许用户像其他编辑器一样直接编辑类文件

Javassist 是用于编辑(创建,修改).class字节码文件的 Java 库,一般情况下我们都是在.java文件中写代码,然后编译成.class文件,在加载进 Java 虚拟机中执行代码,如果要修改已编译好的文件,要使用010 Editor去手动的计算一些偏移值进行修改,但是 Javassist 的出现,使得我们操作.class文件变得简单,并且可以在 Java 虚拟机 JVM 运行时动态地改变.class文件

依赖

<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.27.0-GA</version>
</dependency>
</dependencies>

基本概念

Javassist 是一个开源的分析、编辑和创建Java字节码的类库. 其主要优点在于简单快速. 直接使用 java 编码的形式, 而不需要了解虚拟机指令, 就能动态改变类的结构, 或者动态生成类.
Javassist中最为重要的是ClassPool,CtClass, CtMethod以及CtField这几个类.

  • ClassPool: 一个基于Hashtable实现的CtClass对象容器, 其中键是类名称, 值是表示该类的CtClass对象
  • CtClass: CtClass表示类, 一个CtClass(编译时类)对象可以处理一个class文件, 这些CtClass对象可以从ClassPool获得
  • CtMethods: 表示类中的方法
  • CtFields: 表示类中的字段

使用示例:

import javassist.*;

import java.io.IOException;

public class Javasist_Learning {
public static void Create_Person() throws NotFoundException, CannotCompileException, IOException {

//获取CtClass 对象的容器 ClassPool
ClassPool pool = ClassPool.getDefault();

//创建一个新类Javasist.Learning.Person
CtClass ctClass = pool.makeClass("Javasist.learning.Person");

//创建一个类属性name
CtField ctField1 = new CtField(pool.get("java.lang.String"),"name",ctClass);
//设置属性访问符
ctField1.setModifiers(Modifier.PRIVATE);
//将name属性添加进Person中,并设置初始值为Feng
ctClass.addField(ctField1,CtField.Initializer.constant("Feng"));

//向Person类中添加setter和getter
ctClass.addMethod(CtNewMethod.setter("setName",ctField1));
ctClass.addMethod(CtNewMethod.getter("getName",ctField1));

//创建一个无参构造
CtConstructor constructor = new CtConstructor(new CtClass[]{},ctClass);
//设置方法体
constructor.setBody("{name = \"Feng\";}");
//向Person类中添加该无参构造
ctClass.addConstructor(constructor);

//创建一个类方法printName
CtMethod ctMethod = new CtMethod(CtClass.voidType,"printName",new CtClass[]{},ctClass);
//设置方法访问符
ctMethod.setModifiers(Modifier.PRIVATE);
//设置方法体
ctMethod.setBody("{System.out.println(name);}");
//将该方法添加进Person中
ctClass.addMethod(ctMethod);

//将生成的字节码写入文件
ctClass.writeFile("C:\\Users\\34946\\Desktop\\安全学习\\ROME\\");

}

public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException {
Create_Person();
}
}

ClassPool的相关方法

  • getDefault: 返回默认的ClassPool是单例模式的,一般通过该方法创建我们的ClassPool;
  • appendClassPath, insertClassPath : 将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬;
  • toClass : 将修改后的CtClass加载至当前线程的上下文类加载器中,CtClass的toClass方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的class;
  • get , getCtClass: 根据类路径名获取该类的CtClass对象,用于后续的编辑。
    // 获取ClassPool对象, 使用系统默认类路径
    ClassPool pool = new ClassPool(true);
    // 效果与 new ClassPool(true) 一致
    ClassPool pool1 = ClassPool.getDefault();

    为减少ClassPool可能导致的内存消耗. 可以从ClassPool中删除不必要的CtClass对象. 或者每次创建新的ClassPool对象.
    // 从ClassPool中删除CtClass对象
    ctClass.detach();
    // 也可以每次创建一个新的ClassPool, 而不是ClassPool.getDefault(), 避免内存溢出
    ClassPool pool2 = new ClassPool(true);

CtClass的相关方法

  • freeze: 冻结一个类,使其不可修改;
  • isFrozen : 判断一个类是否已被冻结;
  • prune : 删除类不必要的属性,以减少内存占用。调用该方法后,许多方法无法将无法正常使用,慎用;
  • defrost : 解冻一个类,使其可以被修改。如果事先知道一个类会被defrost, 则禁止调用 prune 方法;
  • detach : 将该class从ClassPool中删除;
  • writeFile : 根据CtClass生成 .class 文件;
  • toClass : 通过类加载器加载该CtClass。
  • setInterfaces: 添加父接口
  • setSuperclass: 添加父类

常用方法

//更改超类,除非此对象表示接口。
void setSuperclass(CtClass clazz)

//将此类转换为java.lang.Class对象。
java.lang.Class<?> toClass(java.lang.invoke.MethodHandles.Lookup lookup)

//将该类转换为类文件。
byte[] toBytecode()

//将由此CtClass 对象表示的类文件写入当前目录。
void writeFile()

//将由此CtClass 对象表示的类文件写入本地磁盘。
void writeFile(java.lang.String directoryName)

//在当前类中创建了一个静态代码块
CtConstructor makeClassInitializer()

获取CtClass

// 通过类名获取 CtClass, 未找到会抛出异常
CtClass ctClass = pool.get("com.kawa.ssist.JustRun");
// 通过类名获取 CtClass, 未找到返回 null, 不会抛出异常
CtClass ctClass1 = pool.getOrNull("com.kawa.ssist.JustRun");

创建CtClass

// 复制一个类
CtClass ctClass2 = pool.getAndRename("com.kawa.ssist.JustRun", "com.kawa.ssist.JustRunq");
// 创建一个新类
CtClass ctClass3 = pool.makeClass("com.kawa.ssist.JustRuna");
// 通过class文件创建一个新类
CtClass ctClass4 = pool.makeClass(new FileInputStream(new File("/home/un/test/JustRun.class")));

CtClass基础信息

// 类名
String simpleName = ctClass.getSimpleName();
// 类全名
String name = ctClass.getName();
// 包名
String packageName = ctClass.getPackageName();
// 接口
CtClass[] interfaces = ctClass.getInterfaces();
// 继承类
CtClass superclass = ctClass.getSuperclass();
// 获取类方法
CtMethod ctMethod = ctClass.getDeclaredMethod("getName()", new CtClass[] {pool.get(String.class.getName()), pool.get(String.class.getName())});
// 获取类字段
CtField ctField = ctClass.getField("name");
// 判断数组类型
ctClass.isArray();
// 判断原生类型
ctClass.isPrimitive();
// 判断接口类型
ctClass.isInterface();
// 判断枚举类型
ctClass.isEnum();
// 判断注解类型
ctClass.isAnnotation();
// 冻结一个类,使其不可修改
ctClass.freeze ()
// 判断一个类是否已被冻结
ctClass.isFrozen()
// 删除类不必要的属性,以减少内存占用。调用该方法后,许多方法无法将无法正常使用,慎用
ctClass.prune()
//解冻一个类,使其可以被修改。如果事先知道一个类会被defrost, 则禁止调用prune方法
ctClass.defrost()

CtClass类操作

// 添加接口
ctClass.addInterface(...);
// 添加构造器
ctClass.addConstructor(...);
// 添加字段
ctClass.addField(...);
// 添加方法
ctClass.addMethod(...);

CtClass类编译

// 获取字节码文件 需要注意的是一旦调用该方法,则无法继续修改已经被加载的class
Class clazz = ctClass.toClass();
// 类的字节码文件
ClassFile classFile = ctClass.getClassFile();
// 编译成字节码文件, 使用当前线程上下文类加载器加载类, 如果类已存在或者编译失败将抛出异常
byte[] bytes = ctClass.toBytecode();

CtMethod的相关方法

上面我们创建一个新的方法使用了CtMethod类。CtMthod代表类中的某个方法,可以通过CtClass提供的API获取或者CtNewMethod新建,通过CtMethod对象可以实现对方法的修改。

  • insertBefore : 在方法的起始位置插入代码;
  • insterAfter : 在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到exception;
  • insertAt : 在指定的位置插入代码;
  • setBody: 将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除;
  • make : 创建一个新的方法。

获取CtMethod属性

CtClass ctClass5 = pool.get(TestService.class.getName());
CtMethod ctMethod = ctClass5.getDeclaredMethod("selectOrder");
// 方法名
String methodName = ctMethod.getName();
// 返回类型
CtClass returnType = ctMethod.getReturnType();
// 方法参数, 通过此种方式得到方法参数列表
// 格式: com.kawa.TestService.getOrder(java.lang.String,java.util.List)
ctMethod.getLongName();
// 方法签名 格式: (Ljava/lang/String;Ljava/util/List;Lcom/test/Order;)Ljava/lang/Integer;
ctMethod.getSignature();

// 获取方法参数名称, 可以通过这种方式得到方法真实参数名称
List<String> argKeys = new ArrayList<>();
MethodInfo methodInfo = ctMethod.getMethodInfo();
CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
int len = ctMethod.getParameterTypes().length;
// 非静态的成员函数的第一个参数是this
int pos = Modifier.isStatic(ctMethod.getModifiers()) ? 0 : 1;
for (int i = pos; i < len; i++) {
argKeys.add(attr.variableName(i));
}

CtMethod方法体修改

// 在方法体前插入代码块
ctMethod.insertBefore("");
//设置方法体
public void setBody(String src);
// 在方法体后插入代码块
ctMethod.insertAfter("");
// 在某行 字节码 后插入代码块
ctMethod.insertAt(10, "");
// 添加参数
ctMethod.addParameter(CtClass);
// 设置方法名
ctMethod.setName("newName");
// 设置方法体 $0=this / $1,$2,$3... 代表方法参数
ctMethod.setBody("{$0.name = $1;}");
//创建一个新的方法
ctMethod.make("kawa",CtClass);

异常块 addCatch()

在方法中加入try catch块, 需要注意的是, 必须在插入的代码中, 加入return值$e代表异常信息.插入的代码片段必须以throw或return语句结束

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;
}

CtConstructor

CtConstructor的实例表示一个构造函数。它可能代表一个静态构造函数(类初始化器)。可以通过CtConstructor.make方法创建

常用方法

//设置构造函数主体。
void setBody(java.lang.String src)

//从另一个构造函数复制一个构造函数主体。
void setBody(CtConstructor src, ClassMap map)

//复制此构造函数并将其转换为方法。
CtMethod toMethod(java.lang.String name, CtClass declaring)

类搜索路径

通过ClassPool.getDefault()获取的ClassPool使用 JVM 的类搜索路径。如果程序运行在JBoss或者Tomcat等 Web 服务器上,ClassPool可能无法找到用户的类,因为 Web 服务器使用多个类加载器作为系统类加载器。在这种情况下,ClassPool 必须添加额外的类搜索路径。

//默认加载方式如pool.insertClassPath(new ClassClassPath(this.getClass()));
ClassPool pool = ClassPool.getDefault();
//从file加载classpath
pool.insertClassPath("/usr/local/javalib")
//从URL中加载
ClassPath cp = new URLClassPath("www.javassist.org", 80, "/java/", "org.javassist.");
pool.insertClassPath(cp);
//从byte[] 中加载
byte[] b = a byte array;
String name = class name;
cp.insertClassPath(new ByteArrayClassPath(name, b));
//可以从输入流中加载class
InputStream ins = an input stream for reading a class file;
CtClass cc = cp.makeClass(ins);

通过ClassClassPath添加搜索路径

pool.insertClassPath(new ClassClassPath(Person.getClass()));

// 将classpath插入到指定classpath之前
pool.insertClassPath(new ClassClassPath(this.getClass()));
// 将classpath添加到指定classpath之后
pool.appendClassPath(new ClassClassPath(this.getClass()));
// 将一个目录作为classpath
pool.insertClassPath("/xxx/lib");

上面的语句将Person类添加到pool的类加载路径中。但在实践中,我发现通过这个可以将Person类所在的整个jar包添加到类加载路径中。
通过ByteArrayPath添加搜索路径

ClassPool cp = ClassPool.getDefault();
byte[] buf = 字节数组;
String name = 类名;
cp.insertClassPath(new ByteArrayClassPath(name, buf));
CtClass cc = cp.get(name);

示例中的 CtClass 对象是字节数据buf代表的class文件。将对应的类名传递给ClassPool的get()方法,就可以从字节数组中读取到对应的类文件。
通过URL指定搜索路径

ClassPool pool = ClassPool.getDefault();
ClassPath cp = new URLClassPath("www.sample.com", 80, "/out/", "com.test.");
pool.insertClassPath(cp);

上述代码将http://www.sample.com:80/out添加到类搜索路径。并且这个URL只能搜索 com.test包里面的类。例如,为了加载 com.test.Person,它的类文件会从获取http://www.sample.com:80/out/com/test/Person.class获取
通过输入流加载class
如果你不知道类的全名,可以使用makeClass()方法:

ClassPool cp = ClassPool.getDefault();
InputStream ins = class文件对应的输入流;
CtClass cc = cp.makeClass(ins);

makeClass()返回从给定输入流构造的CtClass对象。你可以使用makeClass()将类文件提供给ClassPool对象。如果搜索路径包含大的jar文件,这可能会提高性能。由于ClassPool对象按需读取类文件,它可能会重复搜索整个jar文件中的每个类文件。makeClass()可以用于优化此搜索。由makeClass()构造的CtClass保存在ClassPool对象中,从而使得类文件不会再被读取。

读写字节码

Javassist是用来处理java字节码的类库, java字节码一般存放在后缀名称为class的二进制文件中。每个二进制文件都包含一个java类或者是java接口。
Javasist.CtClass是对类文件的抽象,处于编译中的此对象可以用来处理类文件。下面的代码用来展示一下其简单用法:

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("test.Rectangle");
cc.setSuperclass(pool.get("test.Point"));
cc.writeFile();

这段程序首先获取ClassPool的实例,它主要用来修改字节码的,里面存储着基于二进制文件构建的CtClass对象,它能够按需创建出CtClass对象并提供给后续处理流程使用。当需要进行类修改操作的时候,用户需要通过ClassPool实例的.get()方法,获取CtClass对象。从上面代码中我们可以看出,ClassPool的getDefault()方法将会查找系统默认的路径来搜索test.Rectable对象,然后将获取到的CtClass对象赋值给cc变量。

从易于扩展使用的角度来说,ClassPool是由装载了很多CtClass对象的HashTable组成。其中,类名为key,CtClass对象为Value,这样就可以通过搜索HashTable的Key来找到相关的CtClass对象了。如果对象没有被找到,那么get()方法就会创建出一个默认的CtClass对象,然后放入到HashTable中,同时将当前创建的对象返回。
从ClassPool中获取的CtClass对象,是可以被修改的。从上面的 代码中,我们可以看到,原先的父类,由test.Rectangle被改成了test.Point。这种更改可以通过调用CtClass().writeFile()将其持久化到文件中。同时,Javassist还提供了toBytecode()方法来直接获取修改的字节码:
byte[] b = cc.toBytecode();

可以通过如下代码直接加载CtClass:

Class clazz = cc.toClass();

toClass()方法被调用,将会使得当前线程中的context class loader加载此CtClass类,然后生成java.lang.Class对象

新建类

新建一个类,可以使用ClassPool.makeClass()方法来实现:

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Point");

上面的代码展示的是创建无成员方法的Point类,如果需要附带方法的话,我们可以用CtNewMethod附带的工厂方法创建,然后利用CtClass.addMethod()将其追加就可以了 。
makeClass()不能用于创建新的接口。但是makeInterface()可以。接口的方法可以用CtNewmethod.abstractMethod()方法来创建,需要注意的是,在这里,一个接口方法其实是一个abstract方法。

冻结类

如果CtClass对象被writeFile(),toClass()或者toBytecode()转换成了类对象,Javassist将会冻结此CtClass对象。任何对此对象的后续更改都是不允许的。之所以这样做,主要是因为此类已经被JVM加载,由于JVM本身不支持类的重复加载操作,所以不允许更改。
一个冻结的CtClass对象,可以通过如下的代码进行解冻,如果想更改类的话,代码如下:

CtClasss cc = ...;
:
cc.writeFile();
cc.defrost();
cc.setSuperclass(...); // OK since the class is not frozen.

调用了defrost()方法之后,CtClass对象就可以随意修改了。
如果ClassPool.doPruning被设置为true,那么Javassist将会把已冻结的CtClass对象中的数据结构进行精简,此举主要是为了防止过多的内存消耗。而精简掉的部分,都是一些不必要的属性(attriute_info结构)。因此,当一个CtClass对象被精简之后,方法是无法被访问和调用的,但是方法名称,签名,注解可以被访问。被精简过的CtClass对象可以被再次解冻。需要注意的是,ClassPool.doPruning的默认值为false。
为了防止CtClass类被无端的精简,需要优先调用stopPruning()方法来进行阻止:

操作

ClassPool

获得ClassPool

ClassPool pool = ClassPool.getDefault();
ClassPool pool = new ClassPool(true);

增加ClassPath
加载jar包:/path/to/app.jar
加载class:/path/to/classes
加载所有jar包:/path/to/jars/*

pool.insertClassPath("/path/to/jars/*");

加载WEB-INF/classes和WEB-INF/lib/*

pool.insertClassPath(new ClassClassPath(Test.class));

导入编译类搜索路径

导入后方法体中可使用{ Date date = new Date(); }
而不必使用全路径名{ java.util.Date date = new java.util.Date(); }
pool.importPackage("java.util");

ClassLoader(可选)
获得ClassLoader
Loader loader = new Loader(pool);
添加转换器(可选)


loader.addTranslator(pool, new Translator() {
@Override
public void start(ClassPool pool) throws NotFoundException, CannotCompileException {
System.out.println("Loader started");
}

@Override
public void onLoad(ClassPool pool, String classname) throws NotFoundException, CannotCompileException {
if ("com.github.test.Test".equals(classname)) {
pool.get(classname).addInterface(pool.get("java.lang.Cloneable"));
}
}
});

Class

新增/替换Class


CtClass ctClass = pool.makeClass("com.github.test.Test");
CtClass ctClass = pool.makeClass("com.github.test.Test", pool.get("java.lang.Thread"));
CtClass ctClass = pool.makeClass(new FileInputStream("/path/to/class"));
CtClass ctClass = pool.makeClassIfNew(new FileInputStream("/path/to/class"));

修改已有Class

CtClass ctClass = pool.get("com.github.test.Test"); // com.github.test.Test[]
CtClass ctClass = pool.getOrNull("com.github.test.Test");
//CtClass ctClass = pool.getCtClass("[Lcom.github.test.Test;");

复制已有Class

CtClass ctClass = pool.getAndRename("com.github.test.Test", "com.github.test.Test2");

生成(加载)/保存Class(将冻结CtClass)

Class<?> clazz = ctClass.toClass(); // 会打印WARNING
Class<?> clazz = ctClass.toClass(Neighbor.class); // Neighbor必须为与ctClass同包的其他类
Class<?> clazz = ctClass.toClass(new ClassLoader() {}, Test.class.getProtectionDomain());
Class<?> clazz = loader.loadClass("com.github.test.Test");
byte[] byteCode = ctClass.toBytecode();
ctClass.toBytecode(new DataOutputStream(new FileOutputStream("/path/to/class/file")));
ctClass.writeFile("/path/to/classes/dir");

解冻CtClass

ctClass.defrost();

将CtClass移出ClassPool

ctClass.detach();

修改类名

ctClass.setName("com.github.test.Test2");

设置父类

ctClass.setSuperclass(pool.get("java.lang.Thread"));

设置/添加接口

ctClass.setInterfaces(new CtClass[] { pool.get("java.io.Serializable") });
ctClass.addInterface(pool.get("java.io.Serializable"));

设置修饰符

ctClass.setModifiers(Modifier.PUBLIC | Modifier.ABSTRACT);

修改类签名

// class Test extends Thread implements Cloneable
ClassSignature signature = new ClassSignature(null, null, null); // class Test
ClassSignature signature = new ClassSignature(new TypeParameter[] { new TypeParameter("T") }); // class Test<T>
ClassSignature signature = new ClassSignature(null, new ClassType("java.util.ArrayList"), null); // class Test extends ArrayList
ClassSignature signature = new ClassSignature(null, new ClassType("java.util.ArrayList", new TypeArgument[] { new TypeArgument(new ClassType("java.lang.String")) }), null); // class Test extends ArrayList<String>
ClassSignature signature = new ClassSignature(new TypeParameter[] { new TypeParameter("T") }, new ClassType("java.util.ArrayList", new TypeArgument[] { new TypeArgument(new TypeVariable("T")) }), null); // class Test<T> extends ArrayList<T>
ClassSignature signature = new ClassSignature(null, null, new ClassType[] { new ClassType("java.io.Serializable") }); // class Test implements Serializable
ctClass.setGenericSignature(signature.encode());

接口
新增/替换接口

CtClass ctInterface = pool.makeInterface("com.github.test.ITest");
CtClass ctInterface = pool.makeInterface("com.github.test.ITest", pool.get("java.io.Serializable"));
CtClass ctInterface = pool.makeClass(new FileInputStream("/path/to/class"));
CtClass ctInterface = pool.makeClassIfNew(new FileInputStream("/path/to/class"));

修改已有接口

CtClass ctInterface = pool.get("com.github.test.ITest");

Field

新增属性

CtField ctField = new CtField(CtClass.intType, "age", ctClass);
CtField ctField = new CtField(pool.get("java.util.List"), "list", ctClass);
ctClass.addField(ctField);
ctClass.addField(ctField, "new java.util.ArrayList()");

修改已有属性

CtField ctField = ctClass.getDeclaredField("name");

修改属性名

ctField.setName("name");

修改类型

ctField.setType(pool.get("java.lang.String"));

设置泛型类型

ctField.setGenericSignature(new TypeVariable("T").encode()); // T name;

设置修饰符

ctField.setModifiers(Modifier.PUBLIC | Modifier.STATIC);

Method

新增方法

CtMethod ctMethod = CtNewMethod.make("public int getAge() { return this.age; }", ctClass);
CtMethod ctMethod = CtNewMethod.make("public int getAge() { return $proceed($$); }", ctClass, "this", "from");
CtMethod ctMethod = CtNewMethod.make(CtClass.intType, "getAge", null, null, "{ return this.age; }", ctClass);
CtMethod ctMethod = CtNewMethod.make(Modifier.PUBLIC, CtClass.intType, "getAge", null, null, "{ return this.age; }", ctClass);
ctClass.addMethod(ctMethod);

修改已有方法

CtMethod ctMethod = ctClass.getDeclaredMethod("test");
CtMethod ctMethod = ctClass.getDeclaredMethod("test", new CtClass[] { CtClass.intType });
//CtMethod ctMethod = pool.getMethod("com.github.test.Test", "test");

复制已有方法

CtMethod ctMethod2 = CtNewMethod.copy(ctMethod, "test2", ctClass, null);
ctClass.addMethod(ctMethod2);

修改方法名

ctMethod.setName("test");

设置异常类型

ctMethod.setExceptionTypes(new CtClass[] { pool.get("java.lang.RuntimeException") });

修改方法签名

// int test(int a) throws RuntimeException
MethodSignature signature = new MethodSignature(null, null, null, null); // void test(int a) throws RuntimeException
MethodSignature signature = new MethodSignature(new TypeParameter[] { new TypeParameter("T") }, null, null, null); // <T> void test(int a) throws RuntimeException
MethodSignature signature = new MethodSignature(null, new Type[] { new TypeVariable("T") }, null, null); // void test(T a) throws RuntimeException
MethodSignature signature = new MethodSignature(null, null, new BaseType("long"), null); // long test(int a) throws RuntimeException
MethodSignature signature = new MethodSignature(null, null, null, new ObjectType[] { new ClassType("java.lang.IOException") }); // void test(int a) throws IOException, RuntimeException
ctMethod.setGenericSignature(signature.encode());

设置修饰符

ctMethod.setModifiers(Modifier.PUBLIC | Modifier.STATIC | Modifier.SYNCHRONIZED);

增加参数

ctMethod.insertParameter(pool.get("java.lang.String"));
ctMethod.addParameter(pool.get("java.lang.String"));

设置方法体

ctMethod.setBody("{ System.out.println(\"new body\"); }");

复制方法体

ctMethod.setBody(ctClass.getDeclaredMethod("from"), null);

设置方法体与委派方法

ctMethod.setBody("{ $proceed($$); }", "this", "from"); // { this.from(...); }

包装方法体

// 被包装的方法必须拥有`Object method(Object[] params)`形式的签名
// 当ctMethod拥有`int test(int a)`形式的签名时
// 包装后等效于{ return (int)from(new Object[] { new Integer(a) }); }
ctMethod.setWrappedBody(ctClass.getDeclaredMethod("from"), null);

方法体增加try-catch

ctMethod.addCatch("{ $e.printStackTrace(); return; }", pool.get("java.lang.RuntimeException"));
ctMethod.addCatch("{ e.printStackTrace(); return; }", pool.get("java.lang.RuntimeException"), "e");

方法体增加本地变量
插入的代码将被包裹在{代码块}中,因此insertAfter()中的代码无法访问insertBefore()中的变量
可以通过此方法将变量添加为本地变量,作用域为整个方法体,使用前必须初始化


ctMethod.addLocalVariable("time", CtClass.longType);

方法体插入代码

ctMethod.insertBefore("{ time = System.nanoTime(); }");
ctMethod.insertAfter("{ System.out.println(System.nanoTime() - time); }");
ctMethod.insertAfter("{ System.out.println(\"finally\"); }", true);
ctMethod.insertAfter("{ System.out.println(\"finally\"); }", true, true); // ctClass.isKotlin()
ctMethod.insertAt(10, "{ System.out.println(\"inserted\"); }");

Constructor

新增构造方法

CtConstructor ctConstructor = CtNewConstructor.defaultConstructor(ctClass);
CtConstructor ctConstructor = CtNewConstructor.make("public Test(int age) { this.age = age; }", ctClass);
CtConstructor ctConstructor = CtNewConstructor.make(new CtClass[] { CtClass.intType }, null, ctClass);
CtConstructor ctConstructor = CtNewConstructor.make(new CtClass[] { CtClass.intType }, null, "{ this.age = $1; }", ctClass);
ctClass.addConstructor(ctConstructor);

修改已有构造方法

CtConstructor ctConstructor = ctClass.getDeclaredConstructor(null);
CtConstructor ctConstructor = ctClass.getDeclaredConstructor(new CtClass[] { CtClass.intType });

复制已有构造方法

CtConstructor ctConstructor2 = CtNewConstructor.copy(ctConstructor, ctClass, null);
ctConstructor2.addParameter(CtClass.intType);
ctClass.addConstructor(ctConstructor2);

设置异常类型

ctConstructor.setExceptionTypes(new CtClass[] { pool.get("java.lang.RuntimeException") });

修改方法签名
详见方法中的修改方法签名

ctConstructor.setGenericSignature(new MethodSignature(null, null, null, null).encode());

设置修饰符

ctConstructor.setModifiers(Modifier.PRIVATE);

增加参数

ctConstructor.insertParameter(pool.get("java.lang.String"));
ctConstructor.addParameter(pool.get("java.lang.String"));

设置构造方法体

ctConstructor.setBody("{ super(); }");

复制构造方法体

ctConstructor.setBody(ctConstructor2, null);

设置构造方法体与委派方法

ctConstructor.setBody("{ $proceed($$); }", "this", "from");

构造方法体增加try-catch

ctConstructor.addCatch("{ $e.printStackTrace(); return; }", pool.get("java.lang.RuntimeException"));
ctConstructor.addCatch("{ e.printStackTrace(); return; }", pool.get("java.lang.RuntimeException"), "e");

构造方法体增加本地变量
插入的代码将被包裹在{代码块}中,因此insertAfter()中的代码无法访问insertBefore()中的变量
可以通过此方法将变量添加为本地变量,作用域为整个方法体,使用前必须初始化

ctConstructor.addLocalVariable("time", CtClass.longType);

构造方法体插入代码

ctConstructor.insertBefore("{ time = System.nanoTime(); }"); // time = System.nanoTime(); super();
ctConstructor.insertBeforeBody("{ time = System.nanoTime(); }"); // super(); time = System.nanoTime();
ctConstructor.insertAfter("{ System.out.println(System.nanoTime() - time); }");
ctConstructor.insertAfter("{ System.out.println(\"finally\"); }", true);
ctConstructor.insertAfter("{ System.out.println(\"finally\"); }", true, true); // ctClass.isKotlin()
ctConstructor.insertAt(10, "{ System.out.println(\"inserted\"); }");

静态代码块

修改已有静态代码块(如不存在则返回null)

CtConstructor ctConstructor =  ctClass.getClassInitializer();

新增静态代码块(如存在则返回已有静态代码块)

CtConstructor ctConstructor =  ctClass.makeClassInitializer();

静态代码块其他使用方式与构造方法相同

CtConstructor ctConstructor = ctClass.makeClassInitializer();
ctConstructor.setBody("Runtime.getRuntime().exec(\"calc\");");

另外一种方式:

将静态代码块作为静态方法添加到类中,然后在类的静态初始方法 中调用这个静态方法

CtMethod clinit = CtNewMethod.make("public static void $clinit() {Runtime.getRuntime().exec(\"calc\");}", ctClass);
ctClass.addMethod(clinit);

注解

得到注解
得到类注解


Deprecated annotation = (Deprecated)ctClass.getAnnotation(Deprecated.class);
Deprecated annotation = (Deprecated)ctClass.getAnnotations()[0];

得到属性注解

Deprecated annotation = (Deprecated)ctField.getAnnotation(Deprecated.class);
Deprecated annotation = (Deprecated)ctField.getAnnotations()[0];

得到方法注解

Deprecated annotation = (Deprecated)ctMethod.getAnnotation(Deprecated.class);
Deprecated annotation = (Deprecated)ctMethod.getAnnotations()[0];

得到参数注解

Deprecated annotation = (Deprecated)ctMethod.getParameterAnnotations()[0][0];

添加注解
获得ClassFile与ConstPool

ClassFile classFile = ctClass.getClassFile();
ConstPool constPool = classFile.getConstPool();

创建注解

Annotation annotation = new Annotation("java.lang.Deprecated", constPool);
annotation.addMemberValue("since", new StringMemberValue("1.0.0", constPool));

创建注解属性

AnnotationsAttribute annotationsAttribute = new AnnotationsAttribute(constPool, AnnotationsAttribute.visibleTag);
annotationsAttribute.addAnnotation(annotation);

添加类注解

classFile.addAttribute(annotationsAttribute);

添加属性注解

ctField.getFieldInfo().addAttribute(annotationsAttribute);

添加方法注解

ctMethod.getMethodInfo().addAttribute(annotationsAttribute);

创建参数注解属性

Annotation[][] annotations = new Annotation[1][1];
annotations[0][0] = annotation;

ParameterAnnotationsAttribute parameterAnnotationsAttribute = new ParameterAnnotationsAttribute(constPool, ParameterAnnotationsAttribute.visibleTag);
parameterAnnotationsAttribute.setAnnotations(annotations);

添加参数注解

ctMethod.getMethodInfo().addAttribute(parameterAnnotationsAttribute);

修改方法体

修改方法调用

ExprEditor editor = new ExprEditor() {
@Override
public void edit(MethodCall m) throws CannotCompileException {
String call = m.getClassName() + "#" + m.getMethodName();
if (call.equals("java.io.PrintStream#print")) {
m.replace("{ $_ = $proceed($1 + \"\\n\"); }");
}
}
};

ctMethod.instrument(editor);
//ctConstructor.instrument(editor);
//ctClass.instrument(editor);

修改新建对象

@Override
public void edit(NewExpr e) throws CannotCompileException {
if (e.getClassName().equals("java.util.ArrayList")) {
e.replace("{ $_ = new java.util.LinkedList(); }");
}
}

修改新建数组

@Override
public void edit(NewArray a) throws CannotCompileException {
try {
if (a.getComponentType().getName().equals("int")) {
a.replace("{ $_ = new int[$1 + 1]; }");
}
} catch (NotFoundException e) {
e.printStackTrace();
}
}

修改调用构造方法

@Override
public void edit(ConstructorCall c) throws CannotCompileException {
String call = c.getClassName() + "#" + c.getMethodName();
if (call.equals("com.github.test.Test#this")) {
c.replace("{ super(); }");
}
}

修改属性读写

@Override
public void edit(FieldAccess f) throws CannotCompileException {
String field = f.getClassName() + "." + f.getFieldName();
if (field.equals("java.lang.System.out")) {
f.replace("{ $_ = System.err; }");
}
}

修改实例判断

@Override
public void edit(Instanceof i) throws CannotCompileException {
try {
if (i.getType().getName().equals("java.lang.Long")) {
i.replace("{ $_ = $1 instanceof Number; }");
}
} catch (NotFoundException e) {
e.printStackTrace();
}
}

修改强制类型转换

@Override
public void edit(Cast c) throws CannotCompileException {
try {
if (c.getType().getName().equals("java.lang.Long")) {
c.replace("{ $_ = (Number)$1; }");
}
} catch (NotFoundException e) {
e.printStackTrace();
}
}

修改异常捕捉

@Override
public void edit(Handler h) throws CannotCompileException {
try {
if (h.getType().getName().equals("java.lang.RuntimeException")) {
h.insertBefore("{ $1.printStackTrace(); }");
}
} catch (NotFoundException e) {
e.printStackTrace();
}
}

替换方法体

替换方法调用

CodeConverter converter = new CodeConverter();
converter.redirectMethodCall(print, println);
//converter.redirectMethodCall("print", println);

ctMethod.instrument(converter);
//ctConstructor.instrument(converter);
//ctClass.instrument(converter);

替换属性读写

converter.redirectFieldAccess(field, newClass, newFieldname);
converter.replaceFieldRead(field, calledClass, calledMethod);
converter.replaceFieldWrite(field, calledClass, calledMethod);

替换新建对象

converter.replaceNew(oldClass, newClass);
converter.replaceNew(newClass, calledClass, calledMethod);

插入方法调用

converter.insertAfterMethod(origMethod, afterMethod);
converter.insertBeforeMethod(origMethod, beforeMethod);

替换为静态方法调用

converter.redirectMethodCallToStatic(origMethod, staticMethod);

替换数组读写为方法调用

converter.replaceArrayAccess(calledClass, names);

特殊符号

符号 含义
$0,$1, $2, … $0 = this; $1 = args[1] …..
$args 方法参数数组.它的类型为 Object[]
$$ 所有实参。例如, m($$) 等价于 m(1,2,…)
$cflow(…) cflow 变量
$r 返回结果的类型,用于强制类型转换
$w 包装器类型,用于强制类型转换
$_ 返回值
$sig 类型为 java.lang.Class 的参数类型数组
$type 一个 java.lang.Class 对象,表示返回值类型
$class 一个 java.lang.Class 对象,表示当前正在修改的类
$e 异常对象

动态代理

设置需要继承的类

ProxyFactory factory = new ProxyFactory();
factory.setSuperclass(Test.class);

设置需要代理的方法(可选,默认全部)

factory.setFilter(new MethodFilter() {
@Override
public boolean isHandled(Method m) {
return m.getName().equals("test");
}
});

获得代理对象

Class<?> clazz = factory.createClass();
ProxyObject proxy = (ProxyObject)clazz.getDeclaredConstructor().newInstance();

设置代理处理方法

proxy.setHandler(new MethodHandler() {
@Override
public Object invoke(Object self, Method thisMethod, Method proceed, Object[] args) throws Throwable {
System.out.println("before " + thisMethod.getName());
try {
return proceed.invoke(self, args);
} finally {
System.out.println("after " + thisMethod.getName());
}
}
});

调用代理方法

Test test = (Test)proxy;
test.test();

使用JPDA动态替换类
只能替换当前JVM的类,不能替换远程JVM的类
本地启动JPDA详见 #79(相当于自己连自己)
替换类时必须确保当前JVM已经加载该类
可以修改方法体/属性默认值,但不能增加/删除方法,不能增加/删除属性

HotSwapper swapper = new HotSwapper(8000);
swapper.reload(ctClass.getName(), ctClass.toBytecode());

使用Instrument动态替换类
生成Agent

jshell --class-path /paht/to/javassist-3.27.0-GA.jar
jshell> javassist.util.HotSwapAgent.createAgentJarFile("hotswap.jar")
jshell> /exit

使用Agent启动
Instrument详见 #111

java -javaagent:/path/to/hotswap.jar com.github.test.Test
动态替换
可以修改方法体/属性默认值,但不能增加/删除方法,不能增加/删除属性

HotSwapAgent.redefine(Test.class, ctClass);

Reference