1. 程式人生 > >反射修改 static final 變數

反射修改 static final 變數

## 一、測試結論 **static final 修飾的基本型別和String型別不能通過反射修改;** ## 二、測試案例 ```java @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"; } ``` ```log // 列印結果 1 22 s1 change2 ---------------- 1 22 33 s1 change2 change3 ``` 從列印的日誌可以看到,正如開篇所說,除了 static final 修飾的基本型別和String型別修改失敗,其他的都修改成功了; 但是這裡有一個很有意思的現象,在debug的時候顯示 `i1` 已經修改成功了,但是在列印的時候卻任然是原來的值; ![反射1](https://img2020.cnblogs.com/blog/1119937/202007/1119937-20200708154127444-1920474063.png) 就是因為這個debug然我疑惑了很久,但是仔細分析後感覺這是一個bug,詳細原因還暫時未知; ## 三、案例分析 ```java 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 而導致失敗 ```java // UnsafeIntegerFieldAccessorImpl if (this.isFinal) { this.throwFinalFieldIllegalAccessException(var2); } ``` 而我們在 `CC.class.getDeclaredField("i1")` 獲取的 Field 其實是 clazz 物件中的一個備份, ```java // 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 物件的值; ```java 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, 後面的重設就會失效; ## 四、位元組碼分析 這個問題還可以從位元組碼的角度分析: ```java 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 ```log 警告: 二進位制檔案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."":()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 #19 = Utf8 ()V #20 = Utf8 Code #21 = Utf8 LineNumberTable #22 = Utf8 LocalVariableTable #23 = Utf8 this #24 = Utf8 Lcom/sanzao/CC; #25 = Utf8 #26 = Utf8 SourceFile #27 = Utf8 CC.java #28 = NameAndType #18:#19 // "":()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."":()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" ``` ```log #9 = Utf8 i1 #10 = Utf8 I #11 = Utf8 ConstantValue #12 = Integer 1 ``` 從這裡就能看到 i1 其實是在編譯的時候就已經初始化了(程式碼內聯)優化, 而 i4, i5 是在建構函式的時候初始化, i2, i3 是在執行 static 階段初始化, 同時 i2, i3, i4, i5 都會指向一個 Fieldref 物件, 所以在執行階段就能通過 Fieldref 反射到它真實