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