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

40
在Java 8中,可以使用如下方式访问java.lang.reflect.Fields类的字段:
Field.class.getDeclaredFields();

在Java12中(从Java9开始?),这将仅返回一个空数组。即使使用...,这也不会改变。
--add-opens java.base/java.lang.reflect=ALL-UNNAMED

设置。

有什么想法可以实现这个吗? (除了这可能是一个坏主意之外,我想能够通过反射在我的代码中更改“静态final”字段,在JUnit测试期间。在Java8中通过更改“修饰符”已经可以实现这一点)

Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(myfield, myfield.getModifiers() & ~Modifier.FINAL);

)


2
我建议退一步,重新思考你的整个设置。你为什么要首先改变一个static字段? - Lino
1
@Lino,这是因为我需要用一个“可测试”的Logger实现来替换一个Logger。我相信还有其他方法可以解决这个问题,但我想了解发生了什么变化,为什么以及可能在哪里记录下来... - Henning
2
在我看来,你不应该测试一个记录器。但是真正的问题仍然存在。 - Lino
10
这似乎是更改:https://bugs.openjdk.java.net/browse/JDK-8210496 出现在JDK 12中。 - Jorn Vernee
3
您仍可以通过sun.misc.Unsafe修改最终字段,但正如其名称所示,这可能会破坏一切。 - Benjamin Urquhart
4个回答

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包中的许多类具有私有字段,如果直接访问将危及运行时或崩溃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");
    // 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优化(例如,静态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);

7
这种方法可能随时失效,最好不要依赖它。一般来说,没有人应该期望能够更改静态final字段。对于模拟和其他测试工具,长期建议使用代理并删除您想要模拟的字段的final修饰符。 - Alan Bateman
5
似乎有一个通用的模式,invoke包执行的检查不像反射那样严格,例如您可以轻松实例化新的枚举常量。顺便说一下,只要您的代码属于未命名模块,您甚至可以通过在FieldHelper的初始化程序开头插入“Module java_base = Field.class.getModule(),unnamed = FieldHelper.class.getModule(); java_base.addOpens("java.lang.reflect", unnamed); java_base.addOpens("java.util", unnamed);”来以编程方式消除警告。 - Holger
3
当我们考虑负载时间插装时,这与在启动JVM之前修改类文件没有区别,因此适用二进制兼容性约束。除非final字段也是编译时常量,否则可能通过插装工具实现。 - Holger
3
我的表述如下:“您无法通过反射来删除final修饰符;唯一的方法是实际更改类。无论是在编译后更改持久类文件还是通过Instrumentation动态更改,都需要实际更改类,而不是修补Reflection工件。我在JDK 18代码中进行了一些深入研究,有趣的一点是,即使以那种方式更改修饰符字段成功,它也不会影响修改字段值的能力。” - Holger
3
我猜,当你使用单源文件功能时,已经多次使用了 ArrayList(即时编译示例),因此 JIT 优化了静态最终字段访问,因此反射 hack 对该字段没有影响,而对 size 字段的更改在规范内(有点),因此仍会进行更改。大小为2但长度为0的数组(已内联的原始引用)表明存在并发修改。同样地,当你使用 System.out.println(Arrays.toString(list.toArray())); 时,你得到 [null, null] - Holger
显示剩余9条评论

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

1
不错,这确实可以在JDK17上运行! 这使我们能够将项目升级到JDK17,并解决一个错误 - 直到我们的修复正式集成到JDK中。 - Florian Kirmaier
3
停止支持JDK 18。此答案的代码仍然有效,你可以获取modifiers字段的Field实例来进行操作,但它不能让你写入不可修改的final字段。 - Holger

12
你无法这样做,这是有意进行的更改。
例如,你可以使用PowerMock以及它的@PrepareForTest - 在背后它使用javassist(字节码操作)如果你想用它来进行测试。这正是评论中建议做的事情。
换句话说,自从java-12版本以来,没有办法通过原生的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);

8
  1. public static final int MAX_ITEM_ROWS = 35_000; 是一个编译时常量。无论在哪个地方读取 MAX_ITEM_ROWS,常量值 35_000 都已经在编译时插入。
  2. 当你的示例尝试将字段的值设置为它已经具有的“35,000”时,你是不会注意到的。当考虑“无变化”作为成功时,它看起来似乎在任何地方都是成功的。
  3. 如果你能够读取你写入的值,你会注意到这段代码失败了。你正在将一个对象引用赋给一个整数变量。只有 Unsafe 使这成为可能...
- Holger
我在Java 17.0.5+9-LTS-191上遇到了这个问题 java.lang.IllegalArgumentException at java.base/jdk.internal.misc.Unsafe.staticFieldBase0(Native Method) at java.base/jdk.internal.misc.Unsafe.staticFieldBase(Unsafe.java:1128) at jdk.unsupported/sun.misc.Unsafe.staticFieldBase(Unsafe.java:707) at FieldUtil.setFinalStatic(Example.java:24) 即使使用了 --add-opens - TWiStErRob

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接