教程 · 2021年3月23日 0

Java骚操作之反射修改静态常量

内容目录

Java 有着强大的反射机制,但是也是因为反射的存在,导致 Java 写的代码并不是绝对安全的,就比如一个变量声明为 final,却不能保证它在运行中绝对不会被修改。

比如下面的代码:

public class Test {
    private static final Date time = new Date();
}

正常情况下运行中是不能修改这个 time 变量的,但是用反射呢?

import java.lang.reflect.Field;
import java.util.Date;

public class Test {
    private static final Date time = new Date();

    public static void main(String[] args) throws Exception {
        System.out.println(time);
        Field field = Test.class.getDeclaredField("time");
        field.setAccessible(true);
        field.set(null, new Date(0));
        System.out.println(time);
    }
}

很不幸,报错了,提示我们不能修改 final 变量。

查看抛出异常的代码,会看到这个:

// sun.reflect.UnsafeQualifiedStaticObjectFieldAccessorImpl#set (版本:JDK 1.8 8u261)
public void set(Object var1, Object var2) throws IllegalArgumentException, IllegalAccessException {
    if (this.isReadOnly) {
        this.throwFinalFieldIllegalAccessException(var2);
    }

    // 省略无关代码
}

这时候你一定会灵机一动,反射不是可以修改修饰符吗,再用反射把 readOnly 属性去了不就行了。没错,让我们实验一下。

修改上一步的代码。

System.out.println(time);
Field field = Test.class.getDeclaredField("time");
field.setAccessible(true);
int nonFinal = field.getModifiers() & (~Modifier.FINAL); // 位操作去掉 final
Field modifiers = Field.class.getDeclaredField("modifiers"); // 用反射拿到变量的修饰符
modifiers.setAccessible(true);
modifiers.setInt(field, nonFinal);
field.set(null, new Date(0));
System.out.println(time);

输出:

Tue Mar 23 20:30:45 CST 2021
Thu Jan 01 08:00:00 CST 1970

唉?事就这样成了?别着急,这个东西之能在 JDK 12 以下使用,如果你运行上面的代码,很有可能会抛出下面的错误:

Tue Mar 23 20:30:45 CST 2021
Exception in thread "main" java.lang.NoSuchFieldException: modifiers
    at java.base/java.lang.Class.getDeclaredField(Class.java:2489)
    at Test.main(Test.java:15)

打开 JDK 14 的代码,你会看到在执行 java.lang.Class#getDeclaredField 的时候 JDK 进行了过滤,事实上所有 java.lang.reflect 内部的代码都不允许反射修改内存了,这是出于安全方面的考虑。

但是如果你真的需要修改,JDK 9 引入了一个新的概念叫做 VarHandle,利用它可以“安全地”修改 JVM 内存(大概是线程安全,我也不太懂)。于是就有人想到利用这玩意修改变量的访问修饰符来达到修改 final 变量的目的。

下面给出一个工具类:

import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
/**
 * 更改静态常量
 *
 * @see <a href="https://stackoverflow.com/a/56043252">https://stackoverflow.com/a/56043252</a>
 */
public final class FieldHelper {
    private static final VarHandle MODIFIERS;
    static {
        // 用于消除非法访问警告,仅用于未命名模块
        Module javaBase = Field.class.getModule();
        Module my = FieldHelper.class.getModule();
        javaBase.addOpens("java.lang.reflect", my);
        javaBase.addOpens("java.util", my);
        try {
            var lookup = MethodHandles.privateLookupIn(Field.class, MethodHandles.lookup());
            MODIFIERS = lookup.findVarHandle(Field.class, "modifiers", int.class);
        } catch (IllegalAccessException | NoSuchFieldException ex) {
            throw new RuntimeException(ex);
        }
    }
    private FieldHelper() {
        throw new IllegalStateException();
    }
    public static void makeFinal(Field field) {
        int mods = field.getModifiers();
        if (!Modifier.isFinal(mods)) {
            MODIFIERS.set(field, mods | Modifier.FINAL);
        }
    }
    public static void makeNonFinal(Field field) {
        int mods = field.getModifiers();
        if (Modifier.isFinal(mods)) {
            MODIFIERS.set(field, mods & ~Modifier.FINAL);
        }
    }
}

修改之前的代码。

System.out.println(time);
Field field = Test.class.getDeclaredField("time");
field.setAccessible(true);
FieldHelper.makeNonFinal(field);
field.set(null, new Date(0));
System.out.println(time);

注意,如果你的代码属于命名模块,则需要在运行时加入启动参数 --add-opens java.base/java.lang.reflect=模块名

但是如果你用了 Java 9 的模块限制反射访问,那这个方法也许不管用,我只在未命名模块测试成功过。


总结一下就是,Java 12 以下用反射可以随便修改 final 变量,但是高版本 Java 会稍微麻烦一点,而且以后可能会被限制。