当前位置: 首页 > 知识库问答 >
问题:

使用Javassist创建静态构造函数的克隆

邹海超
2023-03-14

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

但是,该副本还包括(公共/私有)静态最终字段。例如,以下类的静态构造函数:

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

因此,静态构造函数的精确副本也将包括对最终字段“名称”的调用。

有没有办法创建不包含对final字段的调用的静态构造函数的副本?

--谢谢

共有1个答案

鲁洋
2023-03-14

作为你的驱动器,重置类的静态状态,但删除最终字段,关键是ExprEditor类。这个类基本上允许您使用Java辅助的高级应用编程接口轻松转换某些操作,而不必费心处理所有字节码。

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

让我们抓住您的示例类,但有一个转折点:

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

我添加了一个不是最终的静态字段,所以我们可以更改它,并且应该能够重置它。我还添加了一个带有一些代码的静态块,在本例中,它只是一个系统。但请记住,其他类可能有不打算多次运行的代码,您可能会发现自己正在调试奇怪的行为(但我相信您可能知道这一点)。

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

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

主要目标是让这张照片再次出现在保罗身上(是的,我知道有时候我会过于以自我为中心!:P)

我们必须问自己的第一个问题是静态初始值设定项中发生了什么?为此,我们将使用javap通过以下命令获取示例的类字节码:

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

如果您不习惯开关,请快速记下开关。

  • c:显示字节码
  • l:显示局部变量表
  • v:详细显示行表、异常表等
  • 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

查看这段代码,我们看到我们的目标是stackframe 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关键字的非法分配吗。这意味着你已经可以在没有“问题”的情况下运行这个方法了,对吗?我之所以说“问题”,是因为如果你希望最后一个变量具有某种永久状态,你会遇到很多麻烦:)

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

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("...);
    }
  }
}

克隆静态构造函数后,我们使用一个编辑字段访问的ExprEditor进行插入。因此,每当我们发现一个字段访问是对一个静态字段的写入,并且修饰符是最终的,我们就用“{}”替换代码,它基本上翻译成“不做任何事情”。

运行新喷油器并检查字节码时,我们得到以下信息:

  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()方法,那将是一个更好的选择,但我很确定你可能无法做到这一点,这就是为什么你要研究字节码解决方案。

很抱歉发了这么长的帖子,但我想引导你完成我的整个思考过程。希望对您有所帮助。

 类似资料:
  • 本文向大家介绍C#静态构造函数?相关面试题,主要包含被问及C#静态构造函数?时的应答技巧和注意事项,需要的朋友参考一下 最先被执行的构造函数,且在一个类里只允许有一个无参的静态构造函数 执行顺序:静态变量>静态构造函数>实例变量>实例构造函数  

  • 问题内容: 我一直认为无需调用构造函数即可创建对象。 但是,在 明智地 阅读《有效的Java 项目11:覆盖克隆》时 ,我发现了一条声明,指出 “没有调用构造函数”的规定太强了。行为良好的克隆方法可以调用构造函数来创建正在构建的克隆内部的对象。如果该类是最终的,则clone甚至可以返回由构造函数创建的对象。 有人可以向我解释一下吗? 问题答案: 我一直以为clone()会创建一个对象而不调用构造函

  • 我一直认为,clone()创建对象时不需要调用构造函数。 但是,在阅读有效Java第11条:明智地覆盖克隆时,我发现了一条声明,上面写着 “不调用构造函数”的规定太强了。行为良好的克隆方法可以调用构造函数来创建正在构建的克隆内部的对象。如果类是最终的,克隆甚至可以返回构造函数创建的对象。 谁能给我解释一下吗?

  • 问题内容: 我正在学习“ 有效的Java” ,书中并未建议我认为一些标准的东西,例如对象的创建,我的印象是构造函数是最好的方法,书中说我们应该利用在静态工厂方法中,我不能说出一些优点,而在缺点中却有缺点,所以我在问这个问题,这是使用它的好处。 优点: 静态工厂方法的一个优点是,与构造函数不同,它们具有名称。 静态工厂方法的第二个优点是,与构造函数不同,不需要每次调用它们时都创建一个新对象。 静态工

  • 问题内容: 按照标准书,构造函数是用于初始化对象的一种特殊类型的函数。由于构造函数被定义为一个函数,并且内部类函数只能具有两种类型的静态或非静态类型。我怀疑是什么构造函数? 我的疑问是如果构造函数是静态方法,那么我们如何在构造函数内部频繁使用此方法 输出是否意味着构造函数是非静态的? 问题答案: 您的第二个例子很重要。引用在构造函数中可用,这意味着构造函数是针对某个对象(当前正在创建的对象)执行的

  • 问题内容: 我最近发现了一种在Google Guava和Project Lombok中 创建对象的新实例的有趣方法:将构造函数隐藏在静态creator方法后面。这意味着您要做的不是执行操作。 我的问题是为什么?隐藏构造函数有什么好处?在我看来,这样做绝对没有优势,而且似乎违反了基本的对象创建原则。由于开始时您使用而不是某种方法创建对象。这似乎是为了创建方法而创建方法。 您从中得到什么? 问题答案: