Статьи

Отражение против инкапсуляции – автономно в модульной системе Java

Исторически рефлексия могла использоваться для взлома любого кода, работающего в той же JVM. С Java 9 это изменится. Одна из двух основных целей новой модульной системы — сильная инкапсуляция; предоставление модулям безопасного пространства, в которое не может проникнуть ни один код. Эти две техники явно расходятся, так как можно решить эту проблему? Похоже, что после долгих обсуждений недавнее предложение об открытых модулях покажет выход

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

Установка сцены

Позвольте мне установить сцену того, как модульная система реализует сильную инкапсуляцию и как это сталкивается с отражением.

Модульный ускоренный курс

Модуль Java Platform Module Saloon (JPMS) представляет концепцию модулей, которые в конечном итоге представляют собой обычные JAR-файлы с дескриптором модуля. Дескриптор скомпилирован из файла module-info.java

 module some.module {

    requires some.other.module;
    requires yet.another.module;

    exports some.module.package;
    exports some.module.other.package;

}

В контексте инкапсуляции следует обратить внимание на два момента:

  • Доступны только открытые типы, методы и поля в экспортированных пакетах.
  • Они доступны только для модулей, которым требуется модуль экспорта.

Здесь «быть доступным» означает, что код может быть скомпилирован с такими элементами и что JVM позволит получить к ним доступ во время выполнения. Таким образом, если код пользователя модуля зависит от кода владельца модуля, все, что нам нужно сделать, чтобы эта работа — это наличие пользователя требуется владелец и владельца экспортировать пакеты, содержащие требуемые типы:

 module user {
    requires owner;
}

module owner {
    exports owner.api.package;
}

Это частый случай, и кроме того, что мы делаем явные зависимости и API, известные системе модулей, все работает так, как мы привыкли.

Пока что всем весело! Затем, в «Размышлении»… разговоры прекращаются в середине предложения, пианист останавливает свою мелодию.

отражение

До Java 9 рефлексия позволяла взломать любой код. Помимо некоторых надоедливых вызовов setAccessible

 Integer two = 2;

Field value = Integer.class.getDeclaredField("value");
value.setAccessible(true);
value.set(two, 3);

if (1 + 1 != two)
    System.out.println("Math died!");

Эта мощь управляет всеми видами фреймворков — начиная с поставщиков JPA, таких как Hibernate, заканчивая тестированием библиотек, таких как JUnit и TestNG, инжекторами зависимостей, такими как Guice, и заканчивая одержимыми сканерами путей классов, такими как Spring, — которые отражают работу нашего приложения или тестового кода. их магия С другой стороны, у нас есть библиотеки, которым нужно что-то из JDK, которое было бы лучше не раскрывать (кто-нибудь говорил sun.misc.Unsafe Здесь также отражение было ответом.

Итак, этот парень, привыкший получать то, что он хочет, теперь заходит в Модульный салон, и бармен должен сказать ему «нет», не в этот раз.

The Stand Off

Внутри модульной системы (давайте отбросим седан, я думаю, вы поняли шутку) отражение могло когда-либо получать доступ только к коду в экспортированных пакетах. Внутренние пакеты модуля были запрещены, и это уже вызвало много шума . Но он все еще позволял использовать отражение для доступа ко всему остальному в экспортированном пакете, например, к видимым в пакете классам или частным полям и методам — ​​это называлось глубоким отражением . В сентябре правила стали еще строже! Теперь глубокое рефлексия также была запрещена, и рефлексия была не более мощной, чем статически типизированный код, который мы могли бы написать иначе: доступны только открытые типы, методы и поля в экспортированных пакетах.

Все, если это вызвало много дискуссий, конечно — некоторые горячие, некоторые дружелюбные, но все с чувством крайней важности.

Некоторые (включая меня) одобряют строгую инкапсуляцию, утверждая, что модулям необходимо безопасное пространство, в котором они могут организовать свои внутренние компоненты без риска другого кода, легко зависящего от него. Примеры, которые я хотел бы привести, — это JUnit 4, где одной из основных причин переписывания было то, что инструменты зависели от деталей реализации, отражая вплоть до уровня приватных полей; и Unsafe

Другие утверждают, что гибкость, обеспечиваемая рефлексией, не только обеспечивает отличное удобство использования для многих основанных на ней фреймворков, где для того, чтобы все работало, достаточно аннотировать некоторые сущности и помещать hibernate.jar в путь классов. Это также дает свободу пользователям библиотек, которые могут использовать свои зависимости так, как они хотят, что не всегда может быть так, как планировали сопровождающие. Здесь Unsafeбез одобрения команды JDK.

Хотя я склоняюсь к инкапсуляции, я вижу обоснованность и других аргументов. Так что делать? Какой выбор у разработчиков, кроме инкапсуляции их внутренних элементов и отказа от размышлений?

Выделитесь между отражением и инкапсуляцией в модульной системе Java

Выбор Оружия

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

  • Какие привилегии нам нужны, чтобы использовать этот подход?
  • Кто может получить доступ к внутренним органам?
  • Могут ли они быть доступны во время компиляции?

Для этого исследования мы создадим два модуля. Один называется владельцем и содержит один класс Ownerowner Другой, intruder , содержит класс IntruderOwner Его код сводится к этому:

 Class<?> owner = Class.forName("owner.Owner");
Method owned = owner.getDeclaredMethod(methodName);
owned.setAccessible(true);
owned.invoke(null);

Вызов setAccessible В конце мы получаем вывод следующим образом:

 public: ✓   protected: ✗   default: ✗   private: ✗

(Здесь доступен только публичный метод.)

Весь код, который я здесь использую, можно найти в репозитории GitHub , включая скрипты Linux, которые запускают его для вас.

Регулярный экспорт

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

 module owner {
    exports owner;
}

При этом мы получаем следующий результат:

 public: ✓   protected: ✗   default: ✗   private: ✗

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

Квалифицированный экспорт

Если экспорт ванильный, то это клюквенный ваниль — выбор по умолчанию с интересным поворотом. Модуль-владелец может экспортировать пакет в определенный модуль с помощью так называемого квалифицированного экспорта :

 module owner {
    exports owner to intruder;
}

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

 public: ✓   protected: ✗   default: ✗   private: ✗

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

Знание вторгающегося модуля может быть применимо в случае таких фреймворков, как Guice, но как только реализация скрывается за API (то, что команда JDK называет абстрактной отражающей структурой ; думаю, JPA и Hibernate), этот подход не срабатывает. Независимо от того, работает он или нет, явное присвоение имени вторгающемуся модулю в дескрипторе модуля-владельца может рассматриваться как ненадежное. С другой стороны, шансы на то, что владелец модуля уже в любом случае зависит от вторгающегося, потому что ему нужны какие-то аннотации или что-то в этом случае, и в этом случае мы не делаем вещи намного хуже.

Открытые пакеты

Теперь это становится интересным. Довольно недавним дополнением к модульной системе является способность модулей открывать пакеты только во время выполнения.

 module owner {
    opens owner;
}

Уступая:

 public: ✓   protected: ✓   default: ✓   private: ✓

Ухоженная! Мы убили двух зайцев одним выстрелом:

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

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

Квалифицированные открытые пакеты

Как и в случае экспорта и квалифицированного экспорта, существует также квалифицированный вариант открытых пакетов:

 module owner {
    opens owner to intruder;
}

Запустив программу, мы получаем тот же результат, что и раньше, но теперь только злоумышленник может достичь их:

 public: ✓   protected: ✓   default: ✓   private: ✓

Это дает нам тот же компромисс, что и между экспортом и квалифицированным экспортом, а также не работает для разделения между API и реализацией. Но есть надежда!

В ноябре Марк Рейнхольд предложил механизм, который позволял бы создавать код в модуле, для которого был открыт пакет, для передачи этого доступа третьему модулю. Возвращаясь к JPA и Hibernate, это точно решает эту проблему. Предположим следующий дескриптор модуля для владельца :

 module owner {
    // the JPA module is called java.persistence
    opens owner to java.persistence;
}

В этом случае механизм может быть использован следующим образом (цитируется почти дословно из предложения):

Менеджер сущностей JPA создается с помощью одного из методов Persistence::createEntityManagerFactory , которые обнаруживают и инициализируют подходящего поставщика постоянства, например, Hibernate. В рамках этого процесса они могут использовать метод addOpensвладельца клиентского модуля, чтобы открыть пакет owner Это будет работать, так как модуль ownerjava.persistence

Существует также вариант, когда контейнеры открывают пакеты для реализаций. В текущей сборке EA (b146) эта функция, похоже, еще не реализована, поэтому я не смог ее опробовать. Но это определенно выглядит многообещающе!

Открытый модуль

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

 opens

Это дает тот же доступ, что и индивидуально открытые пакеты:

 open module owner { }

Открытые модули можно считать промежуточным этапом на пути перехода от JAR-файлов к пути классов к полнофункциональным, сильно инкапсулированным модулям.

Обман класса путь

Теперь мы входим в менее модульную площадку. Как вы, наверное, знаете, public: ✓ protected: ✓ default: ✓ private: ✓
java
Но путь класса не уходит, и JAR-файлы тоже нет. Мы можем использовать два трюка, если у нас есть доступ к командной строке запуска и мы можем перемещать артефакт (поэтому это не будет работать для модулей JDK).

Безымянный модуль

Во-первых, мы можем поместить модуль-владелец в путь к классам.

Как модульная система реагирует на это? Поскольку все должно быть модулем, система модулей просто создает один, безымянный модуль , и помещает в него все, что находит в пути к классам. Внутри безымянного модуля все очень похоже на сегодняшний день, и ад JAR продолжает существовать. Поскольку неназванный модуль является синтетическим, JPMS понятия не имеет, что он может экспортировать, поэтому он просто экспортирует все — во время компиляции и во время выполнения.

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

 javac

Та-да! И не касаясь модуля-владельца, мы можем сделать это с модулями, над которыми у нас нет контроля. Небольшое предостережение: мы не можем требовать неназванного модуля, поэтому нет хорошего способа компилировать код из модуля-владельца из других модулей. Ну, может быть, предостережение не так уж и мало …

Автоматический модуль

Второй подход состоит в том, чтобы лишить модуль-владелец его дескриптора и все же поместить его в путь модуля. Для каждого обычного JAR-файла на пути к модулю JPMS создает новый модуль, присваивает ему имя автоматически на основе имени файла и экспортирует все его содержимое. Поскольку все экспортируется, мы получаем тот же результат, что и с неназванным модулем:

 public: ✓   protected: ✓   default: ✓   private: ✓

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

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

Командная строка Escape Люки

Так как мы все равно возимся с командной строкой, есть более чистый подход (возможно, я должен был рассказать вам об этом ранее): и public: ✓ protected: ✓ default: ✓ private: ✓
javacjava

 --add-opens

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

Выключатель инкапсуляции

Если все это кажется слишком громоздким, и вы просто хотите, чтобы оно работало для вашего приложения пути к java
--module-path mods
--add-modules owner
--add-opens owner/owner=intruder
--module intruder
--permit-illegal-accessдружно названный инкапсуляцией «kill switch» , является правильным аргументом командной строки для вас.
С его помощью весь код в безымянном модуле может обращаться к другим типам независимо от любых ограничений, которые накладывает сильная инкапсуляция. Взамен вы получаете предупреждения о незаконном доступе и удушающем ощущении жизни в скрытое время: опция будет существовать только в Java 9 для облегчения миграции, поэтому вы просто купили себе два, три года.

Вот как запустить с владельцем на модуле и нарушителя на пути к классам:

 java
    --class-path mods/intruder.jar
    --module-path mods/owner.jar
    --add-modules owner
    --permit-illegal-access
    intruder.Intruder

И вот результат — обратите внимание, как предупреждения вписываются в мой тщательно созданный вывод:

 WARNING: --permit-illegal-access will be removed in the next major release
public:WARNING: Illegal access by intruder.Intruder (file:mods/intruder.jar) to method owner.Owner.publicMethod() (permitted by --permit-illegal-access)
 ✓   protected:WARNING: Illegal access by intruder.Intruder (file:mods/intruder.jar) to method owner.Owner.protectedMethod() (permitted by --permit-illegal-access)
 ✓   default:WARNING: Illegal access by intruder.Intruder (file:mods/intruder.jar) to method owner.Owner.defaultMethod() (permitted by --permit-illegal-access)
 ✓   private:WARNING: Illegal access by intruder.Intruder (file:mods/intruder.jar) to method owner.Owner.privateMethod() (permitted by --permit-illegal-access)
 ✓

Как я уже сказал, это работает, только если злоумышленник прибыл из неназванного модуля. Основанием для этого является то, что должным образом модульный код, то есть код, который находится в модулях, не должен нуждаться в таких взломах. Действительно, если мы поместим оба артефакта в путь к модулю, мы увидим, что флаг не используется (нет предупреждений, кроме первоначального), и доступ не выполняется, потому что внутренняя структура owner

 java
    --module-path mods
    --add-modules owner
    --permit-illegal-access
    --module intruder

WARNING: --permit-illegal-access will be removed in the next major release
public: ✗   protected: ✗   default: ✗   private: ✗

Резюме

Оки, все еще помнишь все, что мы делали? Нет? Сводная сводная таблица на помощь!

механизм доступ скомпилировать доступ доступ к отражению Комментарии
экспорт дескриптор весь код> общедоступный весь код> общедоступный делает API общедоступным
квалифицированный экспорт дескриптор указанные модули> общедоступные указанные модули> общедоступные нужно знать вторгающиеся модули
открытая упаковка дескриптор никто весь код> частный
квалифицированный открытый пакет дескриптор никто указанные модули> частные могут быть перенесены в модули реализации
открытый модуль дескриптор никто весь код> частный одно ключевое слово, чтобы открыть все пакеты
неназванный модуль командная строка и артефакт все немодули> общедоступные весь код> частный
автоматический модуль командная строка и артефакт весь код> общедоступный весь код> частный требует возиться с артефактом
флаги командной строки командная строка никто весь код> частный
Аварийная кнопка командная строка никто безымянный модуль> частный

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

Это ваш собственный модуль?

  • Да ⇝ Открытые пакеты (возможно, квалифицированные) или, если их слишком много, весь модуль.
  • Нет ⇝ Используйте флаги командной строки --add-opens--permit-illegal-access