ChatGPT解决这个技术问题 Extra ChatGPT

使用 Java 反射更改私有静态最终字段

我有一个带有 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 内存模型不得不特别提及它们。它们不是应该遵循的例子。
好吧,我的观点是在两者之间找到一个黑客,让我的应用程序正常工作,直到负责的库在下一个版本中进行更改,所以我不再需要黑客了......
十年前的@Bill K:重新编译它会很棒,但它在已部署的系统上,我只需要修补它,直到我们可以更新已部署的应用程序!

C
Community

假设没有 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 中的原始布尔值 true 和 false 自动装箱以引用类型布尔“常量” Boolean.TRUE 和 Boolean.FALSE

反射用于将 public static final Boolean.FALSE 更改为引用 Boolean.TRUE 所引用的布尔值

因此,随后每当一个 false 自动装箱为 Boolean.FALSE,它引用的布尔值与 Boolean.TRUE 所引用的布尔值相同

一切“假”现在都是“真”

相关问题

使用反射更改静态最终 File.separatorChar 以进行单元测试

如何将 setAccessible 限制为仅“合法”使用?有弄乱整数缓存、改变字符串等的例子

有弄乱整数缓存、改变字符串等的例子

注意事项

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

JLS 17.5.3 最终字段的后续修改在某些情况下,例如反序列化,系统将需要在构造后更改对象的最终字段。 final 字段可以通过反射和其他依赖于实现的方式进行更改。唯一具有合理语义的模式是构造对象,然后更新对象的最终字段。在对象的 final 字段的所有更新完成之前,不应使该对象对其他线程可见,也不应读取 final 字段。最终字段的冻结发生在设置了最终字段的构造函数的末尾,以及在每次通过反射或其他特殊机制修改最终字段之后立即发生。即便如此,仍有许多并发症。如果在字段声明中将 final 字段初始化为编译时常量,则可能不会观察到对 final 字段的更改,因为该 final 字段的使用在编译时被编译时常量替换。另一个问题是规范允许对最终字段进行积极优化。在一个线程中,允许使用未在构造函数中发生的对最终字段的修改来重新排序对最终字段的读取。

也可以看看

JLS 15.28 常量表达式 此技术不太可能与原始私有静态最终布尔值一起使用,因为它可以作为编译时常量内联,因此“新”值可能无法观察到

这种技术不太可能与原始私有静态最终布尔值一起使用,因为它可以作为编译时常量内联,因此“新”值可能无法观察到

附录:关于按位操作

本质上,

field.getModifiers() & ~Modifier.FINAL

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

也可以看看

维基百科/按位运算

记住常量表达式

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

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

阅读对此答案的评论,特别是 @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;
e
erickson

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

Java language specification 表示:

如果一个字段是一个常量变量(第 4.12.4 节),那么删除关键字 final 或更改其值不会因为导致它们不运行而破坏与预先存在的二进制文件的兼容性,但它们不会看到任何新的用法值除非它们被重新编译。即使用法本身不是编译时常量表达式也是如此(第 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
S
Stephan Markwalder

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

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

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


A
Aleksandr Dubinsky

我还将它与 joor library

只需使用

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

我还修复了以前的解决方案似乎遗漏的 override 问题。但是要非常小心地使用它,只有在没有其他好的解决方案时才能使用。


当我尝试这个(JDK12)时,出现异常:“无法设置最终___字段”。
@AaronIba Java 12+ 不再允许。
n
nndru

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


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

即使是 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 感谢您的注意。我根据您的注释进行了更正,以免使更多读者感到困惑。
V
VanagaS

如果存在安全管理员,则可以使用 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;
});

是的@dan1st,你是对的!请检查此解决方案:stackoverflow.com/a/56043252/2546381
B
Brice

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

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

如果基础字段是最终字段,则当且仅当此字段对象的 setAccessible(true) 成功时,字段对象才具有写入权限;该字段是非静态的;并且该字段的声明类不是隐藏类;并且该字段的声明类不是记录类。


h
hasskell

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

 /**
 * @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();
    }
}

一些带有最终字符串变量的简单类。所以在主类中 import 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) 仅适用于设置最终实例字段。
T
Tomáš Záluský

接受的答案对我有用,直到部署在 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;
    } ...

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


d
d4vidi

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

安卓的问题

之所以如此,根本原因是在 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)。建议的解决方案适用于我的情况。 (这个错误目前只有一个谷歌结果,所以希望人们现在能找到这个页面)
p
pringi

从 Java 12 开始,给出的答案将不起作用。

下面是一个示例,说明如何修改自 Java 12 以来的 private static final 字段(基于 this answer)。

  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+。
如果您添加 --illegal-access=permit,@JohannesKuhn 就可以工作
--illegal-access=permit 在 Java 17 中被移除。
由于 JEP 416,无法使用 Java 18 进行此操作
您不能更改 static final 字段。 That never really worked。它可能看起来有效,但最终 - 依赖这种效果会使您的代码变得不可移植。
P
Philip Rego

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

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

并抛出/处理 NoSuchFieldException


t
thecoop

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

解决方案不是首先声明它final


此外,final 在多线程执行中具有特殊作用 - 更改 final 值也会破坏 Java 内存模型。
未声明为 final 的字段不应声明为 static
@Tom:一般来说这可能是真的,但我不会禁止所有静态可变变量。
@Tom:你有没有读过为什么单身人士是邪恶的?我做到了!现在我知道他们只在 Java 中作恶。并且仅仅是因为用户定义的类加载器的可用性。自从我知道这一切并且我不使用用户定义的类加载器后,我毫不后悔地使用了单例。 Scala 也是如此,单例是一流的语言特性——单例是邪恶的,这是一个众所周知的错误神话。
@Martin我知道你的评论很旧,也许你的观点现在已经改变了,但我想我只是补充一下:单身人士是邪恶的,原因与Java无关。它们为您的代码增加了隐藏的复杂性。此外,在不知道必须先配置 n 个单例的情况下,它们可能无法进行单元测试。它们是依赖注入的对立面。您的团队可能会认为隐藏复杂性的陷阱不会超过单例的便利性,但许多团队有充分的理由采取相反的立场。