内容目录
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 会稍微麻烦一点,而且以后可能会被限制。
近期评论