【问题标题】:Change private static final field using Java reflection使用 Java 反射更改私有静态最终字段
【发布时间】:2011-03-19 02:45:46
【问题描述】:

我有一个带有 private static final 字段的类,不幸的是,我需要在运行时更改它。

使用反射我得到这个错误:java.lang.IllegalAccessException: Can not set static final boolean field

有什么办法可以改变值吗?

Field hack = WarpTransform2D.class.getDeclaredField("USE_HACK");
hack.setAccessible(true);
hack.set(null, true);

【问题讨论】:

  • 真是个坏主意。我会尝试获取源代码并重新编译(甚至反编译/重新编译)。
  • System.out 是一个公共的静态最终字段,但也可以更改。
  • @irreputable System.out/in/err 是如此“特别”,以至于 Java 内存模型不得不特别提及它们。它们不是应该遵循的示例。
  • 我的意思是在两者之间找到一个 hack 让我的应用程序正常工作,直到负责的 lib 在下一个版本中进行更改,所以我不再需要 hack...
  • 十年前的@Bill K:重新编译它会很棒,但它在已部署的系统上,我只需要修补它,直到我们可以更新已部署的应用程序!

标签: java reflection static private final


【解决方案1】:

假设没有SecurityManager 阻止您这样做,您可以使用setAccessible 绕过private 并重置修饰符以摆脱final,并实际修改private static final 字段。

这是一个例子:

import java.lang.reflect.*;

public class EverythingIsTrue {
   static void setFinalStatic(Field field, Object newValue) throws Exception {
      field.setAccessible(true);

      Field modifiersField = Field.class.getDeclaredField("modifiers");
      modifiersField.setAccessible(true);
      modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);

      field.set(null, newValue);
   }
   public static void main(String args[]) throws Exception {      
      setFinalStatic(Boolean.class.getField("FALSE"), true);

      System.out.format("Everything is %s", false); // "Everything is true"
   }
}

假设没有SecurityException被抛出,上面的代码打印"Everything is true"

这里实际做了如下:

  • main 中的基本 booleantruefalse 自动装箱为引用类型 Boolean“常量”Boolean.TRUEBoolean.FALSE
  • 反射用于将public static final Boolean.FALSE改为引用Boolean.TRUE所引用的Boolean
  • 因此,随后每当 false 自动装箱为 Boolean.FALSE 时,它引用的 BooleanBoolean.TRUE 引用的 Boolean 相同
  • 以前的 "false" 现在都是 "true"

相关问题


注意事项

每当您执行此类操作时都应格外小心。它可能不起作用,因为SecurityManager 可能存在,但即使它不存在,取决于使用模式,它可能会或可能不会起作用。

JLS 17.5.3 Subsequent Modification of Final Fields

在某些情况下,例如反序列化,系统需要在构造后更改对象的final 字段。 final 字段可以通过反射和其他依赖于实现的方式进行更改。唯一具有合理语义的模式是构造一个对象,然后更新对象的final 字段。在对象的final 字段的所有更新完成之前,不应使该对象对其他线程可见,也不应读取final 字段。 final 字段的冻结发生在设置了 final 字段的构造函数的末尾,以及在每次通过反射或其他特殊机制修改 final 字段之后立即发生。

即便如此,也存在许多并发症。如果在字段声明中将final 字段初始化为编译时常量,则可能不会观察到final 字段的更改,因为该final 字段的使用在编译时被编译时常量替换.

另一个问题是规范允许对final 字段进行积极优化。在一个线程中,允许对 final 字段的读取进行重新排序,并对未在构造函数中发生的 final 字段进行修改。

另见

  • JLS 15.28 Constant Expression
    • 这种技术不太可能与原始 private static final boolean 一起使用,因为它可以作为编译时常量内联,因此可能无法观察到“新”值

附录:按位操作

基本上,

field.getModifiers() & ~Modifier.FINAL

field.getModifiers() 关闭对应于Modifier.FINAL 的位。 & 是按位与,~ 是按位补码。

另见


记住常量表达式

仍然无法解决这个问题?像我一样陷入抑郁症吗?你的代码是这样的吗?

public class A {
    private final String myVar = "Some Value";
}

阅读有关此答案的 cmets,特别是 @Pshemo 的答案,它提醒我 Constant Expressions 的处理方式不同,因此不可能对其进行修改。因此,您需要将代码更改为如下所示:

public class A {
    private final String myVar;

    private A() {
        myVar = "Some Value";
    }
}

如果你不是班级的主人......我感觉到你了!

要详细了解为什么会出现这种行为read this

【讨论】:

  • @thecoop, @HalfBrian:毫无疑问,这是极度邪恶,但这个例子是有意选择的。我的回答仅表明在某些情况下这是可能的。我能想到的最恶心的例子是故意选择的,希望人们可能会立即厌恶而不是爱上这种技术。
  • 哟,老兄。听说你喜欢倒影,所以我在场上倒影,这样你就可以一边倒影一边倒影。
  • 请注意,Boolean.FALSE 不是私有的。这真的适用于“私有最终静态”成员吗?
  • @mgaert 确实如此,但你必须使用getDeclaredField() 而不是getField() 用于目标类
  • +1。对于那些尝试更改 final String myConstant = "x"; 之类的东西并且会失败的人:请记住,编译时常量将由编译器内联,因此当您编写像 System.out.println(myConstant); 这样的代码时,它将被编译为 System.out.println("x");,因为编译器知道常量的值在编译时。要摆脱这个问题,您需要在运行时初始化常量,例如final String myConstant = new String("x");。同样对于像final int myField = 11 这样的原语,使用final int myField = new Integer(11);final Integer myField = 11;
【解决方案2】:

如果分配给static final boolean 字段的值在编译时是已知的,则它是一个常量。 原始字段或 String 类型可以是编译时常量。常量将内联在任何引用该字段的代码中。由于在运行时实际上并未读取该字段,因此更改它将无效。

Java language specification 是这样说的:

如果一个字段是一个常量变量 (§4.12.4),然后删除关键字 最终或改变其价值不会 打破与预先存在的兼容性 通过导致它们不运行的二进制文件, 但他们不会看到任何新价值 对于该领域的使用,除非他们 被重新编译。这是真的,即使 用法本身不是编译时 常量表达式(§15.28)

这是一个例子:

class Flag {
  static final boolean FLAG = true;
}

class Checker {
  public static void main(String... argv) {
    System.out.println(Flag.FLAG);
  }
}

如果您反编译 Checker,您会看到代码没有引用 Flag.FLAG,而是将值 1 (true) 压入堆栈(指令 #3)。

0:   getstatic       #2; //Field java/lang/System.out:Ljava/io/PrintStream;
3:   iconst_1
4:   invokevirtual   #3; //Method java/io/PrintStream.println:(Z)V
7:   return

【讨论】:

  • 这是我的第一个想法,但后来我记得 Java 在运行时编译,如果你要重置该位,它会简单地将其重新编译为变量而不是常量。
  • @Bill K - 不,这不是指 JIT 编译。依赖类文件实际上将包含内联值,并且没有对独立类的引用。这是一个非常简单的测试实验;我将添加一个示例。
  • 这与@polygenelubricants 重新定义 Boolean.false 的答案有何不同?--但你是对的,当事情没有正确重新编译时,我已经看到了这种行为。
  • @Bill K - 在 polygenlubricants 的回答中,该字段不是编译时间常数。这是public static final Boolean FALSE = new Boolean(false) 不是public static final boolean FALSE = false
【解决方案3】:

对 Java 语言规范第 17 章第 17.5.4 节“写保护字段”的一点好奇:

通常,不能修改最终和静态的字段。 但是,System.in、System.out 和 System.err 是静态最终字段 由于遗留原因,必须允许通过方法进行更改 System.setIn、System.setOut 和 System.setErr。我们参考这些 字段被写保护以区别于普通字段 最终字段。

来源:http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.5.4

【讨论】:

    【解决方案4】:

    我还把它和joor library集成了

    随便用

          Reflect.on(yourObject).set("finalFieldName", finalFieldValue);
    

    我还修复了 override 的问题,以前的解决方案似乎遗漏了该问题。 但是,请谨慎使用,仅在没有其他好的解决方案时使用。

    【讨论】:

    • 当我尝试这个(JDK12)时,我得到一个异常:“无法设置最终___字段”。
    • @AaronIba 在 Java 12+ 中不再允许。
    【解决方案5】:

    除了排名靠前的答案,您还可以使用最简单的方法。 Apache commons FieldUtils 类已经有特定的方法可以做这些事情。请看一下FieldUtils.removeFinalModifier 方法。您应该指定目标字段实例和可访问性强制标志(如果您使用非公共字段)。更多信息您可以找到here

    【讨论】:

    • 这是一个比目前接受的答案简单得多的解决方案
    • 是吗?复制一个方法听起来比导入整个库更简单(这与您要复制的方法做同样的事情)。
    • 不适用于 Java 12+:java.lang.UnsupportedOperationException: In java 12+ final cannot be removed.
    【解决方案6】:

    如果有安全管理员,可以使用AccessController.doPrivileged

    从上面接受的答案中取相同的例子:

    import java.lang.reflect.*;
    
    public class EverythingIsTrue {
        static void setFinalStatic(Field field, Object newValue) throws Exception {
            field.setAccessible(true);
            Field modifiersField = Field.class.getDeclaredField("modifiers");
    
            // wrapping setAccessible 
            AccessController.doPrivileged(new PrivilegedAction() {
                @Override
                public Object run() {
                    modifiersField.setAccessible(true);
                    return null;
                }
            });
    
            modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
            field.set(null, newValue);
        }
    
        public static void main(String args[]) throws Exception {      
          setFinalStatic(Boolean.class.getField("FALSE"), true);
          System.out.format("Everything is %s", false); // "Everything is true"
        }
    }
    

    在 lambda 表达式中,AccessController.doPrivileged 可以简化为:

    AccessController.doPrivileged((PrivilegedAction) () -> {
        modifiersField.setAccessible(true);
        return null;
    });
    

    【讨论】:

    【解决方案7】:

    即使是final,也可以在静态初始化程序之外修改字段,并且(至少 JVM HotSpot)将完美地执行字节码。

    问题在于 Java 编译器不允许这样做,但使用 objectweb.asm 可以轻松绕过。这是 p̶e̶r̶f̶e̶c̶t̶l̶y̶̶v̶a̶l̶i̶d̶̶c̶l̶a̶s̶s̶f̶i̶l̶e̶ 从 JVMS 规范的角度来看是一个无效的类文件,但它通过了字节码验证,然后在 JVM HotSpot OpenJDK12 下成功加载和初始化:

    ClassWriter cw = new ClassWriter(0);
    cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Cl", null, "java/lang/Object", null);
    {
        FieldVisitor fv = cw.visitField(Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL, "fld", "I", null, null);
        fv.visitEnd();
    }
    {
        // public void setFinalField1() { //... }
        MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "setFinalField1", "()V", null, null);
        mv.visitMaxs(2, 1);
        mv.visitInsn(Opcodes.ICONST_5);
        mv.visitFieldInsn(Opcodes.PUTSTATIC, "Cl", "fld", "I");
        mv.visitInsn(Opcodes.RETURN);
        mv.visitEnd();
    }
    {
        // public void setFinalField2() { //... }
        MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "setFinalField2", "()V", null, null);
        mv.visitMaxs(2, 1);
        mv.visitInsn(Opcodes.ICONST_2);
        mv.visitFieldInsn(Opcodes.PUTSTATIC, "Cl", "fld", "I");
        mv.visitInsn(Opcodes.RETURN);
        mv.visitEnd();
    }
    cw.visitEnd();
    

    在Java中,类大致如下:

    public class Cl{
        private static final int fld;
    
        public static void setFinalField1(){
            fld = 5;
        }
    
        public static void setFinalField2(){
            fld = 2;
        }
    }
    

    不能用javac编译,但可以被JVM加载执行。

    JVM HotSpot 对此类类进行了特殊处理,以防止此类“常量”参与常量折叠。这项检查是在bytecode rewriting phase of class initialization

    // Check if any final field of the class given as parameter is modified
    // outside of initializer methods of the class. Fields that are modified
    // are marked with a flag. For marked fields, the compilers do not perform
    // constant folding (as the field can be changed after initialization).
    //
    // The check is performed after verification and only if verification has
    // succeeded. Therefore, the class is guaranteed to be well-formed.
    InstanceKlass* klass = method->method_holder();
    u2 bc_index = Bytes::get_Java_u2(bcp + prefix_length + 1);
    constantPoolHandle cp(method->constants());
    Symbol* ref_class_name = cp->klass_name_at(cp->klass_ref_index_at(bc_index));
    if (klass->name() == ref_class_name) {
       Symbol* field_name = cp->name_ref_at(bc_index);
       Symbol* field_sig = cp->signature_ref_at(bc_index);
    
       fieldDescriptor fd;
       if (klass->find_field(field_name, field_sig, &fd) != NULL) {
          if (fd.access_flags().is_final()) {
             if (fd.access_flags().is_static()) {
                if (!method->is_static_initializer()) {
                   fd.set_has_initialized_final_update(true);
                }
              } else {
                if (!method->is_object_initializer()) {
                  fd.set_has_initialized_final_update(true);
                }
              }
            }
          }
        }
    }
    

    JVM HotSpot 检查的唯一限制是 final 字段不应在声明 final 字段的类之外修改。

    【讨论】:

    • 这只是纯粹的EVIL 和美丽。
    • 我不同意“完全有效的类文件”。 JVMS §6.5明确表示:“否则,如果resolved字段是final的,则必须在当前类或接口中声明,指令必须出现在当前类或接口的初始化方法中类或接口。否则,会抛出 IllegalAccessError”。所以这只是另一种实施公然违反规范并且代码分布在多个地方来处理应该被拒绝的情况
    • @Holger 感谢您的来信。我根据您的注释进行了更正,以免让更多读者感到困惑。
    【解决方案8】:

    刚刚在一个面试问题上看到了这个问题,如果可能的话,可以通过反射或在运行时更改最终变量。 真的很感兴趣,所以我变成了什么:

     /**
     * @author Dmitrijs Lobanovskis
     * @since 03/03/2016.
     */
    public class SomeClass {
    
        private final String str;
    
        SomeClass(){
            this.str = "This is the string that never changes!";
        }
    
        public String getStr() {
            return str;
        }
    
        @Override
        public String toString() {
            return "Class name: " + getClass() + " Value: " + getStr();
        }
    }
    

    一些带有最终字符串变量的简单类。所以在主课 导入 java.lang.reflect.Field;

    /**
     * @author Dmitrijs Lobanovskis
     * @since 03/03/2016.
     */
    public class Main {
    
    
        public static void main(String[] args) throws Exception{
    
            SomeClass someClass = new SomeClass();
            System.out.println(someClass);
    
            Field field = someClass.getClass().getDeclaredField("str");
            field.setAccessible(true);
    
            field.set(someClass, "There you are");
    
            System.out.println(someClass);
        }
    }
    

    输出如下:

    Class name: class SomeClass Value: This is the string that never changes!
    Class name: class SomeClass Value: There you are
    
    Process finished with exit code 0
    

    根据文档 https://docs.oracle.com/javase/tutorial/reflect/member/fieldValues.html

    【讨论】:

    • 你看过this的帖子吗?
    • 这个问题询问了一个static final 字段,所以这个代码不起作用。 setAccessible(true) 仅适用于设置最终实例字段。
    【解决方案9】:

    在部署到 JDK 1.8u91 之前,接受的答案对我有用。 然后,当我在调用setFinalStatic 方法之前通过反射读取值时,我意识到它在field.set(null, newValue); 行失败了。

    可能读取导致 Java 反射内部的不同设置(即在失败情况下是 sun.reflect.UnsafeQualifiedStaticObjectFieldAccessorImpl 而在成功情况下是 sun.reflect.UnsafeStaticObjectFieldAccessorImpl),但我没有进一步详细说明。

    由于我需要在旧值的基础上临时设置新值,然后再设置旧值,所以我稍微更改了签名以对外提供计算功能并返回旧值:

    public static <T> T assignFinalField(Object object, Class<?> clazz, String fieldName, UnaryOperator<T> newValueFunction) {
        Field f = null, ff = null;
        try {
            f = clazz.getDeclaredField(fieldName);
            final int oldM = f.getModifiers();
            final int newM = oldM & ~Modifier.FINAL;
            ff = Field.class.getDeclaredField("modifiers");
            ff.setAccessible(true);
            ff.setInt(f,newM);
            f.setAccessible(true);
    
            T result = (T)f.get(object);
            T newValue = newValueFunction.apply(result);
    
            f.set(object,newValue);
            ff.setInt(f,oldM);
    
            return result;
        } ...
    

    但是对于一般情况,这还不够。

    【讨论】:

      【解决方案10】:

      这里的许多答案都很有用,但我发现它们都不适用于Android,尤其是。我什至是 joorReflect 的一个相当大的用户,而且它和 apacheFieldUtils 都不是 - 在一些答案中都提到过,做诀窍。

      Android 问题

      之所以如此,根本原因是因为在 Android 上,Field 类中没有 modifiers 字段,这使得任何涉及此代码的建议(如标记的答案)都无用:

      Field modifiersField = Field.class.getDeclaredField("modifiers");
      modifiersField.setAccessible(true);
      modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
      

      其实引用FieldUtils.removeFinalModifier()

      // Do all JREs implement Field with a private ivar called "modifiers"?
      final Field modifiersField = Field.class.getDeclaredField("modifiers");
      

      所以,答案是否定的……

      解决方案

      很简单 - 字段名称不是modifiers,而是accessFlags。这就是诀窍:

      Field accessFlagsField = Field.class.getDeclaredField("accessFlags");
      accessFlagsField.setAccessible(true);
      accessFlagsField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
      

      旁注#1:无论字段在类中是否为静态,这都可以工作。

      旁注 #2:鉴于字段本身可能是私有的,建议还启用对字段本身的访问,使用 field.setAccessible(true)(除了 accessFlagsField.setAccessible(true)

      【讨论】:

      • 在Android中运行原代码会报如下错误:java.lang.NoSuchFieldException: No field modifiers in class Ljava/lang/reflect/Field; (declaration of 'java.lang.reflect.Field' appears in /apex/com.android.runtime/javalib/core-oj.jar)。建议的解决方案适用于我的情况。 (这个错误目前只有一个谷歌结果,所以希望人们现在能找到这个页面)
      【解决方案11】:

      自 Java 12 起,给出的答案将不起作用。

      这是一个关于如何从 Java 12 (基于 this answer)修改 private static final 字段的示例。

        private Object modifyField(Object newFieldValue, String fieldName, Object classInstance) throws NoSuchFieldException, IllegalAccessException {
          Field field = classInstance.getClass().getDeclaredField(fieldName);
          VarHandle MODIFIERS;
      
          field.setAccessible(true);
      
          var lookup = MethodHandles.privateLookupIn(Field.class, MethodHandles.lookup());
          MODIFIERS = lookup.findVarHandle(Field.class, "modifiers", int.class);
          int mods = field.getModifiers();
      
          if (Modifier.isFinal(mods)) {
            MODIFIERS.set(field, mods & ~Modifier.FINAL);
          }
      
          Object previousValue = field.get(classInstance);
          field.set(null, newFieldValue);
      
          return previousValue;
        }
      

      更多详情请见this thread

      【讨论】:

      • 这不适用于 Java 16+。
      • @JohannesKuhn 工作,如果你添加 --illegal-access=permit
      • --illegal-access=permit 在 Java 17 中被删除。
      • 由于JEP 416,Java 18 无法使用此功能
      【解决方案12】:

      对于 JDK 18,这将不再可能,因为在 invokedynamicMethodHandles 上重新实现了核心反射作为 JEP-416 (PR) 的一部分。

      following comment 中引用Mandy Chung——他是这部令人难以置信的作品的主要作者——。重点是我的。

      如果基础字段是最终字段,则Field 对象具有写入权限当且仅当

      • setAccessible(true) 已成功处理此 Field 对象;
      • 该字段是非静态的;和
      • 字段的声明类不是隐藏类;和
      • 字段的声明类不是记录类。

      【讨论】:

        【解决方案13】:

        如果您的字段只是私有的,您可以这样做:

        MyClass myClass= new MyClass();
        Field aField= myClass.getClass().getDeclaredField("someField");
        aField.setAccessible(true);
        aField.set(myClass, "newValueForAString");
        

        并抛出/处理 NoSuchFieldException

        【讨论】:

          【解决方案14】:

          final 字段的全部意义在于它一旦设置就不能重新分配。 JVM 使用这个保证来保持不同地方的一致性(例如,内部类引用外部变量)。所以不行。这样做会破坏 JVM!

          解决办法是一开始就不要声明final

          【讨论】:

          • 此外,final 在多线程执行中具有特殊作用 - 更改 final 值也会破坏 Java 内存模型。
          • 并且未声明final的字段不应声明static
          • @Tom:一般来说这可能是正确的,但我不会禁止 所有 静态可变变量。
          • @Tom:你有没有读过为什么单身人士是邪恶的?我做到了!现在我知道他们只在 Java 中作恶。并且仅仅是因为用户定义的类加载器的可用性。自从我知道这一切并且我不使用用户定义的类加载器后,我毫不后悔地使用单例。 Scala 也是如此,单例是一流的语言特性 — 单例是邪恶的,这是众所周知的错误神话
          • @Martin 我知道你的评论已经过时了,也许你的观点现在已经改变了,但我想我只是补充一下:单例是邪恶的,原因与 Java 无关。它们将隐藏的复杂性添加到您的代码中。此外,如果不知道还必须先配置 n 个单例,它们可能无法进行单元测试。它们是依赖注入的对立面。您的团队可能会做出这样的决定:隐藏复杂性的缺陷不会超过单例的便利性,但许多团队有充分的理由采取相反的立场。
          猜你喜欢
          • 2012-06-26
          • 2012-03-12
          • 2014-10-13
          • 2011-05-29
          • 2015-04-12
          • 2012-09-29
          • 1970-01-01
          相关资源
          最近更新 更多