如何在没有 JVM 参数的情况下在 java 9 中隐藏警告 "Illegal reflective access"?

How to hide warning "Illegal reflective access" in java 9 without JVM argument?

我刚刚尝试 运行 我的服务器 Java 9 并收到下一个警告:

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by io.netty.util.internal.ReflectionUtil (file:/home/azureuser/server-0.28.0-SNAPSHOT.jar) to constructor java.nio.DirectByteBuffer(long,int)
WARNING: Please consider reporting this to the maintainers of io.netty.util.internal.ReflectionUtil
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

我想在启动期间隐藏此警告而不向 JVM 选项添加 --illegal-access=deny。类似于:

System.setProperty("illegal-access", "deny");

有什么办法吗?

所有建议使用 JVM 选项的相关答案,我想从代码中关闭它。这可能吗?

澄清一下 - 我的问题是关于从代码而不是通过 JVM arguments/flags 中提出的类似问题中所述的警告。

您可以 openmodule-info.java 或创建 open module

例如:检查 Migrating Your Project to Jigsaw Step by Step

的第 5 步和第 6 步
module shedlock.example {
    requires spring.context;
    requires spring.jdbc;
    requires slf4j.api;
    requires shedlock.core;
    requires shedlock.spring;
    requires HikariCP;
    requires shedlock.provider.jdbc.template;
    requires java.sql;
    opens net.javacrumbs.shedlockexample to spring.core, spring.beans, spring.context;
}

open module shedlock.example {
    requires spring.context;
    requires spring.jdbc;
    requires slf4j.api;
    requires shedlock.core;
    requires shedlock.spring;
    requires HikariCP;
    requires shedlock.provider.jdbc.template;
    requires java.sql;
}

我不知道有什么办法可以实现你的要求。正如您所指出的,您需要将 command line options--add-opens,而不是 --illegal-access=deny)添加到 JVM 启动中。

您写道:

My goal is to avoid the additional instructions for end users. We have many users with our servers installed and that would be a big inconvenience for them.

从它的外观来看,您的要求只留下项目尚未准备就绪的结论 Java 9. 它应该诚实地向用户报告它需要更多的时间才能完全 Java 9兼容。发布后这么早完全没问题。

有一些方法可以禁用非法访问警告,但我不建议这样做。

1。简单方法

由于警告打印到默认错误流,您只需关闭此流并将 stderr 重定向到 stdout

public static void disableWarning() {
    System.err.close();
    System.setErr(System.out);
}

备注:

  • 这种方法合并了错误和输出流。在某些情况下,这可能是不可取的。
  • 您不能仅通过调用 System.setErr 来重定向警告消息,因为对错误流的引用保存在 JVM 早期的 IllegalAccessLogger.warningStream 字段中 bootstrap。

2。不改变 stderr

的复杂方法

好消息是 sun.misc.Unsafe 仍然可以在 JDK 9 中访问而不会出现警告。解决方案是在 Unsafe API.

的帮助下重置内部 IllegalAccessLogger
public static void disableWarning() {
    try {
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        Unsafe u = (Unsafe) theUnsafe.get(null);

        Class cls = Class.forName("jdk.internal.module.IllegalAccessLogger");
        Field logger = cls.getDeclaredField("logger");
        u.putObjectVolatile(cls, u.staticFieldOffset(logger), null);
    } catch (Exception e) {
        // ignore
    }
}

还有另一个选项不需要任何流抑制,也不依赖于未记录或不受支持的 APIs。使用 Java 代理,可以将模块重新定义为 export/open 所需的包。代码看起来像这样:

void exportAndOpen(Instrumentation instrumentation) {
  Set<Module> unnamed = 
    Collections.singleton(ClassLoader.getSystemClassLoader().getUnnamedModule());
  ModuleLayer.boot().modules().forEach(module -> instrumentation.redefineModule(
        module,
        unnamed,
        module.getPackages().stream().collect(Collectors.toMap(
          Function.identity(),
          pkg -> unnamed
        )),
        module.getPackages().stream().collect(Collectors.toMap(
           Function.identity(),
           pkg -> unnamed
         )),
         Collections.emptySet(),
         Collections.emptyMap()
  ));
}

您现在可以 运行 任何非法访问而不会发出警告,因为您的应用程序包含在未命名的模块中,例如:

Method method = ClassLoader.class.getDeclaredMethod("defineClass", 
    byte[].class, int.class, int.class);
method.setAccessible(true);

为了获得 Instrumentation 实例,您可以编写一个非常简单的 Java agent 并使用 [=15 在命令行(而不是类路径)上指定它=].代理将只包含一个 premain 方法,如下所示:

public class MyAgent {
  public static void main(String arg, Instrumentation inst) {
    exportAndOpen(inst);
  }
}

或者,您可以使用附加 API 动态附加,the byte-buddy-agent project(我编写)可以方便地访问它:

exportAndOpen(ByteBuddyAgent.install());

您需要在非法访问之前调用它。请注意,这仅在 JDK 和 Linux VM 上可用,而如果您需要在其他 VM 上使用,则需要在命令行上将 Byte Buddy 代理作为 Java 代理提供。当您希望在通常安装 JDK 的测试和开发机器上进行自我连接时,这会很方便。

正如其他人指出的那样,这只能作为一种中间解决方案,但我完全理解当前的行为经常会破坏日志记录爬虫和控制台应用程序,这就是为什么我自己在生产环境中使用它作为短期解决方案使用 Java 9 这么久我没有遇到任何问题。

然而,好处是此解决方案对未来的更新很稳健,因为任何操作,甚至动态附件都是合法的。使用辅助进程,Byte Buddy 甚至可以绕过通常被禁止的自我依恋。

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class Main {
    @SuppressWarnings("unchecked")
    public static void disableAccessWarnings() {
        try {
            Class unsafeClass = Class.forName("sun.misc.Unsafe");
            Field field = unsafeClass.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            Object unsafe = field.get(null);

            Method putObjectVolatile = unsafeClass.getDeclaredMethod("putObjectVolatile", Object.class, long.class, Object.class);
            Method staticFieldOffset = unsafeClass.getDeclaredMethod("staticFieldOffset", Field.class);

            Class loggerClass = Class.forName("jdk.internal.module.IllegalAccessLogger");
            Field loggerField = loggerClass.getDeclaredField("logger");
            Long offset = (Long) staticFieldOffset.invoke(unsafe, loggerField);
            putObjectVolatile.invoke(unsafe, loggerClass, offset, null);
        } catch (Exception ignored) {
        }
    }

    public static void main(String[] args) {
        disableAccessWarnings();
    }
}

它适用于我 JAVA 11.

这是对我有用的方法

-Djdk.module.illegalAccess=deny

还有另一种方法,不是基于任何 hack,上面的任何答案都没有提到。 但是,它仅适用于 classpath 上的代码 运行ning。因此,任何需要在 Java 9+ 上支持 运行ning 的库都可以使用此技术,只要它是来自 class 路径的 运行。

它基于这样一个事实,即允许 classpath 上的代码 运行ning(即来自未命名模块)自由动态打开任何模块的包(可以做到仅来自目标模块本身,或未命名模块)。

例如,给定此代码,访问 java.io.Console class 的私有字段:

Field field = Console.class.getDeclaredField("formatter");
field.setAccessible(true);

为了不引起警告,我们必须打开目标模块的包到我们的模块:

if (!ThisClass.class.getModule().isNamed()) {
    Console.class.getModule().addOpens(Console.class.getPackageName(), ThisClass.class.getModule());
}

我们还添加了一项检查,以确保我们确实 运行正在 class 路径上。

我想出了一种方法来禁用该警告,而无需使用 Unsafe 或访问任何未记录的 API。它通过使用反射将 System.errFilterOutputStream::out 字段设置为空来工作。

当然,尝试使用反射实际上会抛出我们试图抑制的警告,但我们可以利用并发来解决这个问题:

  1. 锁定 System.err 以便其他线程无法写入。
  2. 生成 2 个在 out 字段上调用 ​​setAccessible 的线程。其中一个会在尝试显示警告时挂起,但另一个会完成。
  3. System.errout字段设置为null并释放对System.err的锁定。第二个线程现在将完成,但不会显示任何警告。
  4. 等待第二个线程结束,恢复System.errout字段。

以下代码对此进行了演示:

public void suppressWarning() throws Exception
{
    Field f = FilterOutputStream.class.getDeclaredField("out");
    Runnable r = () -> { f.setAccessible(true); synchronized(this) { this.notify(); }};
    Object errorOutput;
    synchronized (this)
    {
        synchronized (System.err) //lock System.err to delay the warning
        {
            new Thread(r).start(); //One of these 2 threads will 
            new Thread(r).start(); //hang, the other will succeed.
            this.wait(); //Wait 1st thread to end.
            errorOutput = f.get(System.err); //Field is now accessible, set
            f.set(System.err, null); // it to null to suppress the warning

        } //release System.err to allow 2nd thread to complete.
        this.wait(); //Wait 2nd thread to end.
        f.set(System.err, errorOutput); //Restore System.err
    }
}

即使 --illegal-access 设置为 "warn" 或 "debug",此代码也能正常工作,因为这些模式不会对同一调用者多次显示警告。

此外,您还可以将其 out 字段设置为自定义 OutputStream,而不是恢复 System.err 的原始状态,这样您就可以过滤以后的警告。

万一有人想重定向日志消息而不是丢弃它们,这对我有用 Java 11。它取代了非法访问记录器写入的流。

public class AccessWarnings {

  public static void redirectToStdOut() {
    try {

      // get Unsafe
      Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
      Field field = unsafeClass.getDeclaredField("theUnsafe");
      field.setAccessible(true);
      Object unsafe = field.get(null);

      // get Unsafe's methods
      Method getObjectVolatile = unsafeClass.getDeclaredMethod("getObjectVolatile", Object.class, long.class);
      Method putObject = unsafeClass.getDeclaredMethod("putObject", Object.class, long.class, Object.class);
      Method staticFieldOffset = unsafeClass.getDeclaredMethod("staticFieldOffset", Field.class);
      Method objectFieldOffset = unsafeClass.getDeclaredMethod("objectFieldOffset", Field.class);

      // get information about the global logger instance and warningStream fields 
      Class<?> loggerClass = Class.forName("jdk.internal.module.IllegalAccessLogger");
      Field loggerField = loggerClass.getDeclaredField("logger");
      Field warningStreamField = loggerClass.getDeclaredField("warningStream");

      Long loggerOffset = (Long) staticFieldOffset.invoke(unsafe, loggerField);
      Long warningStreamOffset = (Long) objectFieldOffset.invoke(unsafe, warningStreamField);

      // get the global logger instance
      Object theLogger = getObjectVolatile.invoke(unsafe, loggerClass, loggerOffset);
      // replace the warningStream with System.out
      putObject.invoke(unsafe, theLogger, warningStreamOffset, System.out);
    } catch (Throwable ignored) {
    }
  }
}