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

时间:2010-07-21 16:35:49

标签: java reflection static private final

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

12 个答案:

答案 0 :(得分:803)

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

这里实际完成的内容如下:

  • boolean中的原始truefalsemain被自动设置为引用类型Boolean“常量”Boolean.TRUE和{{1 }}
  • 反映用于更改public static final Boolean.FALSE以引用Boolean.FALSE
  • 引用的Boolean
  • 因此,随后每当Boolean.TRUE自动装箱到false时,它就会引用与Boolean.FALSE
  • 所引用的Boolean相同的Boolean.TRUE
  • 现在"false"的所有内容都是"true"

相关问题


注意事项

每当你做这样的事情时都要特别小心。它可能不起作用,因为SecurityManager可能存在,但即使它不存在,取决于使用模式,它可能也可能不起作用。

  

JLS 17.5.3 Subsequent Modification of Final Fields

     

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

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

     

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

另见

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

附录:关于按位操作

基本上,

field.getModifiers() & ~Modifier.FINAL

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

另见


记住常量表达式

仍然无法解决这个问题?已经像我一样陷入了萧条?你的代码看起来像这样吗?

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

答案 1 :(得分:50)

如果在编译时已知分配给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,您将看到代码只是将值1(Flag.FLAG)推送到堆栈(指令#3)而不是引用true

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

答案 2 :(得分:13)

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

答案 3 :(得分:6)

我还将其与joor library

集成在一起

只需使用

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

此外,我修复了以前的解决方案似乎错过的override问题。 但是,只有在没有其他好的解决方案时才非常谨慎地使用它。

答案 4 :(得分:5)

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

答案 5 :(得分:5)

除了排名靠前的答案,您可以使用最简单的方法。 Apache commons FieldUtils类已经有了可以完成这些工作的特殊方法。请看一下FieldUtils.removeFinalModifier方法。您应该指定目标字段实例和辅助功能强制标记(如果您使用非公共字段)。您可以找到更多信息here

答案 6 :(得分: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;
    } ...

然而,对于一般情况,这还不够。

答案 7 :(得分:0)

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

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

并抛出/处理NoSuchFieldException

答案 8 :(得分:0)

即使是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在防止此类“常量”参与恒定折叠的意义上对此类进行了特殊处理。此检查是在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字段。

答案 9 :(得分:0)

这里的许多答案都很有用,但是我发现其中没有一个特别适合Android使用。我什至是 joor Reflect的相当大的用户,而它和 apache FieldUtils都不是-两者在此处答案,解决问题。

Android的问题

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

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)之外,还可以对字段本身进行访问。

答案 10 :(得分:-1)

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

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

一些带有final String变量的简单类。所以在主类 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

答案 11 :(得分:-5)

final字段的重点是一旦设置就无法重新分配。 JVM使用此保证来维护各个位置的一致性(例如,引用外部变量的内部类)。所以不行。能够这样做会破坏JVM!

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