使用 Javassist 创建静态构造函数的克隆
Create a clone of a static constructor with Javassist
似乎 Javassist 的 API 允许我们创建在 class:
中声明的 class 初始值设定项(即静态构造函数)的精确副本
CtClass cc = ...;
CtConstructor staticConstructor = cc.getClassInitializer();
if (staticConstructor != null) {
CtConstructor staticConstructorClone = new CtConstructor(staticConstructor, cc, null);
staticConstructorClone.getMethodInfo().setName(__NEW_NAME__);
staticConstructorClone.setModifiers(Modifier.PUBLIC | Modifier.STATIC);
cc.addConstructor(staticConstructorClone);
}
但是,该副本还包括 (public/private) 个静态 final 字段。例如下面class:
的静态构造函数
public class Example {
public static final Example ex1 = new Example("__EX_1__");
private String name;
private Example(String name) {
this.name = name;
}
}
实际上是:
static {
Example.ex1 = "__NAME__";
}
因此,静态构造函数的精确副本还将包括对最终字段的调用 "name"。
有什么方法可以创建不包括对 final 字段的调用的静态构造函数的副本吗?
-- 谢谢
简介
作为您的驱动器,重置 class 的静态状态但删除最后的字段,关键是 ExprEditor class。 class 基本上允许您使用 Javassist 的高级 API 轻松转换某些操作,而不必费心处理所有字节码。
即使我们将在高级别完成所有这些工作API我仍然会转储一些字节码,以便我们也可以看到该级别的更改。
工作基地
让我们抓住你的例子class但有一个转折:
public class Example {
public static final Example ex1 = new Example("__EX_1__");
public static String DEFAULT_NAME = "Paulo"; // <-- change 1
private String name;
static {
System.out.println("Class inited"); // <-- change 2
}
public Example(String name) {
this.name = name;
}
}
我添加了一个非最终的静态字段,因此我们可以更改它并且应该能够重置它。我还添加了一个带有一些代码的静态块,在这种情况下,它只是一个 System.out 但请记住,其他 classes 可能具有不打算多次 运行 的代码并且您可能会发现自己在调试奇怪的行为(但我相信您可能已经意识到这一点)。
为了测试我们的修改,我还使用以下代码创建了一个测试 class:
public class Test {
public static void main(String[] args) throws Throwable {
System.out.println(Example.DEFAULT_NAME);
Example.DEFAULT_NAME = "Jose";
System.out.println(Example.DEFAULT_NAME);
try {
reset();
} catch (Throwable t) {
System.out.println("Problems calling reset, maybe not injected?");
}
System.out.println(Example.DEFAULT_NAME);
}
private static void reset() throws Throwable {
Method declaredMethod = Example.class.getDeclaredMethod("__NEW_NAME__", new Class[] {});
declaredMethod.invoke(null, new Object[] {});
}
}
如果我们 运行 这个 class 开箱即用,我们会得到以下输出:
Class inited
Paulo
Jose
Problems calling reset, maybe not injected?
Jose
主要目标 是再次打印 Paulo(是的,我知道有时候我太以自我为中心了!:P)
开始吧
我们要问自己的第一个问题是静态初始化程序中发生了什么?为此,我们将使用 javap 获取示例的 class 字节码,使用以下命令:
javap -c -l -v -p Example.class
快速说明 如果您不习惯开关。
- c: 显示字节码
- l: 显示局部变量 table
- v:详细显示行 table、异常 table 等
- p: 包含私有方法
初始化程序的代码是(我剪掉了其他所有内容):
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=3, locals=0, args_size=0
0: new #1 // class test7/Example
3: dup
4: ldc #13 // String __EX_1__
6: invokespecial #15 // Method "<init>":(Ljava/lang/String;)V
9: putstatic #19 // Field ex1:Ltest7/Example;
12: ldc #21 // String Paulo
14: putstatic #23 // Field DEFAULT_NAME:Ljava/lang/String;
17: getstatic #25 // Field java/lang/System.out:Ljava/io/PrintStream;
20: ldc #31 // String Class inited
22: invokevirtual #33 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
25: return
LineNumberTable:
line 4: 0
line 5: 12
line 10: 17
line 11: 25
LocalVariableTable:
Start Length Slot Name Signature
查看这段代码,我们看到我们的目标是堆栈帧 9,其中 putstatic
完成到我们实际上知道是最终的字段 ex1 中,我们只对更改对这些字段的写入感兴趣,读取应该还在制作中。
所以现在让我们 运行 编写您的注入器并再次检查字节码。在 NEW_NAME() 方法字节码下方:
public static void __NEW_NAME__();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=0, args_size=0
0: new #1 // class test7/Example
3: dup
4: ldc #13 // String __EX_1__
6: invokespecial #15 // Method "<init>":(Ljava/lang/String;)V
9: putstatic #19 // Field ex1:Ltest7/Example;
12: ldc #21 // String Paulo
14: putstatic #23 // Field DEFAULT_NAME:Ljava/lang/String;
17: getstatic #25 // Field java/lang/System.out:Ljava/io/PrintStream;
20: ldc #31 // String Class inited
22: invokevirtual #33 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
25: return
LineNumberTable:
line 4: 0
line 5: 12
line 10: 17
line 11: 25
LocalVariableTable:
Start Length Slot Name Signature
正如预期的那样,Stackframe 9 仍然存在。
好奇心:您知道吗,字节码验证器不会检查有关 final 关键字的非法赋值。这意味着你已经可以 运行 这个方法而不需要 "problems",是不是很狡猾?我说 "problems" 是因为如果您希望最终变量具有某种永久状态,您将会遇到很多麻烦:)
好的,但回到正轨,让我们最终重写您的注入器以执行您想要的操作。这是我修改后的代码:
public class Injector {
public static void main(String[] args) throws Throwable {
CtClass cc = ClassPool.getDefault().get(Example.class.getName());
CtConstructor staticConstructor = cc.getClassInitializer();
if (staticConstructor != null) {
CtConstructor staticConstructorClone = new CtConstructor(staticConstructor, cc, null);
staticConstructorClone.getMethodInfo().setName("__NEW_NAME__");
staticConstructorClone.setModifiers(Modifier.PUBLIC | Modifier.STATIC);
cc.addConstructor(staticConstructorClone);
// Here's the trick :-)
staticConstructorClone.instrument(new ExprEditor() {
@Override
public void edit(FieldAccess f) throws CannotCompileException {
try {
if (f.isStatic() && f.isWriter() && Modifier.isFinal(f.getField().getModifiers())) {
System.out.println("Found field");
f.replace("{ }");
}
} catch (NotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
cc.writeFile("...);
}
}
}
克隆静态构造函数后,我们 instrument 使用编辑字段访问的 ExprEditor。因此,每当我们发现 是对 静态字段 的写入 并且 修饰符是最终的 的字段访问时,我们将代码替换为“{}”,它基本上转换为 "do nothing".
当运行启用新注入器并检查字节码时,我们得到以下信息:
public static void __NEW_NAME__();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=0
0: new #1 // class test7/Example
3: dup
4: ldc #13 // String __EX_1__
6: invokespecial #15 // Method "<init>":(Ljava/lang/String;)V
9: astore_1
10: aconst_null
11: astore_0
12: ldc #21 // String Paulo
14: putstatic #23 // Field DEFAULT_NAME:Ljava/lang/String;
17: getstatic #25 // Field java/lang/System.out:Ljava/io/PrintStream;
20: ldc #31 // String Class inited
22: invokevirtual #33 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
25: return
LineNumberTable:
line 4: 0
line 5: 12
line 10: 17
line 11: 25
LocalVariableTable:
Start Length Slot Name Signature
如您所见,stackframe 9 不再是 putstatic 而是 astore_1,实际上 javassist 注入了 3 个新的 stackframes,从 9 到 11:
9: astore_1
10: aconst_null
11: astore_0
现在,如果我们再次 运行 测试 class,我们将得到以下输出:
Class inited
Paulo
Jose
Class inited
Paulo
结尾注释
请记住,即使在这种沙盒场景中一切正常,但在现实世界中执行这种 魔法 时,它可能会因意外情况而适得其反...您很可能需要创建一个更智能的 ExprEditor 来处理更多场景,但您的基点将是这个。
如果您真的可以实现 resetState() 方法,那将是更好的选择,但我敢肯定您可能无法做到,这就是您研究字节码解决方案的原因。
很抱歉 post,但我想引导您完成我所有的思考过程。希望对您有所帮助。
似乎 Javassist 的 API 允许我们创建在 class:
中声明的 class 初始值设定项(即静态构造函数)的精确副本CtClass cc = ...;
CtConstructor staticConstructor = cc.getClassInitializer();
if (staticConstructor != null) {
CtConstructor staticConstructorClone = new CtConstructor(staticConstructor, cc, null);
staticConstructorClone.getMethodInfo().setName(__NEW_NAME__);
staticConstructorClone.setModifiers(Modifier.PUBLIC | Modifier.STATIC);
cc.addConstructor(staticConstructorClone);
}
但是,该副本还包括 (public/private) 个静态 final 字段。例如下面class:
的静态构造函数public class Example {
public static final Example ex1 = new Example("__EX_1__");
private String name;
private Example(String name) {
this.name = name;
}
}
实际上是:
static {
Example.ex1 = "__NAME__";
}
因此,静态构造函数的精确副本还将包括对最终字段的调用 "name"。
有什么方法可以创建不包括对 final 字段的调用的静态构造函数的副本吗?
-- 谢谢
简介
作为您的驱动器,重置 class 的静态状态但删除最后的字段,关键是 ExprEditor class。 class 基本上允许您使用 Javassist 的高级 API 轻松转换某些操作,而不必费心处理所有字节码。
即使我们将在高级别完成所有这些工作API我仍然会转储一些字节码,以便我们也可以看到该级别的更改。
工作基地
让我们抓住你的例子class但有一个转折:
public class Example {
public static final Example ex1 = new Example("__EX_1__");
public static String DEFAULT_NAME = "Paulo"; // <-- change 1
private String name;
static {
System.out.println("Class inited"); // <-- change 2
}
public Example(String name) {
this.name = name;
}
}
我添加了一个非最终的静态字段,因此我们可以更改它并且应该能够重置它。我还添加了一个带有一些代码的静态块,在这种情况下,它只是一个 System.out 但请记住,其他 classes 可能具有不打算多次 运行 的代码并且您可能会发现自己在调试奇怪的行为(但我相信您可能已经意识到这一点)。
为了测试我们的修改,我还使用以下代码创建了一个测试 class:
public class Test {
public static void main(String[] args) throws Throwable {
System.out.println(Example.DEFAULT_NAME);
Example.DEFAULT_NAME = "Jose";
System.out.println(Example.DEFAULT_NAME);
try {
reset();
} catch (Throwable t) {
System.out.println("Problems calling reset, maybe not injected?");
}
System.out.println(Example.DEFAULT_NAME);
}
private static void reset() throws Throwable {
Method declaredMethod = Example.class.getDeclaredMethod("__NEW_NAME__", new Class[] {});
declaredMethod.invoke(null, new Object[] {});
}
}
如果我们 运行 这个 class 开箱即用,我们会得到以下输出:
Class inited
Paulo
Jose
Problems calling reset, maybe not injected?
Jose
主要目标 是再次打印 Paulo(是的,我知道有时候我太以自我为中心了!:P)
开始吧
我们要问自己的第一个问题是静态初始化程序中发生了什么?为此,我们将使用 javap 获取示例的 class 字节码,使用以下命令:
javap -c -l -v -p Example.class
快速说明 如果您不习惯开关。
- c: 显示字节码
- l: 显示局部变量 table
- v:详细显示行 table、异常 table 等
- p: 包含私有方法
初始化程序的代码是(我剪掉了其他所有内容):
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=3, locals=0, args_size=0
0: new #1 // class test7/Example
3: dup
4: ldc #13 // String __EX_1__
6: invokespecial #15 // Method "<init>":(Ljava/lang/String;)V
9: putstatic #19 // Field ex1:Ltest7/Example;
12: ldc #21 // String Paulo
14: putstatic #23 // Field DEFAULT_NAME:Ljava/lang/String;
17: getstatic #25 // Field java/lang/System.out:Ljava/io/PrintStream;
20: ldc #31 // String Class inited
22: invokevirtual #33 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
25: return
LineNumberTable:
line 4: 0
line 5: 12
line 10: 17
line 11: 25
LocalVariableTable:
Start Length Slot Name Signature
查看这段代码,我们看到我们的目标是堆栈帧 9,其中 putstatic
完成到我们实际上知道是最终的字段 ex1 中,我们只对更改对这些字段的写入感兴趣,读取应该还在制作中。
所以现在让我们 运行 编写您的注入器并再次检查字节码。在 NEW_NAME() 方法字节码下方:
public static void __NEW_NAME__();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=0, args_size=0
0: new #1 // class test7/Example
3: dup
4: ldc #13 // String __EX_1__
6: invokespecial #15 // Method "<init>":(Ljava/lang/String;)V
9: putstatic #19 // Field ex1:Ltest7/Example;
12: ldc #21 // String Paulo
14: putstatic #23 // Field DEFAULT_NAME:Ljava/lang/String;
17: getstatic #25 // Field java/lang/System.out:Ljava/io/PrintStream;
20: ldc #31 // String Class inited
22: invokevirtual #33 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
25: return
LineNumberTable:
line 4: 0
line 5: 12
line 10: 17
line 11: 25
LocalVariableTable:
Start Length Slot Name Signature
正如预期的那样,Stackframe 9 仍然存在。
好奇心:您知道吗,字节码验证器不会检查有关 final 关键字的非法赋值。这意味着你已经可以 运行 这个方法而不需要 "problems",是不是很狡猾?我说 "problems" 是因为如果您希望最终变量具有某种永久状态,您将会遇到很多麻烦:)
好的,但回到正轨,让我们最终重写您的注入器以执行您想要的操作。这是我修改后的代码:
public class Injector {
public static void main(String[] args) throws Throwable {
CtClass cc = ClassPool.getDefault().get(Example.class.getName());
CtConstructor staticConstructor = cc.getClassInitializer();
if (staticConstructor != null) {
CtConstructor staticConstructorClone = new CtConstructor(staticConstructor, cc, null);
staticConstructorClone.getMethodInfo().setName("__NEW_NAME__");
staticConstructorClone.setModifiers(Modifier.PUBLIC | Modifier.STATIC);
cc.addConstructor(staticConstructorClone);
// Here's the trick :-)
staticConstructorClone.instrument(new ExprEditor() {
@Override
public void edit(FieldAccess f) throws CannotCompileException {
try {
if (f.isStatic() && f.isWriter() && Modifier.isFinal(f.getField().getModifiers())) {
System.out.println("Found field");
f.replace("{ }");
}
} catch (NotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
cc.writeFile("...);
}
}
}
克隆静态构造函数后,我们 instrument 使用编辑字段访问的 ExprEditor。因此,每当我们发现 是对 静态字段 的写入 并且 修饰符是最终的 的字段访问时,我们将代码替换为“{}”,它基本上转换为 "do nothing".
当运行启用新注入器并检查字节码时,我们得到以下信息:
public static void __NEW_NAME__();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=0
0: new #1 // class test7/Example
3: dup
4: ldc #13 // String __EX_1__
6: invokespecial #15 // Method "<init>":(Ljava/lang/String;)V
9: astore_1
10: aconst_null
11: astore_0
12: ldc #21 // String Paulo
14: putstatic #23 // Field DEFAULT_NAME:Ljava/lang/String;
17: getstatic #25 // Field java/lang/System.out:Ljava/io/PrintStream;
20: ldc #31 // String Class inited
22: invokevirtual #33 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
25: return
LineNumberTable:
line 4: 0
line 5: 12
line 10: 17
line 11: 25
LocalVariableTable:
Start Length Slot Name Signature
如您所见,stackframe 9 不再是 putstatic 而是 astore_1,实际上 javassist 注入了 3 个新的 stackframes,从 9 到 11:
9: astore_1
10: aconst_null
11: astore_0
现在,如果我们再次 运行 测试 class,我们将得到以下输出:
Class inited
Paulo
Jose
Class inited
Paulo
结尾注释
请记住,即使在这种沙盒场景中一切正常,但在现实世界中执行这种 魔法 时,它可能会因意外情况而适得其反...您很可能需要创建一个更智能的 ExprEditor 来处理更多场景,但您的基点将是这个。
如果您真的可以实现 resetState() 方法,那将是更好的选择,但我敢肯定您可能无法做到,这就是您研究字节码解决方案的原因。
很抱歉 post,但我想引导您完成我所有的思考过程。希望对您有所帮助。