Перейти к основному содержимому

Предупреждение о нелегальном рефлексивном доступе Java 9

· 6 мин. чтения

1. Обзор

До Java 9 API-интерфейс Java Reflection обладал суперсилой: он мог без ограничений получать доступ к закрытым членам класса. После Java 9 модульная система хочет разумно ограничить API Reflection.

В этом руководстве мы рассмотрим взаимосвязь между системой модулей и отражением.

2. Модульная система и отражение

Несмотря на то, что отражение и модульная система появляются в истории Java в разное время, они должны работать вместе, чтобы создать надежную платформу.

2.1. Базовая модель

Одной из целей модульной системы Java является строгая инкапсуляция. Строгая инкапсуляция в основном состоит из удобочитаемости и доступности :

  • Читаемость модулей является грубой концепцией и касается того, зависит ли один модуль от другого модуля.
  • Доступность модулей является более тонкой концепцией и заботится о том, может ли один класс получить доступ к полю или методу другого класса. Это обеспечивается границей класса, границей пакета и границей модуля.

./7528780d47d2d71471ed9077a2c56a61.png

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

Чтобы повысить читаемость, мы можем использовать директиву « requires » в объявлении модуля, указать опцию « -add-reads » в командной строке или вызвать метод Module.addReads . Таким же образом, чтобы разорвать инкапсуляцию границ, мы можем использовать директиву opens в объявлении модуля, указать параметр --add-opens в командной строке или вызвать метод Module.addOpens .

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

2.2. Различные варианты использования отражения

В модульной системе Java существуют различные типы модулей, например, именованный модуль, безымянный модуль, модуль платформы/системы, модуль приложения и т. д.:

./ff4268d2b08af67f74e5751edc635b22.png

Чтобы было ясно, два понятия «модульная система» и «системный модуль» могут показаться запутанными. Итак, давайте использовать понятие «модуль платформы» вместо «модуль системы».

Учитывая вышеперечисленные типы модулей, существует довольно много комбинаций между различными типами модулей. Как правило, безымянный модуль не может быть прочитан именованными модулями, за исключением автоматических модулей. Давайте рассмотрим только три типичных сценария:

./6d9310aba3d9a8af34638446086905fa.png

На приведенном выше рисунке глубокое отражение означает использование Reflection API для получения доступа к закрытым членам класса путем вызова метода setAccessible(flag) . При использовании отражения для доступа к именованному модулю из другого именованного модуля мы получим исключение IllegalAccessException или InaccessibleObjectException . Точно так же при использовании отражения для доступа к именованному модулю приложения из безымянного модуля мы получаем те же ошибки.

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

WARNING: Illegal reflective access by $PERPETRATOR to $VICTIM

В приведенной выше форме предупреждающего сообщения $PERPETRATOR представляет информацию об отражающем классе, а $VICTIM представляет информацию об отражаемом классе. И это сообщение связано с ослабленной сильной инкапсуляцией .

2.3. Расслабленная сильная инкапсуляция

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

Расслабленная строгая инкапсуляция предоставляет параметр запуска -незаконный доступ для управления поведением во время выполнения. Следует отметить, что параметр --illegal-access работает только тогда, когда мы используем отражение для доступа к модулям платформы из неназванных модулей. В противном случае этот параметр не действует.

Опция --illegal-access имеет четыре конкретных значения:

  • разрешение : открывает каждый пакет модулей платформы для безымянных модулей и показывает предупреждающее сообщение только один раз.
  • warn : идентичен « разрешению », но показывает предупреждающее сообщение о каждой незаконной операции рефлексивного доступа .
  • debug : идентичен « warn », а также печатает соответствующую трассировку стека
  • deny : отключает все незаконные операции рефлексивного доступа.

Начиная с Java 9, --illegal-access=permit является режимом по умолчанию. Чтобы использовать другие режимы, мы можем указать эту опцию в командной строке:

java --illegal-access=deny com.foreach.module.unnamed.Main

В Java 16 параметр –illegal-access=deny становится режимом по умолчанию. Начиная с Java 17, опция –illegal-access полностью удалена .

3. Как исправить незаконный доступ к отражению

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

3.1. В объявлении модуля

Если мы являемся автором кода, мы можем открыть пакет в module-info.java :

module foreach.reflected {
opens com.foreach.reflected.opened;
}

Чтобы быть более осторожными, мы можем использовать квалифицированные открытия :

module foreach.reflected {
opens com.foreach.reflected.internal to foreach.intermedium;
}

При переносе нашего существующего кода на модульную систему для удобства мы можем открыть весь модуль:

open module foreach.reflected {
// don't use opens directive
}

Следует отметить, что открытый модуль не допускает внутренних директив открытия .

3.2. В командной строке

Если мы не являемся автором кода, мы можем использовать параметр --add-opens в командной строке:

--add-opens java.base/java.lang=foreach.reflecting.named

И, чтобы добавить открытия ко всем безымянным модулям, мы можем использовать ALL-UNNAMED :

java --add-opens java.base/java.lang=ALL-UNNAMED

3.3. Во время выполнения

Чтобы добавить открытия во время выполнения, мы можем использовать метод Module.addOpens :

srcModule.addOpens("com.foreach.reflected.internal", targetModule);

В приведенном выше фрагменте кода srcModule открывает пакет « com.foreach.reflected.internal » для targetModule .

Следует отметить одну вещь: метод Module.addOpens чувствителен к вызывающей стороне . Этот метод будет успешным только тогда, когда мы вызываем его из модифицируемого модуля, из модулей, к которым он предоставил открытый доступ, или из безымянного модуля. В противном случае это приведет к IllegalCallerException .

Другой способ добавления открытий к целевому модулю — использование агента Java . В модуле java.instrument класс Instrumentation добавил новый метод redefineModule начиная с Java 9. Этот метод можно использовать для добавления дополнительных операций чтения, экспорта, открытия, использования и предоставления:

void redefineModule(Instrumentation inst, Module src, Module target) {
// prepare extra reads
Set<Module> extraReads = Collections.singleton(target);

// prepare extra exports
Set<String> packages = src.getPackages();
Map<String, Set<Module>> extraExports = new HashMap<>();
for (String pkg : packages) {
extraExports.put(pkg, extraReads);
}

// prepare extra opens
Map<String, Set<Module>> extraOpens = new HashMap<>();
for (String pkg : packages) {
extraOpens.put(pkg, extraReads);
}

// prepare extra uses
Set<Class<?>> extraUses = Collections.emptySet();

// prepare extra provides
Map<Class<?>, List<Class<?>>> extraProvides = Collections.emptyMap();

// redefine module
inst.redefineModule(src, extraReads, extraExports, extraOpens, extraUses, extraProvides);
}

В приведенном выше коде мы сначала используем целевой модуль для создания переменных extraReads , extraExports и extraOpens . Затем мы вызываем метод Instrumentation.redefineModule . В результате модуль src будет доступен целевому модулю.

4. Вывод

В этом уроке мы впервые представили удобочитаемость и доступность модульной системы. Затем мы рассмотрели различные варианты использования нелегального рефлексивного доступа и то, как упрощенная строгая инкапсуляция помогает нам перейти с Java 8 на модульную систему Java 9. Наконец, мы предоставили различные подходы к решению проблемы нелегального отражающего доступа.

Как обычно, исходный код этого руководства можно найти на GitHub .