使用Java反射更改私有静态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);
326557 次浏览

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

解决方案是一开始就不要将其声明为final

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

这里的实际操作如下:

  • 原语booleantruefalsemain中被自动装箱,以引用类型Boolean "常量"Boolean.TRUEBoolean.FALSE
  • 反射用于将public static final Boolean.FALSE更改为由Boolean.TRUE引用的Boolean
  • 因此,当false被自动装箱到Boolean.FALSE时,它引用的是与Boolean.TRUE引用的相同的Boolean
  • 所有"false"的东西现在都是"true"

相关问题


警告

无论何时你做这样的事情都应该非常小心。它可能无法工作,因为可能存在SecurityManager,但即使没有,根据使用模式,它也可能工作,也可能不工作。

JLS 17.5.3 Final字段的后续修改

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

即便如此,仍有许多复杂的问题。如果在字段声明中将final字段初始化为编译时常量,则可能观察不到对final字段的更改,因为在编译时,final字段的使用已被编译时常量所取代。

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

另请参阅

  • JLS 15.28常量表达式
    • 这种技术不太可能适用于原语private static final boolean,因为它是可内联的编译时常数,因此"值可能无法观察到

附录:关于按位操作

从本质上讲,

field.getModifiers() & ~Modifier.FINAL

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

另请参阅


记住常数表达式

还是不能解决这个问题?会像我一样陷入抑郁吗?您的代码是这样的吗?

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

阅读这个答案的评论,特别是@Pshemo的评论,它提醒我常数表达式处理不同,所以将是不可能的来修改它。因此,你需要修改你的代码,看起来像这样:

public class A {
private final String myVar;


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

如果你不是类的所有者…我懂你!

有关为什么这种行为读到这?

如果分配给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语言规范,第17章,第17.5.4节“写保护字段”:

通常,final和static字段不能被修改。 然而,系统。在系统。out和System。Err是静态的最终字段 由于遗留的原因,必须允许方法对其进行更改 系统。开始,系统。setOut和System.setErr。我们提到这些 字段被写入保护,以区别于普通字段 最后的领域。< / p >

来源:# EYZ0

我还集成了joor图书馆

只使用

      Reflect.on(yourObject).set("finalFieldName", finalFieldValue);
我还修复了override的一个问题,以前的解决方案似乎错过了。 但是,只有在没有其他好的解决方案时,才要小心使用这个方法
刚刚在一个面试问题上看到了这个问题,如果可能的话,在反射或运行时改变最终变量。 我真的很感兴趣,所以我变成了:

 /**
* @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; < / p >
/**
* @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

根据文档 # EYZ0 < / p >

如果存在安全管理器,则可以使用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;
});
在部署到JDK 1.8u91之前,接受的答案对我来说是有效的。 然后我意识到它失败在field.set(null, newValue);行,当我在调用setFinalStatic方法之前通过反射读取值

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

因为我需要在旧值的基础上临时设置新值,然后再将旧值设置回来,所以我对signature做了一点改变,在外部提供计算功能的同时也返回旧值:

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

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

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

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

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

和抛出/处理NoSuchFieldException

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

问题是Java编译器不允许这样做,但是可以使用objectweb.asm轻松绕过。下面是p——e——r─f─e─c─t─l─y─v─a─l─i─d─s─f─i─l─e─从jvm规范的角度来看,这是一个无效的类文件,但它通过了字节码验证,然后在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字段。

这里的许多答案都是有用的,但我发现它们中没有一个特别适用于Android。我甚至是Reflectjoor的忠实用户,无论是它还是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)之外)。

从Java 12开始,给出的答案将不能工作。

下面是一个关于如何从Java 12(基于这个答案)开始修改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;
}

更多细节请参见这个线程

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

引用曼迪涌 -谁是这个令人难以置信的工作的主要作者-在下面的评论。重点是我的。

如果底层字段是final,则Field对象具有写访问权当且仅当

  • setAccessible(true)已成功用于Field对象;
  • 电场是非静态的;而且
  • 字段的声明类不是一个隐藏类;而且
  • 字段的声明类不是记录类。