获取jdk12中java.lang.reflect.Fields的声明字段

问题描述 投票:0回答:5

在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 unit-testing reflection java-12
5个回答
57
投票

为什么它不再起作用

这在 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 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");
    // 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+

不幸的是,上述结果在 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);

16
投票

我找到了一种方法,它适用于 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

12
投票

你不能。这是有意进行的改变。

例如,您可以使用

PowerMock
,它是
@PrepareForTest
- 如果您想将其用于测试目的,它在幕后使用 javassist (字节码操作)。这正是评论中的错误建议要做的。

换句话说,自从

java-12
- 就无法通过 vanilla java 访问它。


9
投票

适用于 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);

0
投票

在 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.
© www.soinside.com 2019 - 2024. All rights reserved.