为什么它不再起作用
Java 12中不再起作用的原因是由于JDK-8210522。这个CSR说:
摘要
核心反射具有过滤机制,可以隐藏类getXXXField(s)和getXXXMethod(s)中的安全和完整性敏感字段和方法。 过滤机制已经在几个版本中用于隐藏安全敏感字段,例如System.security和Class.classLoader。
此CSR建议扩展筛选器以从java.lang.reflect和java.lang.invoke中隐藏来自一些高度安全敏感的类的字段。
问题
java.lang.reflect和java.lang.invoke包中的许多类具有私有字段,如果直接访问将危及运行时或崩溃VM。 理想情况下,应该通过核心反射过滤java.base中所有非公共/非受保护字段,并且不能通过Unsafe API读取/写入,但我们目前还没有到达这个地步。 在此期间,过滤机制被用作绷带。
解决方案
将过滤器扩展到以下类别中的所有字段:
java.lang.ClassLoader
java.lang.reflect.AccessibleObject
java.lang.reflect.Constructor
java.lang.reflect.Field
java.lang.reflect.Method
并且过滤了java.lang.invoke.MethodHandles.Lookup中用于查找类和访问模式的私有字段。
规范
没有规范更改,这是过滤非公共/非受保护字段,不应依赖java.base之外的任何内容。 这些类都不可序列化。
基本上,它们过滤了java.lang.reflect.Field的字段,以便您无法滥用它们-就像您当前正在尝试的那样。 您应该找到另一种完成您需要的方式; Eugene的答案似乎提供了至少一种选项。
适当修复
删除final
修饰符的正确方法是instrument运行的程序,并使您的代理重新定义类。 如果在加载类时执行此操作,则与在JVM启动之前修改类文件没有区别。 换句话说,就像从未出现过final
修饰符一样。
解决方法
义务警告: Java的开发人员显然不希望您能够将最终字段更改为非最终字段而不实际更改类文件(例如,通过重新编译源代码,插桩等)。使用任何 hack 都要自担风险;它可能会产生意想不到的副作用,在某些情况下仅起作用,并且/或在未来的版本中停止工作。
使用java.lang.invoke
以下使用java.lang.invoke
包。由于某种原因,Reflection API所施加的相同限制不适用于Invoke API(至少到Java 17为止;请继续阅读获取更多信息)。
该示例修改ArrayList类的EMPTY_ELEMENTDATA final字段。当使用容量为0初始化时,此字段通常包含在所有ArrayList实例之间共享的空数组。下面将该字段设置为{"Hello", "World!"}
,并且通过运行程序可以看到,这导致列表实例包含从未添加到其中的元素。
Java 12-17
我在Java 16.0.2和Java 17.0.3上进行了测试,两者都从https://adoptium.net/下载。
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
public class Main {
private static final VarHandle MODIFIERS;
static {
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);
}
}
public static void main(String[] args) throws Exception {
var emptyElementDataField = ArrayList.class.getDeclaredField("EMPTY_ELEMENTDATA");
MODIFIERS.set(emptyElementDataField, emptyElementDataField.getModifiers() & ~Modifier.FINAL);
emptyElementDataField.setAccessible(true);
emptyElementDataField.set(null, new Object[] {"Hello", "World!"});
var list = new ArrayList<>(0);
var sizeField = ArrayList.class.getDeclaredField("size");
sizeField.setAccessible(true);
sizeField.set(list, 2);
System.out.println(list);
}
}
使用以下命令运行代码:
javac Main.java
java --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED Main
注意: 我尝试使用“单一源文件”功能,但导致了ConcurrentModificationException
异常。正如评论中指出的那样,这可能是由于某些JIT优化(例如,静态final字段已被内联,因为JVM不希望此类字段能够更改)。
[Hello, World!]
Java 18+
不幸的是,在 Java 18.0.1 上(从 https://adoptium.net/ 下载),上述操作会导致以下异常:
Exception in thread "main" java.lang.UnsupportedOperationException
at java.base/java.lang.invoke.VarForm.getMemberName(VarForm.java:114)
at Main.main(Main.java:23)
第23行是:
MODIFIERS.set(emptyElementDataField, emptyElementDataField.getModifiers() & ~Modifier.FINAL);
static
字段? - Linosun.misc.Unsafe
修改最终字段,但正如其名称所示,这可能会破坏一切。 - Benjamin Urquhart