JEP 238: Multi-Release JAR Files
Многоверсионные JAR-файлы
| Ответственный | Paul Sandoz |
| Тип | Feature |
| Область | SE |
| Статус | Closed / Delivered |
| Выпуск | 9 |
| Компонент | tools / jar |
| Обсуждение | core dash libs dash dev at openjdk dot java dot net |
| Трудоёмкость | M |
| Длительность | M |
| Рецензенты | Alan Bateman, Brian Goetz, Paul Sandoz, Steve Drach |
| Одобрен | Brian Goetz |
| Создан | 2014/06/18 22:29 |
| Обновлён | 2026/07/29 18:52 |
| Задача | 8047305 |
Аннотация
Расширить формат JAR-файлов, чтобы в одном архиве могли сосуществовать несколько версий class-файлов, каждая для своего выпуска Java.
Цели
-
Доработать инструмент Java Archive Tool (
jar), чтобы он мог создавать многоверсионные JAR-файлы (multi-release JAR). -
Реализовать многоверсионные JAR-файлы в JRE, включая поддержку в стандартных загрузчиках классов и в API
JarFile. -
Доработать другие важные инструменты (например,
javac,javap,jdepsи т. д.), чтобы они могли обрабатывать многоверсионные JAR-файлы. -
Поддержать многоверсионные модульные JAR-файлы для целей 1–3.
-
Сохранить производительность: использование многоверсионных JAR-файлов не должно существенно влиять на производительность инструментов и компонентов. В частности, не должна снижаться производительность при доступе к обычным (т. е. не многоверсионным) JAR-файлам.
Мотивация
Сторонние библиотеки и фреймворки обычно поддерживают целый диапазон версий платформы Java, как правило на несколько версий назад. Поэтому они часто не используют возможности языка и API, доступные в более новых выпусках: трудно выразить условные зависимости от платформы, что обычно требует рефлексии, или распространять разные артефакты библиотеки для разных версий платформы.
У библиотек и фреймворков пропадает стимул использовать новые возможности, а из-за этого у пользователей пропадает стимул переходить на новые версии JDK — возникает порочный круг, который мешает внедрению и вредит всем.
Кроме того, некоторые библиотеки и фреймворки используют внутренние API JDK, которые станут недоступны в Java 9, когда границы модулей будут строго соблюдаться. Это тоже лишает стимула поддерживать новые версии платформы, даже если для таких внутренних API есть публичные поддерживаемые замены.
Описание
У JAR-файла есть корень содержимого, где находятся классы и ресурсы, а также каталог META-INF с метаданными о JAR-файле. Если добавить к определённым группам файлов метаданные о версиях, формат JAR сможет совместимым образом хранить несколько версий библиотеки для разных целевых выпусков платформы Java.
Многоверсионный JAR-файл («MRJAR») будет содержать главный атрибут:
Multi-Release: true
объявленный в главной секции MANIFEST.MF JAR-файла. Имя атрибута также объявлено как константа java.util.jar.Attributes.MULTI_RELEASE. Как и у других главных атрибутов, имя, объявленное в MANIFEST.MF, не зависит от регистра. Значение тоже не зависит от регистра, но перед ним и после него не должно быть пробельных символов (такое ограничение помогает достичь цели по производительности).
Многоверсионный JAR-файл («MRJAR») будет содержать дополнительные каталоги для классов и ресурсов, предназначенных для определённых выпусков платформы Java. JAR-файл типичной библиотеки может выглядеть так:
jar root
- A.class
- B.class
- C.class
- D.class
Предположим, что есть альтернативные версии A и B, которые могут использовать возможности Java 9. Их можно упаковать в один JAR-файл так:
jar root
- A.class
- B.class
- C.class
- D.class
- META-INF
- versions
- 9
- A.class
- B.class
В JDK, который не поддерживает MRJAR, будут видны только классы и ресурсы из корневого каталога, и два варианта упаковки будут неотличимы. В JDK, который поддерживает MRJAR, каталоги, соответствующие любым более поздним выпускам платформы Java, будут игнорироваться; поиск классов и ресурсов будет выполняться сначала в каталоге для той основной версии выпуска платформы Java, которая сейчас запущена, затем в каталогах для более низких версий и, наконец, в корне JAR-файла. В JDK для Java 9 это будет выглядеть так, как будто у JAR-файла есть собственный путь к классам, в котором сначала идут файлы версии 9, а затем корень JAR-файла; в JDK для Java 8 этот путь к классам будет содержать только корень JAR-файла.
Предположим, что в будущем выйдет Java 10 и A будет обновлён, чтобы использовать возможности Java 10. Тогда MRJAR может выглядеть так:
jar root
- A.class
- B.class
- C.class
- D.class
- META-INF
- versions
- 9
- A.class
- B.class
- 10
- A.class
По этой схеме версия класса, предназначенная для более позднего выпуска платформы Java, может переопределять версию того же класса, предназначенную для более раннего выпуска. В примере выше JDK для Java 9 с поддержкой MRJAR увидит версии A и B для 9 и общие версии C и D; будущий JDK для Java 10 с поддержкой MRJAR увидит версию A для 10 и версию B для 9; более старые JDK или JDK без поддержки MRJAR увидят только корневые версии всех классов.
Метаданные JAR-файла, например содержимое файла MANIFEST.MF и каталога META-INF/services, не нужно версионировать. MRJAR по сути является одной единицей выпуска, поэтому у него только одна версия выпуска (в этом он не отличается от обычного JAR-файла, распространяемого, скажем, через Maven Central), хотя внутри он содержит несколько версий реализации библиотеки для использования на разных выпусках платформы Java. Каждая версия библиотеки должна предоставлять один и тот же API; нужно исследовать, должна ли это быть строгая обратная совместимость, при которой API полностью совпадает (совпадение сигнатур байт-кода), или это требование можно в какой-то мере ослабить, не открывая обязательно возможности добавлять новые улучшения, которые размыли бы понятие единой единицы выпуска. Как минимум это может означать, что публичный класс, присутствующий в каталоге для определённого выпуска, должен присутствовать и в корне, хотя в каталоге более раннего выпуска его может не быть. Среда выполнения не будет проверять это свойство, но инструменты могут и должны обнаруживать такие проблемы совместимости API; кроме того, может быть предоставлен библиотечный метод для такой проверки (например, в java.util.jar.JarFile).
В итоге этот механизм позволяет разработчикам библиотек и фреймворков отделить использование API определённой версии выпуска платформы Java от требования, чтобы все их пользователи перешли на эту версию. Сопровождающие библиотек и фреймворков могут постепенно переходить на новые возможности и поддерживать их, продолжая поддерживать и старые. Так разрывается замкнутый круг «курица или яйцо», и библиотека может быть «готова к Java 9», фактически не требуя Java 9.
Подробности
Для поддержки многоверсионных JAR-файлов будут изменены следующие компоненты JDK.
-
URLClassLoaderна основе JAR-файлов должен читать выбранные версии class-файлов в соответствии с версией запущенной платформы Java. Аналогичные изменения потребуются для загрузчика классов на основе модулей, появившегося в проекте Jigsaw. -
Обработчик протокола для схемы URL
jarи классjava.util.jar.JarFileдолжны выбирать подходящую версию класса из многоверсионного JAR-файла. -
Компилятор Java (
javac) через нижележащие APIJavacFileManagerиZipFileSystemдолжен читать выбранные версии class-файлов в соответствии с параметрами командной строки-targetи-release. Инструментыjavah,schemagenиwsgenбудут использовать изменения вJavacFileManagerиZipFileSystem, на которых они основаны. -
Инструмент Java Archive (
jar) будет доработан, чтобы он мог создавать многоверсионные JAR-файлы. -
Инструмент упаковки JAR-файлов (
pack200/unpack200) должен быть обновлён (см. JDK-8066272). -
Инструмент
javapдолжен быть обновлён, чтобы можно было выбирать версионированные class-файлы. -
Инструмент
jdepsпотребуется изменить, чтобы он показывал информацию о версиях и учитывал зависимости class-файлов, специфичные для версии. -
Спецификацию JAR нужно переработать, чтобы описать формат многоверсионных JAR-файлов и все связанные изменения (например, возможные дополнения к манифесту).
Совместимость
По умолчанию поведение java.util.jar.JarFile и обработчиков протокола схемы jar останется прежним. Чтобы выбирать записи по версии, нужно явно включить эту возможность при создании JarFile, указывающего на MRJAR. Так же явное включение нужно для URL jar (подробности в следующем разделе).
Экземпляры JarFile, создаваемые средой выполнения для загрузки классов, будут явно включать эту возможность и настраиваться так, чтобы выбирать записи в соответствии с версией запущенной платформы Java. Про такой экземпляр JarFile говорят, что он версионирован по среде выполнения.
Ресурсы загрузчика классов
URL ресурса, возвращённый загрузчиком классов и указывающий на ресурс в MRJAR, будет ссылаться непосредственно на версионированную запись (если она есть). Например, для версионированного ресурса foo/baz/resource.txt:
URL r = loader.getResource("foo/baz/resource.txt");
URL «r» может быть таким:
jar:file:/mrjar.jar!/META-INF/versions/9/foo/baz/resource.txt
а не таким:
jar:file:/mrjar.jar!/foo/baz/resource.txt
Этот подход считается наименее разрушительным вариантом. Изменение структуры URL ресурсов (например, новая схема или добавленный фрагмент) сопряжено с риском. Старый код может обрабатывать символы URL напрямую, вместо того чтобы разбирать URL и корректно извлекать его части. Хотя такая обработка URL некорректна, было решено, что лучше не ломать такой код.
Многоверсионные модульные JAR-файлы
Многоверсионный модульный JAR-файл — это многоверсионный JAR-файл, у которого в корне на верхнем уровне есть дескриптор модуля module-info.class, как у модульного JAR-файла (см. раздел Packaging: Modular JAR в JEP 261). Кроме того, дескрипторы модулей могут присутствовать в версионированных областях. Такие версионированные дескрипторы должны быть идентичны корневому дескриптору модуля, за двумя исключениями:
-
у версионированного дескриптора могут быть другие не-
transitiveпредложенияrequiresдля модулейjava.*иjdk.*; и -
у версионированного дескриптора могут быть другие предложения
uses, даже для типов сервисов, определённых вне модулейjava.*иjdk.*.
Причина в том, что это детали реализации, а не часть API модуля, и их вполне может понадобиться менять по мере развития самого JDK. Изменения непубличных requires для модулей не из JDK не допускаются. Если это необходимо, требуется новая версия модуля (как минимум с увеличенным номером версии), и это уже другой вид проблемы совместимости, выходящий за рамки MRJAR.
Многоверсионному модульному файлу не обязательно иметь дескриптор модуля в найденном корне. В этом отношении дескриптор модуля обрабатывается так же, как любой другой class-файл или файл ресурса. Так можно добиться, например, чтобы в корневой области были только классы версии Java 8, а классы версии Java 9 (включая дескриптор модуля) находились в области версии 9.
Classpath и modulepath
Модульный JAR-файл можно собрать так, чтобы он корректно работал в classpath среды выполнения Java 8, в classpath среды выполнения Java 9 или в modulepath среды выполнения Java 9. То же самое верно для многоверсионного модульного JAR-файла (в котором, помимо module-info.class, другие классы могут быть скомпилированы для платформы Java 9).
Если дескриптор модуля не объявляет некоторые пакеты экспортируемыми и, следовательно, публичные классы в этих пакетах приватны для модуля, то при размещении соответствующего JAR-файла в module path эти классы будут недоступны. Однако если JAR-файл размещён в classpath, эти классы будут доступны. Это неприятное следствие поддержки и classpath, и modulepath.
В результате публичный API многоверсионного JAR-файла может различаться в зависимости от того, размещён он в classpath или в module path. Обычно инструмент jar при создании многоверсионного JAR-файла по мере возможности завершается с ошибкой, если обнаруживает различия в публичном API. Однако при создании многоверсионного модульного JAR-файла предлагается, чтобы инструмент jar выводил предупреждение, если различия в публичном API вызваны тем, что приватные классы модуля становятся доступны при размещении JAR-файла в classpath.
Многоверсионные JAR-файлы и загрузчик boot
Многоверсионные JAR-файлы не поддерживаются загрузчиком boot (например, когда многоверсионный JAR-файл указан в опции -Xbootclasspath/a). Такая поддержка усложнила бы реализацию загрузчика boot ради сценария, который считается редким.
Альтернативы
Распространённый подход — использовать статическую рефлексивную проверку, чтобы определить, есть ли возможность API, и в зависимости от этого выбрать подходящий класс, который, соответственно, зависит или не зависит от этой возможности. Затраты на рефлексию возникают при инициализации класса, а не при каждом использовании зависимой возможности. Для компиляции выбирается выпуск платформы Java, а флаги source и target указывают на более ранний выпуск, чтобы генерировать class-файлы, совместимые с этим более ранним выпуском. Этот подход часто дополняют инструментами вроде Animal Sniffer для проверки несовместимостей API. Помимо контроля совместимости API, код можно аннотировать, указав, зависит ли он от более позднего выпуска платформы Java. У этого подхода есть ряд ограничений:
-
Рефлексивные проверки нужно тщательно поддерживать.
-
Невозможно использовать новые возможности языка.
-
Если возможность API какого-либо выпуска платформы удалена (например, внутренний API), зависимый код не будет компилироваться.
Рассматривались «толстые» class-файлы, в которых у класса может быть один или несколько методов, предназначенных для разных версий платформы Java. Это было признано слишком сложным с точки зрения возможностей языка и среды выполнения, необходимых для поддержки таких объявлений методов и их динамического выбора.
Дескрипторы методов (invokedynamic) использовать нельзя, так как необходимо сохранять бинарную совместимость.
Риски и допущения
Ожидается, что создание MRJAR в основном совместимо с существующими популярными инструментами сборки и, следовательно, с IDE, которые поддерживают эти инструменты, но удобство работы разработчиков можно повысить за счёт улучшений.
Структуру исходного кода и сборку MRJAR-файла можно реализовать в Maven с помощью многомодульного проекта. Например, см. этот пример проекта Maven, который может создавать MRJAR-файл, пока довольно примитивный. В нём будет подпроект для корня и для конкретных выпусков платформы Java, а также подпроект, собирающий упомянутые подпроекты в MVJAR. Процесс сборки можно улучшить, например с помощью специального плагина Maven, используя те же возможности, что и инструмент jar, чтобы обеспечить обратную совместимость.
Дизайн и реализация обработки MRJAR во время выполнения в настоящее время предполагают, что среда выполнения использует URL class loader или что пользовательский загрузчик классов использует JarFile для получения class-файлов, специфичных для платформы. Среды выполнения, загрузчики классов которых используют ZipFile для загрузки классов, не будут учитывать MRJAR. Популярные фреймворки и инструменты для приложений, такие как Jetty, Tomcat, Maven и т. д., необходимо проверить на совместимость.
Зависимости
Расширенный формат JAR-файлов, который рассматривается для Java Platform Module System, должен будет учитывать метаданные многоверсионных JAR-файлов.
JEP 247 (Compile for Older Platform Versions), поддерживающий компиляцию с библиотеками более старых версий платформы, может помочь инструментам сборки в создании многоверсионных JAR-файлов.