在java8中,可以使用例如访问类java.lang.reflect.Fields的字段
Field.class.getDeclaredFields();
在 java12(从 java9 开始?)中,这仅返回一个空数组。即使使用
,这也不会改变--add-opens java.base/java.lang.reflect=ALL-UNNAMED
设置。
有什么想法可以实现这一目标吗? (除了这可能是一个坏主意这一事实之外,我希望能够在 junit 测试期间通过反射更改代码中的“静态最终”字段。通过更改“修饰符”,这在 java8 中是可能的
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(myfield, myfield.getModifiers() & ~Modifier.FINAL);
)
这在 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 包中的许多类都有私有字段,如果直接访问这些私有字段,将会损害运行时或使虚拟机崩溃。理想情况下,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
的字段,因此您无法像您当前正在尝试的那样滥用它们。你应该找到另一种方法来完成你需要的事情;尤金的答案似乎提供了至少一种选择。
删除
final
修饰符的正确方法是检测正在运行的程序,并让您的代理重新定义类。如果在首次加载类时执行此操作,则与在 JVM 启动之前修改类文件没有什么不同。换句话说,就像 final
修饰符从未存在过。
强制警告:Java 开发人员显然不希望您能够在不实际更改类文件的情况下将 Final 字段更改为非 Final 字段(例如,通过重新编译源代码、检测等) 。使用任何黑客行为需您自担风险;它可能会产生意想不到的副作用,只能在某些时候起作用,和/或在未来的版本中停止工作。
java.lang.invoke
以下使用
java.lang.invoke
包。无论出于何种原因,适用于 Reflection API 的相同限制不适用于 Invoke API(至少直至并包括 Java 17;继续阅读以获取更多信息)。
该示例修改了
EMPTY_ELEMENTDATA
类的 ArrayList
Final 字段。该字段通常包含一个空数组,当使用 ArrayList
容量进行初始化时,该数组在所有 0
实例之间共享。下面将该字段设置为 {"Hello", "World!"}
,正如您通过运行程序所看到的,这会导致列表实例包含从未添加到其中的元素。
我在 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");
// make field non-final
MODIFIERS.set(emptyElementDataField, emptyElementDataField.getModifiers() & ~Modifier.FINAL);
// set field to new value
emptyElementDataField.setAccessible(true);
emptyElementDataField.set(null, new Object[] {"Hello", "World!"});
var list = new ArrayList<>(0);
// println uses toString(), and ArrayList.toString() indirectly relies on 'size'
var sizeField = ArrayList.class.getDeclaredField("size");
sizeField.setAccessible(true);
sizeField.set(list, 2); // the new "empty element data" has a length of 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 优化(例如,静态最终字段已被内联,因为 JVM 不期望这样的字段能够更改)。
输出:
[Hello, World!]
不幸的是,上述结果在 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);
我找到了一种方法,它适用于 JDK 8、11、17。
Method getDeclaredFields0 = Class.class.getDeclaredMethod("getDeclaredFields0", boolean.class);
getDeclaredFields0.setAccessible(true);
Field[] fields = (Field[]) getDeclaredFields0.invoke(Field.class, false);
Field modifiers = null;
for (Field each : fields) {
if ("modifiers".equals(each.getName())) {
modifiers = each;
break;
}
}
assertNotNull(modifiers);
使用 JDK 11 或更高版本时,不要忘记设置以下参数:
--add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED
你不能。这是有意进行的改变。
例如,您可以使用
PowerMock
,它是 @PrepareForTest
- 如果您想将其用于测试目的,它在幕后使用 javassist (字节码操作)。这正是评论中的错误建议要做的。
换句话说,自从
java-12
- 就无法通过 vanilla java 访问它。
这适用于 JDK 17。
import java.lang.reflect.Field;
import sun.misc.Unsafe;
/**
* @author Arnah
* @since Feb 21, 2021
**/
public class FieldUtil{
private static Unsafe unsafe;
static{
try{
final Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
unsafe = (Unsafe) unsafeField.get(null);
}catch(Exception ex){
ex.printStackTrace();
}
}
public static void setFinalStatic(Field field, Object value) throws Exception{
Object fieldBase = unsafe.staticFieldBase(field);
long fieldOffset = unsafe.staticFieldOffset(field);
unsafe.putObject(fieldBase, fieldOffset, value);
}
}
public class YourClass{
public static final int MAX_ITEM_ROWS = 35_000;
}
FieldUtil.setFinalStatic(YourClass.class.getDeclaredField("MAX_ITEM_ROWS"), 1);
在 Java 21 上无需
java.misc.Unsafe
也可以使用以下代码和 JVM 参数:
JVM 参数:
--add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-exports=java.base/java.lang.invoke=ALL-UNNAMED --add-exports=java.base/jdk.internal.access=ALL-UNNAMED --add-exports=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-exports=jdk.unsupported/sun.misc=ALL-UNNAMED
Java 代码
public class HiddenFieldsRevealed {
private static final Object greeting = "Hello world";
public static void main(String... args) throws Throwable {
Field myStaticFinalField = HiddenFieldsRevealed.class.getDeclaredField("greeting");
myStaticFinalField.setAccessible(true);
removeFinalness(myStaticFinalField);
//Logic taken from java.lang.invoke.MethodHandle.unreflectSetter(Field)
//.unreflectSetter(Field)
//=> .unreflectField(Field, false)
//=> lookup.getDirectFieldNoSecurityManager(memberName.getReferenceKind(), f.getDeclaringClass(), memberName);
//=> .getDirectFieldCommon(refKind, referenceClass, memberName, false)
Class<?> memberNameClass = Class.forName("java.lang.invoke.MemberName");
Constructor<?> memberNameConstructor = memberNameClass.getDeclaredConstructor(Field.class, boolean.class);
memberNameConstructor.setAccessible(true);
Object memberNameInstanceForField = memberNameConstructor.newInstance(myStaticFinalField, true);
Field memberNameFlagsField = memberNameClass.getDeclaredField("flags");
memberNameFlagsField.setAccessible(true);
//Manipulate flags to remove hints to it being final
memberNameFlagsField.setInt(memberNameInstanceForField, (int)memberNameFlagsField.getInt(memberNameInstanceForField) & ~Modifier.FINAL);
Method getReferenceKindMethod = memberNameClass.getDeclaredMethod("getReferenceKind");
getReferenceKindMethod.setAccessible(true);
byte getReferenceKind = (byte)getReferenceKindMethod.invoke(memberNameInstanceForField);
MethodHandles.Lookup mh = MethodHandles.privateLookupIn(HiddenFieldsRevealed.class, MethodHandles.lookup());
Method getDirectFieldCommonMethod = mh.getClass().getDeclaredMethod("getDirectFieldCommon", byte.class, Class.class, memberNameClass, boolean.class);
getDirectFieldCommonMethod.setAccessible(true);
//Invoke last method to obtain the method handle
MethodHandle o = (MethodHandle) getDirectFieldCommonMethod.invoke(mh, getReferenceKind, myStaticFinalField.getDeclaringClass(), memberNameInstanceForField, false);
o.invoke("ModifiedValue");
System.out.println(greeting);
}
public static void removeFinalness(Field field) throws Throwable {
Method[] classMethods = Class.class.getDeclaredMethods();
Method declaredFieldMethod = Arrays.stream(classMethods).filter(x -> Objects.equals(x.getName(), "getDeclaredFields0")).findAny().orElseThrow();
declaredFieldMethod.setAccessible(true);
Field[] declaredFieldsOfField = (Field[]) declaredFieldMethod.invoke(Field.class, false);
Field modifiersField = Arrays.stream(declaredFieldsOfField).filter(x -> Objects.equals(x.getName(), "modifiers")).findAny().orElseThrow();
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
}
```
As of Java 21 this works because of the direct call to `getDirectFieldCommon(...)` that we can fool by modifying the flags on `MemberName` instance for a field. But this is prone to break in another version.