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


478

我有一堂课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);

4
真是个坏主意。我会尝试获取源代码并重新编译(甚至反编译/重新编译)。
Bill K

System.out是公共静态最终字段,但也可以更改。
无可争议的

19
@irreputable System.out/in/err非常“特殊”,因此Java内存模型必须特别提及它们。它们不是应遵循的示例。
Tom Hawtin-大头钉

8
好吧,我的观点是在两者之间找到一个黑客,让我的应用程序正常工作,直到负责的lib在下一个版本中进行更改,所以我不再需要黑客了……
fixitagain 2010年

1
十年前的@Bill K:重新编译它很棒,但是它在已部署的系统上,我只需要对其进行修补,直到我们可以更新已部署的应用程序为止!
比尔K

Answers:


887

假设没有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"

实际执行的操作如下:

  • 基本booleantruefalsein自动main装箱以引用类型Boolean“常量”,Boolean.TRUE并且Boolean.FALSE
  • 反射是用来更改所public static final Boolean.FALSE引用BooleanBoolean.TRUE
  • 结果,随后在将a自动false装箱到时Boolean.FALSE,它所指的含义与所指的相同BooleanBoolean.TRUE
  • "false"现在的一切都是"true"

相关问题


注意事项

每当您执行此类操作时,都应格外小心。它可能不起作用,因为SecurityManager可能存在a,但是即使它不存在,根据使用方式的不同,它可能也可能不起作用。

JLS 17.5.3最终字段的后续修改

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

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

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

也可以看看

  • JLS 15.28常数表达式
    • 这项技术不太可能与原始private static final boolean函数一起使用,因为它作为编译时常量是可内联的,因此“ new”值可能无法观察到

附录:关于按位操作

实质上,

field.getModifiers() & ~Modifier.FINAL

关闭与Modifier.FINALfrom 相对应的位field.getModifiers()&是按位与,并且~是按位补码。

也可以看看


记住常数表达式

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

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

阅读有关此答案的注释,特别是@Pshemo的注释,它使我想起了常量表达式的处理方式不同,因此将无法对其进行修改。因此,您需要将代码更改为如下所示:

public class A {
    private final String myVar;

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

如果您不是课程的所有者,我会感觉到您!

有关为什么此行为的更多详细信息,请阅读此内容


41
@ thecoop,@ HalfBrian:毫无疑问,这是极度邪恶,但这个示例是设计使然。我的回答仅表明在某些情况下这是可能的。我能想到的最令人作呕的例子是有意选择的,希望人们会立即被厌恶而不是爱上该技术。
polygenelubricants 2010年

59
da 我听说您喜欢反射,所以我在田野上进行反射,以便您在反射的同时可以进行反射。
马修·弗莱申

11
请注意,Boolean.FALSE 不是私有的。这真的适用于“私有最终静态”成员吗?
mgaert

15
@mgaert可以,但是您必须使用getDeclaredField()而不是将其getField()用于目标类别
2013年

10
+1。对于那些尝试更改类似内容final String myConstant = "x";并会失败的人:请记住,编译器将内联编译时常量,因此在编写代码时将像System.out.println(myConstant);编译时那样进行编译,System.out.println("x");因为编译器在编译时就知道常量的值。要解决此问题,您需要在运行时像一样初始化常量final String myConstant = new String("x");。同样在诸如final int myField = 11use final int myField = new Integer(11);final Integer myField = 11;
Pshemo

58

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

Java语言规范这样说:

如果字段是常量变量(第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

4
@Bill K-不,这不涉及JIT编译。依赖类文件实际上将包含内联值,并且不引用独立类。这是一个非常简单的测试实验;我将添加一个示例。
艾里克森(Erickson)2010年

1
@polygenelubricants的答案在重新定义Boolean.false的情况下如何与之混为一谈?-但是您是正确的,当事情没有正确地重新编译时,我已经看到了这种行为。
Bill K

26
@Bill K-在polygenlubricants的答案中,该字段不是编译时间常数。这是public static final Boolean FALSE = new Boolean(false)不是public static final boolean FALSE = false
埃里克森

17

Java语言规范第17章,第17.5.4节“写保护字段”:

通常,不得修改final字段和static字段。但是,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


9

我也将其与joor库集成

只需使用

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

我还override解决了以前的解决方案似乎遗漏的问题。但是,只有在没有其他好的解决方案时,才应谨慎使用此功能。


当我尝试使用此方法(JDK12)时,出现异常:“无法设置最终的___字段”。
艾伦·伊巴

@AaronIba Java 12+中不再允许。
NateS

7

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


这是比当前接受的答案更简单的解决方案
伯尼(Bernie)

4
是吗?复制一个方法听起来比导入整个库更简单(这与您要复制的方法具有相同的作用)。
eski

1
在Java 12以上版本中不起作用:java.lang.UnsupportedOperationException: In java 12+ final cannot be removed.
MrPowerGamerBR

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

1
这似乎也不适用于Java 12+。
dan1st

是的@ dan1st,您是对的!请检查以下解决方案:stackoverflow.com/a/56043252/2546381
VanagaS

2

在将其部署到JDK 1.8u91上之前,可接受的答案一直对我有用。然后我意识到,field.set(null, newValue);在调用setFinalStatic方法之前通过反射读取值时,它无法在线执行。

可能是读取导致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;
    } ...

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


2

即使final是字段,也可以在静态初始值设定项之外进行修改,并且(至少JVM HotSpot)可以完美执行字节码。

问题是Java编译器不允许这样做,但是可以使用轻松地绕过它objectweb.asm。这是通过字节码验证并在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对此类有特殊的对待,因为它可以防止此类“常量”参与常量折叠。该检查是在类初始化字节码重写阶段完成的

// 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声明该字段的类之外修改该字段。


0

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

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

一些带有最终String变量的简单类。因此,在主类中导入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


你看过这篇文章吗?
拉文德拉(Ravindra)HV

该问题询问static最后一个字段,因此此代码不起作用。setAccessible(true)仅适用于设置最终实例字段。
Radiodef '18年

0

如果您的字段只是私有的,则可以执行以下操作:

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

并抛出/处理NoSuchFieldException


-4

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

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


4
此外,final它在多线程执行中具有特殊作用-更改final值也会破坏Java内存模型。
彼得Török

1
并且未声明的字段也不final应声明static
Tom Hawtin-大头钉

2
@Tom:总的来说,这可能是对的,但我不会禁止所有静态可变变量。
bcat

7
@汤姆:你有没有读过为什么单身人士是邪恶的?我做到了!现在我知道它们仅在Java中是邪恶的。并且仅由于用户定义的类加载器的可用性。而且自从我了解了所有这些并且我不使用用户定义的类加载器后,我便毫无疑问地使用了单例。Scala也是其中的佼佼者,其中单例是一流的语言功能-单例是邪恶的是众所周知的错误神话
马丁

3
@Martin我知道您的评论很陈旧,也许您的观点现在已经改变了,但是我想我要补充一点:单例是邪恶的,原因与Java无关。它们为您的代码增加了隐藏的复杂性。此外,如果不知道还必须先配置n个单例,它们就可能无法进行单元测试。它们是依赖注入的对立面。您的团队可能会做出以下决定:隐藏复杂性的陷阱不会超过Singletons的便利性,但是许多团队出于充分的理由采取相反的立场。
2016年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.