如何在不使用JVM参数的情况下隐藏Java 9中的警告“非法反射访问”?

98

我刚试图使用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

我希望在启动时不添加--illegal-access=deny到JVM选项中,就能隐藏此警告。就像这样:

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

有没有什么方法可以做到这一点?

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

澄清一下 - 我的问题是关于通过代码关闭此警告,而不是通过类似问题中所述的JVM参数/标志。


我知道这种方法,而且问题已经报告了。然而,我现在想要关闭这个警告。因为上述问题的修复需要一些时间。 - Dmitriy Dumanskiy
1
再说一遍 - 我知道我可以使用JVM标志,但是我需要通过代码来实现。我这里讲得不清楚吗? - Dmitriy Dumanskiy
3
HotSpotDiagnosticMXBean 允许更改一些 JVM 选项。不确定是否可以用它来更改这个选项,即使可以,在生产环境中这样做也有些冒险。 - Mick Mnemonic
1
@nullpointer 不是的。我的目标是避免为最终用户增加额外的指令。我们有很多安装了我们服务器的用户,这对他们来说将是一个很大的不便。 - Dmitriy Dumanskiy
9个回答

75

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

1. 简单的方法

由于警告消息被输出到默认的错误流中,您可以简单地关闭此流并将 stderr 重定向到 stdout

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

注意:

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

2. 不更改stderr的复杂方法

好消息是,在JDK 9中可以仍然使用sun.misc.Unsafe而不会收到警告。解决方案是利用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
    }
}

2
@nullpointer 它没有被导出,但是这个技巧不需要它被导出。 - apangin
17
这是一个可能在未来更新中失效的 hack。对于此类问题,正确的做法是向有问题的库提交 bug 报告(您可能已经这样做了)。您可以通过精确打开被篡改的类所在的包来暂时解决此问题。有几种方法可以传递 --add-opens 选项(CLI、可执行 JAR 的 JAR 清单、环境变量)。 - Alan Bateman
9
什么鬼?!无害的反射,例如对于文档化的official API中的“protected”成员进行访问,会得到一个警告,但是访问sun.misc.Unsafe却没有?好的,我明白了,停止使用反射(除了获取theUnsafe之外),并始终使用Unsafe。然后,我们甚至不需要去关闭警告... - Holger
3
@Holger 是的,这种情况的讽刺之处在于,“封装内部API”的所有这些麻烦可能会导致更广泛地使用Unsafe。 - apangin
5
我发现另一种方法,你可以以编程方式执行与 --add-opens 等效的操作,例如 Target.class.getModule().addOpens(Target.class.getPackage(), MyClass.class.getModule()); 你需要为每个要访问的包重复此操作。 (说明:这是关于Java中模块系统的一个技术问题的翻译) - Holger
显示剩余9条评论

16

还有一种选项,不需要流抑制并且不依赖于未记录或不受支持的API。使用Java代理,可以重新定义模块以导出/打开所需的包。这段代码看起来可能是这样的:

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,并在命令行上(而不是类路径)使用-javaagent:myjar.jar指定它。该代理将只包含以下premain方法:
public class MyAgent {
  public static void main(String arg, Instrumentation inst) {
    exportAndOpen(inst);
  }
}

或者,您可以使用附加API动态附加,该API由我编写的byte-buddy-agent项目方便地提供:

exportAndOpen(ByteBuddyAgent.install());

需要在非法访问之前调用该方法。请注意,此方法仅适用于JDK和Linux VM,而如果您需要在其他VM上使用它,则需要在命令行中提供Byte Buddy代理作为Java代理。这在您想要在测试和开发机器上进行自附加时非常方便,因为通常安装了JDK。

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

然而,好处在于,该解决方案对未来的更新是稳健的,因为任何操作,甚至是动态附加都是合法的。使用一个帮助进程,Byte Buddy甚至可以绕过通常禁止的自附加。


这样的解决方案不仅难以实践,而且违反了Java 9新模块系统“强封装”的目标。我们应该与Oracle的JDK开发人员合作解决现实世界中的向后兼容性问题,或者与我们工具的用户合作,让他们接受JDK 9+更严格的要求,而不是推广这种肮脏的黑客行为。例如,我们可以要求Oracle添加一个(非常合理的)java.lang.instrument.InstrumentationFactory类,这样就不再需要动态附加了。 - Rogério
11
这个讨论在邮件列表上持续了好几个月,但最终被拒绝了。如果你需要将一个运行于Java 9的系统迁移过来,而预算有限,有时你需要采用一个中间解决方案。由于它没有使用非官方的API,因此也不算是一种hack。 - Rafael Winterhalter
@Rogério,你的想法很错误。问题总有一天要解决,但普通开发人员无能为力也不应该去做。非法访问通常发生在库中,这些库现在运行良好,而且可能已经处理了这个问题:它们会进行非法访问以提高效率,并在失败时回退到合法方式。只要存在非常小的非法访问成功几率,它们就不会在这方面得到更新,即使是几十年后也是如此。除非它们找到了一种合法的高效方式... - maaartinus
@Rogério,如果我非常希望“违背Java 9新模块系统的‘强封装’目标”怎么办?我并没有要求使用模块系统;而蓝领程序员Joe Java肯定也不会用到它。这给我带来了很多麻烦,却没有一点好处。 - barneypitt

14
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中对我有效。


1
@AlexByrth 找到一种方法将 jdk.internal.module.IllegalAccessLoggerlogger 字段设置为 null。查看 IllegalAccessLogger 的源代码。 - Gan

12

我不知道如何实现您要求的功能。正如您所指出的,您需要在JVM启动时添加一些命令行选项--add-opens,而不是--illegal-access=deny)。

您写道:

我的目标是避免给最终用户增加额外的说明。我们有很多安装了我们服务器的用户,这对他们来说将是一个很大的不便。

看起来,您的要求只能得出这样一个结论:该项目尚未准备好支持Java 9。它应该诚实地向其用户报告,需要一些时间才能完全兼容Java 9。在发行后的初期,这完全可以接受。


6
有另一种方法,不基于任何黑客技巧,也没有在以上任何答案中提到。但是它只适用于在类路径上运行的代码。因此,任何需要支持在Java 9+上运行的库都可以使用这种技术,只要它从类路径运行即可。
它是基于这样的事实:允许运行在类路径上的代码(即来自未命名模块的代码)自由动态打开任何模块的包(只能从目标模块本身或未命名模块中完成打开)。
例如,给定以下代码,访问java.io.Console类的私有字段:
Field field = Console.class.getDeclaredField("formatter");
field.setAccessible(true);

为了避免警告,我们需要将目标模块的包导入到我们的模块中:
if (!ThisClass.class.getModule().isNamed()) {
    Console.class.getModule().addOpens(Console.class.getPackageName(), ThisClass.class.getModule());
}

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

这很有帮助,我需要类似的东西来在反射访问java.net.InetAddress.cache时消除警告。 - neu242

5

我想到了一种方法,可以在不使用Unsafe或访问任何未文档化的API的情况下禁用该警告。它通过使用反射将FilterOutputStream::out字段设置为null来实现。

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

  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",此代码仍将正常工作,因为这些模式不会为相同的调用者显示警告超过一次。

另外,您也可以将System.err的原始状态恢复,或者将其out字段设置为自定义OutputStream,以便过滤未来的警告。


2

如果有人希望重定向日志消息而不是将它们丢弃,这个方法可以在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) {
    }
  }
}


1
你可以在 module-info.java打开 包,或创建一个 开放式模块
例如:查看 逐步将您的项目迁移到 Jigsaw 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;
}

该链接再次提到了命令行的使用,而原帖作者明确否认了这种方式。 - Naman

1
这是对我有用的方法。
-Djdk.module.illegalAccess=deny

2
那个JVM参数怎么样了? - crusy
3
这正是他试图避免的解决方案。 - Pedro

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