Как скрыть предупреждение "Незаконный рефлексивный доступ" в java 9 без аргумента JVM?

Я просто попытался запустить свой сервер с 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, как указано в аналогичных вопросах.

5 ответов

Решение

Есть способы отключить предупреждение о незаконном доступе, хотя я не рекомендую делать это.

1. Простой подход

Поскольку предупреждение выводится в поток ошибок по умолчанию, вы можете просто закрыть этот поток и перенаправить stderr в stdout,

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

Заметки:

  • Этот подход объединяет ошибки и выходные потоки. Это может быть нежелательно в некоторых случаях.
  • Вы не можете перенаправить предупреждающее сообщение, просто позвонив System.setErr, поскольку ссылка на поток ошибок сохраняется в IllegalAccessLogger.warningStream поле в начале JVM начальной загрузки.

2. Сложный подход без изменения stderr

Хорошая новость в том, что sun.misc.Unsafe все еще может быть доступен в JDK 9 без предупреждений. Решение состоит в том, чтобы сбросить внутренний IllegalAccessLogger с помощью небезопасного 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
    }
}

Есть еще одна опция, которая не требует подавления потока и не использует недокументированные или неподдерживаемые 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-агент и указать его в командной строке (а не в classpath), используя -javaagent:myjar.jar, Агент будет содержать только premain метод следующим образом:

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

В качестве альтернативы, вы можете подключить динамически, используя API присоединения, который удобно доступен для проекта byte-buddy-agent (который я создал):

exportAndOpen(ByteBuddyAgent.install());

который вам нужно будет позвонить до незаконного доступа. Обратите внимание, что это доступно только в JDK и в виртуальных машинах Linux, в то время как вам потребуется указывать в командной строке агент Byte Buddy в качестве агента Java, если он вам нужен на других виртуальных машинах. Это может быть удобно, если вы хотите самоподключение на машинах тестирования и разработки, где обычно устанавливаются JDK.

Как отмечали другие, это должно служить лишь промежуточным решением, но я полностью понимаю, что текущее поведение часто нарушает сканеры журналирования и консольные приложения, поэтому я сам использовал это в производственных средах в качестве краткосрочного решения для использования Java 9 и До тех пор, пока я не столкнулся с какими-либо проблемами.

Однако хорошо то, что это решение устойчиво к будущим обновлениям, так как любая операция, даже динамическое вложение, является законной. Используя вспомогательный процесс, Байт Бадди даже работает вокруг обычно запрещенной самопривязанности.

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.

Я не знаю, как достичь того, о чем ты просишь. Как вы указали, вам нужно будет добавить параметры командной строки (--add-opens, хотя и не --illegal-access=deny) к запуску JVM.

Вы написали:

Моя цель - избежать дополнительных инструкций для конечных пользователей. У нас есть много пользователей с нашими установленными серверами, и это было бы большим неудобством для них.

Судя по всему, ваши требования оставляют только вывод о том, что проект не готов к Java 9. Он должен честно сообщить своим пользователям, что для полной совместимости с Java 9 требуется немного больше времени. Это нормально в начале этого релиза.

Существует еще один метод, не основанный на каких-либо взломах, который не упоминался ни в одном из ответов выше. Однако он работает только для кода, запущенного в пути к классам. Таким образом, любая библиотека, которая должна поддерживать работу на 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());
}

Мы также добавили проверку того, что мы действительно работаем по пути к классам.

Я придумал способ отключить это предупреждение без использования Unsafe и доступа к недокументированным API. Он работает, используя Reflection для установкиFilterOutputStream::out поле System.err обнулить.

Конечно, попытка использования Reflection фактически выдаст предупреждение, которое мы пытаемся подавить, но мы можем использовать параллелизм, чтобы обойти это:

  1. Замок System.err так что никакой другой поток не может писать в него.
  2. Создайте 2 потока, которые вызывают setAccessible на outполе. Один из них зависнет при попытке показать предупреждение, но другой завершится.
  3. Установить out поле System.err обнулить и снять блокировку 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, чтобы вы могли фильтровать будущие предупреждения.

Вот что сработало для меня

-Djdk.module.illegalAccess=deny

В случае, если кто-то захочет перенаправить сообщения журнала вместо их отбрасывания, это работает для меня в 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) {
    }
  }
}

Вы можете open пакеты в module-info.java или создать open module,

Пример. Оформить шаг 5 и 6 раздела " Перемещение вашего проекта в Jigsaw". Шаг за шагом.

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;
}
Другие вопросы по тегам