【问题标题】:Create a clone of a static constructor with Javassist使用 Javassist 创建静态构造函数的克隆
【发布时间】:2016-02-21 16:35:20
【问题描述】:

似乎 Javassist 的 API 允许我们创建一个类中声明的类初始值设定项(即静态构造函数)的完全副本:

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

但是,该副本还包括(公共/私有)静态 final 字段。比如下面这个类的静态构造函数:

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”的调用。

有什么方法可以创建一个不包括对最终字段的调用的静态构造函数的副本?

--谢谢

【问题讨论】:

  • 您好 Jose,您这样做的主要目标是什么?为静态对象中的每个对象创建工厂?我问你这个,因为你可以通过使用 ExprEditor 来实现你的要求,尽管在这种特殊情况下(对我来说)看起来更容易注入一个返回新对象的方法。您能否提供一些有关您正在尝试做的事情的详细信息,或者您只想拥有 ExprEditor 解决方案?
  • 据我所知,有两种“重启”课程的方法。 1)使用自定义类加载器的新实例并在每次我们需要使用它时重新加载类[非常昂贵的解决方案];或 2)创建静态构造函数的副本,并在每次我们想要“重新启动”一个类时调用它[便宜的解决方案]。尽管解决方案 1) 完美运行,但它非常昂贵(就计算时间而言)。因此,我想尝试解决方案 2),但我不知道如何创建静态构造函数的克隆。
  • 定义“重启”。你想重置它的静态内部状态吗?或者您是否期望任何代码修改都可以加载到 VM 中?后者不会仅通过调用静态构造函数来实现,您确实需要使用自定义 ClassLoader。如果这是您想要的第一个,这有点危险但可能。
  • 是的,第一个,重置类的静态内部状态。

标签: java javassist


【解决方案1】:

简介

作为您的驱动器重置类的静态状态但删除最终字段的关键是ExprEditor 类。此类基本上允许您使用 Javassist 的高级 API 轻松转换某些操作,而不必费心处理所有字节码。

即使我们将在高级 API 中完成所有这些工作,我仍然会转储一些字节码,以便我们也可以看到该级别的更改。

工作基地

让我们抓住你的 Example 类,但有一点不同:

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 但请记住,其他类可能具有不打算多次运行的代码,您可能会发现自己在调试奇怪的行为(但我我相信你可能知道这一点)。

为了测试我们的修改,我还使用以下代码创建了一个测试类:

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 inited
Paulo
Jose
Problems calling reset, maybe not injected?
Jose

主要目标是让 Paulo 再次成为这个印刷品(是的,我知道有时我会过于以自我为中心!:P)

开始吧

我们要问自己的第一个问题是静态初始化器中发生了什么?为此,我们将使用 javap 获取 Example 的类字节码,使用以下命令:

javap -c -l -v -p Example.class

快速说明如果您不习惯开关。

  • c: 显示字节码
  • l:显示局部变量表
  • v: be verbose 显示行表、异常表等
  • 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 仍然存在。

好奇心:您知道字节码验证器不会检查有关最终关键字的非法分配。这意味着您已经可以毫无“问题”地运行此方法,对吧?我说“问题”是因为如果您期望最终变量具有某种永久状态,那么您将遇到很多麻烦:)

好的,但回到正轨,让我们最终重写您的注射器以做您想做的事。这是我修改后的代码:

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。因此,每当我们发现是对静态字段的写入并且修饰符是final的字段访问时,我们将代码替换为“ { } " 基本上翻译为“什么都不做”。

当运行新的注入器并检查字节码时,我们得到以下信息:

  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 个新的 stackframe,从 9 到 11:

         9: astore_1
        10: aconst_null
        11: astore_0

现在如果我们再次运行 Test 类,我们会得到以下输出:

Class inited
Paulo
Jose
Class inited
Paulo

结束说明

请记住,即使在这种沙盒场景中一切正常,但在现实世界中执行这种魔法时,它可能会因意外情况而适得其反……你很可能可能需要创建一个更智能的 ExprEditor 来处理更多场景,但您的基点将是这个。

如果您实际上可以实现 resetState() 方法,那将是一个更好的选择,但我很确定您可能无法做到,这就是您正在研究字节码解决方案的原因。

抱歉,这篇文章很长,但我想指导您完成我所有的思考过程。希望对您有所帮助。

【讨论】:

    猜你喜欢
    • 2016-06-19
    • 2014-01-10
    • 2021-08-27
    • 2015-09-10
    • 2013-05-28
    • 2017-09-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多