История. До Java 9 отражение могло произвольно обходить модификаторы доступа через setAccessible(true), нарушая инкапсуляцию по желанию. Введение Java Platform Module System (JPMS) установило строгую инкапсуляцию по умолчанию, при которой модули должны явным образом предоставлять разрешение на глубокий рефлексивный доступ к своим внутренним пакетам.
Проблема. Когда код в одном модуле пытается использовать MethodHandles или основное отражение для доступа к непубличному полю в пакете другого модуля, JVM выполняет строгую проверку доступа. Эта верификация обеспечивает, что целевой пакет был явно открыт для модуля вызывающего. Без этого разрешения JVM выбрасывает InaccessibleObjectException (или IllegalAccessException для наследуемого отражения), независимо от того, установлен ли SecurityManager или поле доступно через VarHandle.
Решение. Модуль должен объявить opens package.name [to specific.module]; в своем module-info.java, или приложение должно быть запущено с параметром --add-opens source.module/package.name=target.module. Эта директива динамически изменяет внутреннюю графику доступности модуля, добавляя целевой модуль в набор модулей, уполномоченных выполнять глубокое отражение над закрытыми членами этого пакета.
// Модуль: app.core (module-info.java) module app.core { // Пакет com.app.internal не открыт exports com.app.api; } // Модуль: framework.inject public class Injector { public void inject(Object target) throws Throwable { MethodHandles.Lookup lookup = MethodHandles.privateLookupIn( target.getClass(), MethodHandles.lookup() ); // Выбросит InaccessibleObjectException без --add-opens VarHandle handle = lookup.findVarHandle( target.getClass(), "secretField", String.class ); handle.set(target, "injected"); } }
Команда разработчиков мигрировала свое монолитное приложение на базе Spring в Java Module System, разделив код на модуль основной бизнес-логики (app.core) и отдельный модуль фреймворка внедрения зависимостей (framework.inject). Непосредственно после развертывания приложение аварийно завершилось во время инициализации бинов с InaccessibleObjectException, когда фреймворк пытался внедрить конфигурационные значения в закрытые поля, находящиеся в внутреннем пакете com.app.internal модуля app.core.
Три потенциальных архитектурных решения были оценены. Первый подход включал перемещение всех инжектируемых классов в экспортируемые пакеты в рамках app.core. Хотя это решало немедленное нарушение доступа, это в корне нарушало бы принципы инкапсуляции, выставляя внутренние детали реализации всем другим модулям, тем самым увеличивая нагрузку на поддержку и расширяя поверхность атаки для будущих проверок безопасности. Второе решение предложило использовать аргумент JVM --add-exports, чтобы открыть внутренние пакеты для модуля фреймворка. Однако, хотя --add-exports предоставляет видимость на этапе компиляции и времени выполнения для публичных типов, он явно не разрешает глубокое отражение над закрытыми членами, что делает его недостаточным для механизмов внедрения полей Spring, которые требуют изменения закрытого состояния. Третий вариант использовал целевой аргумент командной строки --add-opens app.core/com.app.internal=framework.inject. Этот подход сохранил строгую инкапсуляцию на уровне источника для всех других модулей, одновременно явно предоставляя только фреймворку внедрения необходимые привилегии для выполнения глубокого отражения над конкретным внутренним пакетом.
Команда в конечном итоге выбрала третий вариант, задокументировав необходимые директивы --add-opens в своих скриптах развертывания и конфигурациях Docker. Это решение сохранило целостность системы модулей во время разработки, позволяя фреймворку функционировать корректно, что привело к успешной миграции с явно контролируемыми границами доступа.
Почему setAccessible(true) не срабатывает на закрытом поле внутри экспортируемого пакета, когда доступ происходит из другого модуля, несмотря на отсутствие SecurityManager?
Кандидаты часто путают экспорт пакета с его открытостью. Директива exports просто делает публичные типы и члены доступными для стандартной компиляции и вызова; она не предоставляет ReflectPermission, необходимое для подавления проверок доступа языка Java. Сильная инкапсуляция JPMS работает независимо от SecurityManager, строго исполняется механизмами контроля доступа JVM. Чтобы разрешить setAccessible(true) на непубличных членах, пакет должен быть явным образом объявлен как open, или весь модуль должен быть объявлен как open module.
Как механизм захвата MethodHandles.Lookup влияет на межмодульную доступность, и почему вызов MethodHandles.lookup().in(targetClass) может ухудшить возможности поиска?
Объект Lookup инкапсулирует права доступа контекста своего создателя, модуля и пакета. Когда вызывается Lookup.in(targetClass), JVM переоценивает права доступа поиска на основе модуля целевого класса. Если целевой класс находится в другом модуле, который не открыл свой пакет для модуля поиска, доступ поиска «ухудшается» до режима PUBLIC, лишаясь доступа к PRIVATE и MODULE. Для поддержания полных прав доступа между модулями целевой модуль должен явно открыть пакет для модуля поиска, или код должен использовать privateLookupIn, что требует, чтобы целевой класс находился в том же модуле или был доступен через граф модулей.
В чем фундаментальное различие между --add-exports и --add-opens на уровне JVM, и почему первое вызывает IllegalAccessException во время внедрения зависимостей, даже когда компиляция проходит успешно?
Флаг --add-exports добавляет пакет в список экспортируемых модулем, позволяя целевому модулю получать доступ к публичным типам как на этапе компиляции, так и во время выполнения. Однако эта директива не изменяет «открытый» набор модуля, который управляет глубоким отражением. Java Language Specification строго разделяет читаемость (экспорт) и рефлексивность (открытие). Фреймворки внедрения зависимостей требуют последнего для манипуляции закрытыми полями через Reflection или VarHandle. Следовательно, хотя --add-exports удовлетворяет компилятору и позволяет вызывать методы, попытки изменить закрытое состояние во время выполнения все равно потерпят неудачу. Только --add-opens добавляет пакет в набор пакетов, доступных для глубокого отражения, разрешая тем самым фреймворку изменять значения закрытых полей.