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

JEP 403: Strongly Encapsulate JDK Internals

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

AuthorsAlex Buckley, Mark Reinhold
ОтветственныйMark Reinhold
ТипFeature
ОбластьSE
СтатусClosed / Delivered
Выпуск17
Обсуждениеjigsaw dash dev at openjdk dot java dot net
ТрудоёмкостьXS
ДлительностьXS
Связан сJEP 261: Module System
JEP 260: Encapsulate Most Internal APIs
JEP 396: Strongly Encapsulate JDK Internals by Default
РецензентыAlan Bateman, Chris Hegarty, Mandy Chung
ОдобренBrian Goetz
Создан2021/03/13 00:19
Обновлён2025/08/21 10:02
Задача8263547

Аннотация

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

История

Этот JEP — преемник JEP 396, который перевёл JDK с режима ослабленной строгой инкапсуляции по умолчанию на режим строгой инкапсуляции по умолчанию, но позволял пользователям при желании вернуться к ослабленному режиму. Разделы «Цели», «Что не является целью», «Мотивация» и «Риски и допущения» этого JEP по существу совпадают с соответствующими разделами JEP 396, но воспроизведены здесь для удобства читателя.

Цели

  • Продолжить повышать безопасность и удобство сопровождения 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. Поэтому код библиотек и приложений в пути классов мог по-прежнему через рефлексию обращаться к элементам пакетов java.*, не являющимся public, и ко всем элементам пакетов sun.* и других внутренних пакетов — для пакетов, существовавших в JDK 8. Такой порядок называется ослабленной строгой инкапсуляцией и был поведением по умолчанию в JDK 9.

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

В JDK 16, выпущенном в марте 2021 года, мы сделали следующий шаг к строгой инкапсуляции всех внутренних элементов JDK. JEP 396 сделал строгую инкапсуляцию поведением по умолчанию, за исключением критически важных внутренних API, таких как sun.misc.Unsafe, которые остались доступными. В JDK 16 конечные пользователи ещё могли выбрать ослабленную строгую инкапсуляцию, чтобы получить доступ к внутренним элементам, существовавшим в JDK 8.

Теперь мы готовы сделать на этом пути ещё один шаг и убрать возможность выбрать ослабленную строгую инкапсуляцию. Это означает, что все внутренние элементы JDK будут строго инкапсулированы, за исключением критически важных внутренних API, таких как sun.misc.Unsafe.

Описание

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

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

    Этот режим был режимом по умолчанию с JDK 9 по JDK 15.

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

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

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

    Этот режим был режимом по умолчанию в JDK 16.

В качестве следующего шага к строгой инкапсуляции всех внутренних элементов JDK мы предлагаем сделать параметр --illegal-access вышедшим из употребления (obsolete). Любое использование этого параметра, будь то с permit, warn, debug или deny, не будет иметь никакого эффекта, кроме вывода предупреждения. Мы ожидаем полностью удалить параметр --illegal-access в одном из будущих выпусков.

После этого изменения конечные пользователи больше не смогут с помощью параметра --illegal-access разрешать доступ к внутренним элементам JDK. (Список затронутых пакетов доступен здесь.) Пакеты sun.misc и sun.reflect по-прежнему будут экспортироваться модулем jdk.unsupported и по-прежнему будут открыты, так что код сможет обращаться к их непубличным элементам через рефлексию. Никакие другие пакеты JDK таким образом открыты не будут.

Открывать отдельные пакеты по-прежнему можно будет с помощью параметра командной строки --add-opens или атрибута манифеста JAR-файла Add-Opens.

Экспортируемые API com.sun

Большинство пакетов com.sun.* в JDK предназначены для внутреннего использования, но некоторые поддерживаются для внешнего использования. Эти поддерживаемые пакеты экспортировались в JDK 9 и будут экспортироваться и дальше, так что вы можете по-прежнему писать код с использованием их публичных API. Однако открытыми они больше не будут. Примеры:

  • Compiler Tree API в модуле jdk.compiler,
  • HTTP Server API в модуле jdk.httpserver,
  • SCTP API в модуле jdk.sctp и
  • специфичные для JDK расширения NIO API в пакете com.sun.nio.file модуля jdk.unsupported.

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

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

  • Фреймворки, которые используют методы protected defineClass класса java.lang.ClassLoader, чтобы определять новые классы в существующих загрузчиках классов. Такие фреймворки должны вместо этого использовать 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