1. 程式人生 > 實用技巧 >反射修改 static final 變數

反射修改 static final 變數

一、測試結論

static final 修飾的基本型別和String型別不能通過反射修改;

二、測試案例

@Test
public void test01() throws Exception {
setFinalStatic(Constant.class.getDeclaredField("i1"), 11);
System.out.println(Constant.i1); setFinalStatic(Constant.class.getDeclaredField("i2"), 22);
System.out.println(Constant.i2); setFinalStatic(Constant.class.getDeclaredField("s1"), "change1");
System.out.println(Constant.s1); setFinalStatic(Constant.class.getDeclaredField("s2"), "change2");
System.out.println(Constant.s2); System.out.println("----------------"); setFinalStatic(CC.class.getDeclaredField("i1"), 11);
System.out.println(CC.i1); setFinalStatic(CC.class.getDeclaredField("i2"), 22);
System.out.println(CC.i2); setFinalStatic(CC.class.getDeclaredField("i3"), 33);
System.out.println(CC.i3); setFinalStatic(CC.class.getDeclaredField("s1"), "change1");
System.out.println(CC.s1); setFinalStatic(CC.class.getDeclaredField("s2"), "change2");
System.out.println(CC.s2); setFinalStatic(CC.class.getDeclaredField("s3"), "change3");
System.out.println(CC.s3); } private 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);
} interface Constant {
int i1 = 1;
Integer i2 = 1;
String s1 = "s1";
String s2 = new String("s2");
} static class CC {
private static final int i1 = 1;
private static final Integer i2 = 1;
private static Integer i3 = 1;
private static final String s1 = "s1";
private static final String s2 = new String("s2");
private static String s3 = "s3";
}
// 列印結果
1
22
s1
change2
----------------
1
22
33
s1
change2
change3

從列印的日誌可以看到,正如開篇所說,除了 static final 修飾的基本型別和String型別修改失敗,其他的都修改成功了;

但是這裡有一個很有意思的現象,在debug的時候顯示 i1 已經修改成功了,但是在列印的時候卻任然是原來的值;

就是因為這個debug然我疑惑了很久,但是仔細分析後感覺這是一個bug,詳細原因還暫時未知;

三、案例分析

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

首先這裡修改 static final 值得原理是,將這個 Field 的 FieldAccessor 的 final 給去掉了,否則在 field.set(null, newValue); 的時候, 就會檢查 final 而導致失敗

// UnsafeIntegerFieldAccessorImpl
if (this.isFinal) {
this.throwFinalFieldIllegalAccessException(var2);
}

而我們在 CC.class.getDeclaredField("i1") 獲取的 Field 其實是 clazz 物件中的一個備份,

// Class
private static Field searchFields(Field[] fields, String name) {
String internedName = name.intern();
for (int i = 0; i < fields.length; i++) {
if (fields[i].getName() == internedName) {
return getReflectionFactory().copyField(fields[i]);
}
}
return null;
} Field copy() {
if (this.root != null)
throw new IllegalArgumentException("Can not copy a non-root Field"); Field res = new Field(clazz, name, type, modifiers, slot, signature, annotations);
res.root = this;
// Might as well eagerly propagate this if already present
res.fieldAccessor = fieldAccessor;
res.overrideFieldAccessor = overrideFieldAccessor; return res;
}

所以在 field.set(null, newValue); 設定新值得時候,這裡就應該是類似值傳遞和引用傳遞的問題,複製出來的 field 其實已經修改成功了,但是 root 物件仍然是原來的值,而在列印的時候,其實是直接取的 root 物件的值;

private void setFinalStatic(Field field, Object newValue) throws Exception {
field.setAccessible(true);
// Object o1 = field.get(null);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
field.set(null, newValue);
Object o1 = field.get(null);
} // 列印 11

注意如果這裡在去掉 final 之前就取了一次值,就會 set 失敗, 因為 Class 預設開啟了 useCaches 快取, get 的時候會獲取到 root field 的 FieldAccessor, 後面的重設就會失效;

四、位元組碼分析

這個問題還可以從位元組碼的角度分析:

public class CC {
public static final int i1 = 1;
public static final Integer i2 = 1;
public static int i3 = 1;
public final int i4 = 1;
public int i5 = 1;
}

// javap -verbose class

警告: 二進位制檔案CC包含com.sanzao.CC
Classfile /Users/wangzichao/workspace/test/target/classes/com/sanzao/CC.class
Last modified 2020-7-8; size 572 bytes
MD5 checksum 5f5847cb849315f98177420057130de6
Compiled from "CC.java"
public class com.sanzao.CC
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #8.#28 // java/lang/Object."<init>":()V
#2 = Fieldref #7.#29 // com/sanzao/CC.i4:I
#3 = Fieldref #7.#30 // com/sanzao/CC.i5:I
#4 = Methodref #31.#32 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#5 = Fieldref #7.#33 // com/sanzao/CC.i2:Ljava/lang/Integer;
#6 = Fieldref #7.#34 // com/sanzao/CC.i3:I
#7 = Class #35 // com/sanzao/CC
#8 = Class #36 // java/lang/Object
#9 = Utf8 i1
#10 = Utf8 I
#11 = Utf8 ConstantValue
#12 = Integer 1
#13 = Utf8 i2
#14 = Utf8 Ljava/lang/Integer;
#15 = Utf8 i3
#16 = Utf8 i4
#17 = Utf8 i5
#18 = Utf8 <init>
#19 = Utf8 ()V
#20 = Utf8 Code
#21 = Utf8 LineNumberTable
#22 = Utf8 LocalVariableTable
#23 = Utf8 this
#24 = Utf8 Lcom/sanzao/CC;
#25 = Utf8 <clinit>
#26 = Utf8 SourceFile
#27 = Utf8 CC.java
#28 = NameAndType #18:#19 // "<init>":()V
#29 = NameAndType #16:#10 // i4:I
#30 = NameAndType #17:#10 // i5:I
#31 = Class #37 // java/lang/Integer
#32 = NameAndType #38:#39 // valueOf:(I)Ljava/lang/Integer;
#33 = NameAndType #13:#14 // i2:Ljava/lang/Integer;
#34 = NameAndType #15:#10 // i3:I
#35 = Utf8 com/sanzao/CC
#36 = Utf8 java/lang/Object
#37 = Utf8 java/lang/Integer
#38 = Utf8 valueOf
#39 = Utf8 (I)Ljava/lang/Integer;
{
public static final int i1;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 1 public static final java.lang.Integer i2;
descriptor: Ljava/lang/Integer;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL public static int i3;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC public final int i4;
descriptor: I
flags: ACC_PUBLIC, ACC_FINAL
ConstantValue: int 1 public int i5;
descriptor: I
flags: ACC_PUBLIC public com.sanzao.CC();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field i4:I
9: aload_0
10: iconst_1
11: putfield #3 // Field i5:I
14: return
LineNumberTable:
line 3: 0
line 7: 4
line 8: 9
LocalVariableTable:
Start Length Slot Name Signature
0 15 0 this Lcom/sanzao/CC; static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_1
1: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
4: putstatic #5 // Field i2:Ljava/lang/Integer;
7: iconst_1
8: putstatic #6 // Field i3:I
11: return
LineNumberTable:
line 5: 0
line 6: 7
}
SourceFile: "CC.java"
   #9 = Utf8               i1
#10 = Utf8 I
#11 = Utf8 ConstantValue
#12 = Integer 1

從這裡就能看到 i1 其實是在編譯的時候就已經初始化了(程式碼內聯)優化, 而 i4, i5 是在建構函式的時候初始化, i2, i3 是在執行 static 階段初始化, 同時 i2, i3, i4, i5 都會指向一個 Fieldref 物件, 所以在執行階段就能通過 Fieldref 反射到它真實的值;