openjdk.ruOpenJDK на русском

JEP 396: Strongly Encapsulate JDK Internals by Default

Строгая инкапсуляция внутренних элементов JDK по умолчанию

AuthorsAlex Buckley, Mark Reinhold
ОтветственныйMark Reinhold
ТипFeature
ОбластьSE
СтатусClosed / Delivered
Выпуск16
Обсуждениеjigsaw dash dev at openjdk dot java dot net
ТрудоёмкостьS
ДлительностьS
Связан сJEP 261: Module System
JEP 260: Encapsulate Most Internal APIs
JEP 403: Strongly Encapsulate JDK Internals
РецензентыAlan Bateman, Chris Hegarty, Mandy Chung
ОдобренBrian Goetz
Создан2020/10/23 19:41
Обновлён2024/04/28 18:08
Задача8255363

Аннотация

По умолчанию строго инкапсулировать все внутренние элементы JDK, кроме критически важных внутренних API, таких как sun.misc.Unsafe. Дать конечным пользователям возможность выбрать ослабленную строгую инкапсуляцию, которая действует по умолчанию начиная с JDK 9.

Цели

  • Продолжить повышать безопасность и сопровождаемость JDK — это одна из главных целей проекта Jigsaw.

  • Побудить разработчиков перейти от использования внутренних элементов к стандартным API, чтобы и они, и их пользователи могли без затруднений переходить на будущие выпуски Java.

Что не является целью

  • Цель не состоит в том, чтобы удалить, инкапсулировать или изменить какие-либо критически важные внутренние API JDK, для которых ещё нет стандартной замены. Это означает, что sun.misc.Unsafe останется доступным.

  • Цель не состоит в том, чтобы определить новые стандартные API на замену внутренним элементам, для которых ещё нет стандартной замены, хотя такие API можно было бы предложить в ответ на этот JEP.

Мотивация

За прошедшие годы разработчики различных библиотек, фреймворков, инструментов и приложений использовали внутренние элементы JDK так, что это ставило под угрозу и безопасность, и сопровождаемость. В частности:

  • Некоторые классы, методы и поля пакетов java.*, не объявленные как public, определяют привилегированные операции, например возможность определить новый класс в заданном загрузчике классов, а другие передают конфиденциальные данные, например криптографические ключи. Эти элементы являются внутренними для JDK, хотя и находятся в пакетах java.*. Использование этих внутренних элементов внешним кодом через рефлексию угрожает безопасности платформы.

  • Все классы, методы и поля пакетов sun.* являются внутренними API JDK. Некоторые классы, методы и поля пакетов com.sun.*, jdk.* и org.* тоже являются внутренними API. Эти API никогда не были стандартными, никогда не поддерживались и никогда не предназначались для внешнего использования. Использование этих внутренних элементов внешним кодом — постоянная нагрузка на сопровождение. Время и силы, которые уходят на сохранение этих API, чтобы не сломать существующий код, было бы лучше потратить на развитие платформы.

В Java 9 мы повысили и безопасность, и сопровождаемость JDK, использовав модули, чтобы ограничить доступ к его внутренним элементам. Модули обеспечивают строгую инкапсуляцию, а это значит, что

  • код вне модуля может обращаться только к элементам public и protected пакетов, экспортируемых этим модулем, и

  • к элементам protected, кроме того, можно обращаться только из подклассов тех классов, в которых они определены.

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

В JDK 9 и последующих выпусках мы строго инкапсулировали все новые внутренние элементы и тем самым ограничили доступ к ним. Однако, чтобы облегчить миграцию, мы сознательно решили не инкапсулировать строго во время выполнения содержимое пакетов, которые существовали в JDK 8. Поэтому код библиотек и приложений на пути к классам (class path) мог и дальше через рефлексию обращаться к элементам пакетов java.*, не объявленным как public, и ко всем элементам sun.* и других внутренних пакетов — для пакетов, которые существовали в JDK 8. Такой режим называется ослабленной строгой инкапсуляцией.

Мы выпустили JDK 9 в сентябре 2017 года. У большинства часто используемых внутренних элементов JDK теперь есть стандартные замены. У разработчиков было больше трёх лет, чтобы перейти от внутренних элементов JDK к стандартным API, таким как java.lang.invoke.MethodHandles.Lookup::defineClass, java.util.Base64 и java.lang.ref.Cleaner. Многие сопровождающие библиотек, фреймворков и инструментов завершили этот переход и выпустили обновлённые версии своих компонентов. Теперь мы готовы сделать следующий шаг к строгой инкапсуляции всех внутренних элементов JDK — кроме критически важных внутренних API, таких как sun.misc.Unsafe, — как изначально и планировалось в проекте Jigsaw.

Описание

Ослабленной строгой инкапсуляцией управляет параметр средства запуска --illegal-access. Этот параметр, введённый в JEP 261, намеренно назван вызывающе, чтобы отбить желание его использовать. Сейчас он работает так:

  • --illegal-access=permit делает каждый пакет, существовавший в JDK 8, открытым для кода в безымянных модулях. Поэтому код на пути к классам может и дальше через рефлексию обращаться к непубличным элементам пакетов java.* и ко всем элементам sun.* и других внутренних пакетов — для пакетов, которые существовали в JDK 8. При первой операции рефлексивного доступа к любому такому элементу выдаётся предупреждение, но после этого предупреждения больше не выдаются.

    Этот режим действует по умолчанию начиная с JDK 9.

  • --illegal-access=warn идентичен permit, за исключением того, что предупреждение выдаётся при каждой недопустимой операции рефлексивного доступа.

  • --illegal-access=debug идентичен warn, за исключением того, что при каждой недопустимой операции рефлексивного доступа выдаются и предупреждение, и трассировка стека.

  • --illegal-access=deny запрещает все недопустимые операции доступа, кроме разрешённых другими параметрами командной строки, например, --add-opens.

В качестве следующего шага к строгой инкапсуляции всех внутренних элементов JDK мы предлагаем изменить режим параметра --illegal-access по умолчанию с permit на deny. После этого изменения пакеты, которые существовали в JDK 8 и не содержат критически важных внутренних API, больше не будут открыты по умолчанию; полный список доступен здесь. Пакет sun.misc по-прежнему будет экспортироваться модулем jdk.unsupported и по-прежнему будет доступен через рефлексию.

Мы также изменим соответствующий текст в Java Platform Specification (спецификации платформы Java), чтобы запретить открытие любого пакета по умолчанию в любой реализации платформы Java, если только этот пакет явно не объявлен как open в объявлении содержащего его модуля.

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

Мы ожидаем, что будущий JEP полностью удалит параметр --illegal-access. После этого открыть все пакеты JDK 8 одним параметром командной строки будет невозможно. Для открытия отдельных пакетов по-прежнему можно будет использовать параметр командной строки --add-opens или атрибут JAR-файла Add-Opens.

Чтобы подготовиться к будущему удалению параметра --illegal-access, в рамках этого JEP мы пометим его как Deprecated for Removal (устаревший, будет удалён). Поэтому при указании этого параметра средству запуска java будет выдаваться предупреждение об устаревании.

Риски и допущения

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

  • Фреймворки, которые используют методы defineClass класса java.lang.ClassLoader с модификатором protected, чтобы определять новые классы в существующих загрузчиках классов. Таким фреймворкам следует вместо этого использовать java.lang.invoke.MethodHandles.Lookup::defineClass, доступный начиная с JDK 9.

  • Код, который использует класс sun.util.calendar.ZoneInfo для работы с информацией о часовых поясах. Такому коду следует вместо этого использовать API java.time, доступный начиная с JDK 8.

  • Код, который использует пакет com.sun.rowset для обработки наборов строк SQL (row sets). Такому коду следует вместо этого использовать пакет javax.sql.rowset, доступный начиная с JDK 7.

  • Инструменты, которые используют пакеты com.sun.tools.javac.* для обработки исходного кода. Таким инструментам следует вместо этого использовать API javax.tools, javax.lang.model и com.sun.source.*, доступные начиная с JDK 6.

  • Код, который использует класс sun.security.tools.keytool.CertAndKeyGen для создания самоподписанных сертификатов. Стандартного API для этой функциональности пока нет (хотя запрос уже подан); тем временем разработчики могут использовать существующие сторонние библиотеки, в которых есть такая функциональность.

  • Код, который использует внутреннюю копию XML-процессора Xerces из JDK. Такому коду следует вместо этого использовать отдельную копию библиотеки Xerces, доступную в Maven Central.
  • Код, который использует внутреннюю версию библиотеки байт-кода ASM из JDK. Такому коду следует вместо этого использовать отдельную копию библиотеки ASM, доступную в Maven Central.

Мы призываем всех разработчиков:

  • С помощью инструмента jdeps найти код, который зависит от внутренних элементов JDK.

    • Если доступны стандартные замены, перейти на них.

    • В остальных случаях мы приветствуем обоснованные предложения новых стандартных API в рассылке проекта Jigsaw. Но, пожалуйста, учтите, что мы вряд ли будем определять новые стандартные API для внутренних элементов, которые не используются широко.

  • На существующем выпуске, например JDK 11, протестировать существующий код с --illegal-access=warn, чтобы выявить внутренние элементы, к которым обращаются через рефлексию, затем с помощью --illegal-access=debug точно найти проблемный код и, наконец, протестировать с --illegal-access=deny.

Второстепенные риски

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

  • Сопровождающие некоторых библиотек, фреймворков и инструментов говорят разработчикам приложений, что предупреждения о недопустимом рефлексивном доступе можно спокойно игнорировать при использовании JDK 9 и более поздних версий. Это создаёт напряжённость с разработчиками приложений, которые всегда используют самый последний выпуск JDK и понимают, что компоненты, от которых они зависят, сломаются, как только внутренние элементы JDK будут строго инкапсулированы по умолчанию. Для таких разработчиков приложений откат на JDK 8 или отказ от перехода на последний выпуск — неприемлемый вариант.

Примеры последствий этого изменения

  • Код, успешно скомпилированный в более ранних выпусках и напрямую обращающийся к внутренним API JDK, по умолчанию больше не будет работать. Например,

    System.out.println(sun.security.util.SecurityConstants.ALL_PERMISSION);

    завершится ошибкой с исключением вида

    Exception in thread "main" java.lang.IllegalAccessError: class Test
      (in unnamed module @0x5e481248) cannot access class
      sun.security.util.SecurityConstants (in module java.base) because
      module java.base does not export sun.security.util to unnamed
      module @0x5e481248
  • Код, который через рефлексию обращается к полям private экспортируемых API java.*, по умолчанию больше не будет работать. Например,

    var ks = java.security.KeyStore.getInstance("jceks");
    var f = ks.getClass().getDeclaredField("keyStoreSpi");
    f.setAccessible(true);

    завершится ошибкой с исключением вида

    Exception in thread "main" java.lang.reflect.InaccessibleObjectException:
      Unable to make field private java.security.KeyStoreSpi
      java.security.KeyStore.keyStoreSpi accessible: module java.base does
      not "opens java.security" to unnamed module @6e2c634b
  • Код, который через рефлексию вызывает методы protected экспортируемых API java.*, по умолчанию больше не будет работать. Например,

    var dc = ClassLoader.class.getDeclaredMethod("defineClass",
                                                 String.class,
                                                 byte[].class,
                                                 int.class,
                                                 int.class);
    dc.setAccessible(true);

    завершится ошибкой с исключением вида

    Exception in thread "main" java.lang.reflect.InaccessibleObjectException:
      Unable to make protected final java.lang.Class
      java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int)
      throws java.lang.ClassFormatError accessible: module java.base does
      not "opens java.lang" to unnamed module @5e481248