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


76

我只是尝试使用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

再一次-我知道我可以使用JVM标志,但是,我需要通过代码来完成。我在这里不清楚吗?
Dmitriy Dumanskiy

3
HotSpotDiagnosticMXBean允许改变一些JVM选项。不知道是否可以将其用于此产品,如果可以,在生产中很难做到这一点。
米克助记键

1
@nullpointer否。我的目标是避免为最终用户提供其他说明。我们有许多安装服务器的用户,这对他们来说是一个很大的不便。
Dmitriy Dumanskiy

Answers:


58

有几种方法可以禁用非法访问警告,尽管我不建议您这样做。

1.简单的方法

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

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

笔记:

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

2.无需更改标准错误的复杂方法

一个好消息是,sun.misc.Unsafe仍可以在JDK 9中访问它而不会发出警告。解决方案是在IllegalAccessLoggerUnsafe API的帮助下重置内部。

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

13
这是一种黑客,很可能会在以后的更新中中断。处理此类问题的正确方法是向有问题的库提交错误(那时您可能已经这样做了)。您可以通过精确打开包含被黑客入侵的类的软件包来临时解决该问题。有几种方法可以传递该--add-opens选项(CLI,可执行JAR的JAR清单,环境变量)。
艾伦·贝特曼

7
WTF ?!无害反思,例如对protected正式API的文档成员,将获得警告,但访问sun.misc.Unsafe不成功?好的,收到消息后,请停止使用Reflection(获取之外theUnsafe),并始终使用Unsafe。然后,我们甚至不需要关闭警告…
Holger

2
@Holger是的,具有讽刺意味的是,围绕“内部API封装”的所有这些大惊小怪可能导致Unsafe的更广泛使用。
apangin

5
我发现了另一种方法,您可以以--add-opens编程方式执行等效操作,例如,Target.class.getModule().addOpens(Target.class.getPackage(), MyClass.class.getModule());您必须对要访问的每个程序包重复此操作。
Holger

14

还有另一个选项不需要抑制流,并且不依赖未记录或不支持的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代理,然后使用在命令行(而不是类路径)上指定它-javaagent:myjar.jar。该代理仅包含premain如下方法:

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

另外,您可以使用附加API动态附加,该附加API可以方便地访问 字节好友代理的项目(这是我创作的):

exportAndOpen(ByteBuddyAgent.install());

您需要先致电该地址,然后再进行非法访问。请注意,这仅在JDK和Linux VM上可用,而如果在其他VM上需要,则需要在命令行上将Byte Buddy代理作为Java代理提供。当您希望在通常安装了JDK的测试和开发计算机上进行自我附加时,这会很方便。

正如其他人指出的那样,这仅应作为中间解决方案,但我完全理解当前的行为通常会破坏日志爬网程序和控制台应用程序,这就是为什么我在生产环境中将其作为使用Java 9和Java的短期解决方案的原因。很久没遇到任何问题。

但是,好消息是,此解决方案对于任何操作(即使动态附件都是合法的)都可以在将来进行更新。通过使用助手过程,Byte Buddy甚至可以解决通常被禁止的自我连接。


-1这样的解决方案不仅要付诸实践,而且还要面对Java 9新模块系统的“强封装”目标。我们应该与Oracle的JDK开发人员一起解决现实世界中的向后兼容性问题,或者与我们的工具的用户一起使用,以便他们接受JDK 9+的更严格的要求,而不是散布这些肮脏的技巧。例如,我们可以要求Oracle添加一个(非常合理的)java.lang.instrument.InstrumentationFactory类,这样就不再需要动态附加。
罗杰里奥

10
讨论在邮件列表上进行了许多个月,遭到拒绝。如果您需要使系统运行在Java 9上并且预算有限,那么有时您需要一个中间解决方案。由于它不使用非官方的API,因此也没有资格视为黑客。
拉斐尔·温特

@Rogério你错了。必须有一天解决这些问题,但是普通开发人员无法或不应做任何事情。非法访问通常发生在图书馆中,图书馆现在运作良好,并且可能已经解决了这一问题:出于效率原因,他们会进行非法访问,并在失败时恢复合法状态。只要存在非法访问的机会(即可能是几十年),它们就不会在这方面得到更新。除非他们获得了合法有效的方式....
maaartinus

10

我无法实现您的要求。如您所指出的,您需要向JVM启动添加命令行选项--add-opens尽管不是--illegal-access=deny)。

你写了:

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

从外观上看,您的需求仅得出一个结论,即该项目尚未为Java 9做好准备。它应该诚实地向用户报告,与Java 9完全兼容需要花费更多时间。发布后的早期,这完全可以。


10
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找到一种将logger字段设置jdk.internal.module.IllegalAccessLogger为null的方法。看一下IllegalAccessLogger的源代码。
甘:

这也对我有效,在ArchLinux上使用OpenJDK 14,非常感谢!
Hartmut P.

4

还有另一种方法(不基于任何黑客手段)在上述任何答案中均未提及。但是,它仅适用于在类路径上运行的代码。因此,只要需要从classpath运行,任何需要支持在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());
}

我们还添加了一个检查,表明我们确实在classpath上运行。


2

这对我有用

-Djdk.module.illegalAccess=deny

没有JVM参数怎么回事?
crusy

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

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

例如:逐步将项目迁移到Jigsaw的Checkout步骤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;
}

该链接再次提到了OP明确拒绝使用的命令行。
Naman

1

我想出了一种在不使用Unsafe也不访问任何未公开的API的情况下禁用该警告的方法。它通过使用Reflection将FilterOutputStream::out字段设置System.err为null来工作。

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

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

以下代码对此进行了说明:

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设置为“警告”或“调试”,此代码也将起作用,因为对于同一个调用方,这些模式不会多次显示警告。

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


-1

仍然可以在JDK 9-15中访问sun.misc.Unsafe而不发出警告。我使用以下方法:

通过“ unsafe.defineAnonymousClass(hostClass,useMethodCode)”方法将用户代码注册为主机类的匿名内部类。然后,您可以获得hostClass权限。

支持的:

  • 否非法反光交战
  • 不添加jvm选项--add-opens =

范例1:

    // use jdk.internal.misc.Unsafe.defineClass(), but not --add-opens
    Class<T> aClass = Platform.doPrivileged(Unsafe.class, new PrivilegedAction<Class<T>>()
{
    @Override
    public Class<T> run()
            throws Exception
    {
        Object theInternalUnsafe = Class.forName("jdk.internal.misc.Unsafe")
                .getMethod("getUnsafe")
                .invoke(null);
        return (Class<T>) Class.forName("jdk.internal.misc.Unsafe").getMethod("defineClass",
                String.class, byte[].class, int.class, int.class, ClassLoader.class, ProtectionDomain.class)
                .invoke(theInternalUnsafe, null, classBytes, 0, classBytes.length, classLoader, defaultDomain);
    }
});

示例2:

1. Defining interfaces
public static interface Java11PrivateApi
{
    /**
     * not printWarring
     * Make this method get permission to `FilterOutputStream.class`
     */
    @Privilege(FilterOutputStream.class)
    public OutputStream getSystemOut()
            throws Exception;

    /**
     * not --add-opens=java.base/jdk.internal.ref=ALL-UNNAMED
     * Make this method get permission to `ByteBuffer.class`
     */
    @Privilege(ByteBuffer.class)
    public Object createCleaner(Object ob, Runnable thunk)
            throws Exception;
}
2. Implementation method
/* warring: Don't rely on any user classes here  */

public static class JvmPrivilegeFunctionDemo
        implements Java11PrivateApi
{
    @Override
    public OutputStream getSystemOut()
            throws Exception
    {
        Field field = FilterOutputStream.class.getDeclaredField("out");
        field.setAccessible(true);
        return (OutputStream) field.get(System.out);
    }

    @Override
    public Object createCleaner(Object ob, Runnable thunk)
            throws Exception
    {
        Method method = Class.forName("jdk.internal.ref.Cleaner").getDeclaredMethod("create", Object.class, Runnable.class);
        method.setAccessible(true);
        return method.invoke(null, ob, thunk);
    }
}
3. Use
Java11PrivateApi api = Platform.doPrivileged(Java11PrivateApi.class, new JvmPrivilegeFunctionDemo());
// show
OutputStream outputStream = api.getSystemOut();
Assert.assertNotNull(outputStream);

Object cleaner = api.createCleaner(null, () -> System.out.println("hello java 11"));
Assert.assertEquals("jdk.internal.ref.Cleaner", cleaner.getClass().getName());

完整的Wiki:https : //github.com/harbby/gadtry/wiki/Hide-warning-%60Illegal-reflective-access-and-not-open-module%60-in-java-9--without-JVM-argument


@Deprecated(since = "15", forRemoval = true)。该方法将在少数版本中发布。
约翰内斯·库恩

谢谢你提醒我。我可以继续尝试新的Use the MethodHandles.Lookup.defineHiddenClass(byte[], boolean, MethodHandles.Lookup.ClassOption...)
理想的

1
但是您需要为此进行私有查找-您不会得到。
约翰内斯·库恩

是的,我需要MethodHandles.Lookup.class.getDeclaredMethod(“ newLookup”)
理想

停止。请住手。如果有人要取消保修,那么他们应该添加适当的VM标志。
约翰内斯·库恩
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.