反射修改 static final 變數

三棗發表於2020-07-08

一、測試結論

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 已經修改成功了,但是在列印的時候卻任然是原來的值;

反射1

就是因為這個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 反射到它真實的值;

相關文章