一、测试结论

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 已经修改成功了,但是在打印的时候却任然是原来的值;

反射修改 static final 变量-LMLPHP

就是因为这个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 反射到它真实的值;

07-08 23:18