JEP 396: Strongly Encapsulate JDK Internals by Default
Строгая инкапсуляция внутренних элементов JDK по умолчанию
| Authors | Alex 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для работы с информацией о часовых поясах. Такому коду следует вместо этого использовать APIjava.time, доступный начиная с JDK 8. -
Код, который использует пакет
com.sun.rowsetдля обработки наборов строк SQL (row sets). Такому коду следует вместо этого использовать пакетjavax.sql.rowset, доступный начиная с JDK 7. -
Инструменты, которые используют пакеты
com.sun.tools.javac.*для обработки исходного кода. Таким инструментам следует вместо этого использовать APIjavax.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экспортируемых APIjava.*, по умолчанию больше не будет работать. Например,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экспортируемых APIjava.*, по умолчанию больше не будет работать. Например,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