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

JEP 261: Module System

Система модулей

AuthorsAlan Bateman, Alex Buckley, Jonathan Gibbons, Mark Reinhold
ОтветственныйMark Reinhold
ТипFeature
ОбластьSE
СтатусClosed / Delivered
Выпуск9
JSR376
Обсуждениеjigsaw dash dev at openjdk dot java dot net
ТрудоёмкостьXL
ДлительностьL
БлокируетJEP 200: The Modular JDK
JEP 282: jlink: The Java Linker
Зависит отJEP 220: Modular Run-Time Images
JEP 260: Encapsulate Most Internal APIs
Связан сJEP 396: Strongly Encapsulate JDK Internals by Default
JEP 403: Strongly Encapsulate JDK Internals
РецензентыAlan Bateman, Alex Buckley, Chris Hegarty, Jonathan Gibbons, Mandy Chung, Paul Sandoz
ОдобренBrian Goetz
Создан2014/10/23 15:05
Обновлён2024/08/19 18:30
Задача8061972

Аннотация

Реализовать систему модулей платформы Java (Java Platform Module System), как она определена в JSR 376, вместе со связанными с ней изменениями и улучшениями, специфичными для JDK.

Описание

Java Platform Module System (JSR 376) определяет изменения и расширения языка программирования Java, виртуальной машины Java и стандартных Java API. Этот JEP реализует эту спецификацию. В результате компилятор javac, виртуальная машина HotSpot и библиотеки времени выполнения реализуют модули как принципиально новый вид компонентов Java-программ и обеспечивают надёжную конфигурацию и строгую инкапсуляцию модулей на всех этапах разработки.

Этот JEP также изменяет, расширяет и дополняет специфичные для JDK инструменты и API, которые выходят за рамки JSR и связаны с компиляцией, компоновкой и выполнением. Связанные с этим изменения других инструментов и API, например инструмента javadoc и Doclet API, рассматриваются в отдельных JEP.

Предполагается, что читатель этого JEP знаком с последней версией документа State of the Module System, а также с другими JEP проекта Jigsaw:

Этапы

К привычным этапам времени компиляции (команда javac) и времени выполнения (средство запуска java) мы добавляем понятие времени компоновки — необязательного этапа между ними, на котором набор модулей можно собрать и оптимизировать в собственный образ среды выполнения. Инструменту компоновки jlink посвящён JEP 282; многие новые параметры командной строки, реализованные в javac и java, реализованы также в jlink.

Пути модулей

Команды javac, jlink и java, а также ряд других теперь принимают параметры, задающие различные пути модулей. Путь модулей — это последовательность, каждый элемент которой является либо определением модуля, либо каталогом, содержащим определения модулей. Каждое определение модуля — это либо

  • артефакт модуля, т. е. модульный JAR-файл или файл JMOD, содержащий скомпилированное определение модуля, либо

  • каталог развёрнутого модуля, имя которого по соглашению совпадает с именем модуля, а содержимое представляет собой «развёрнутое» дерево каталогов, соответствующее иерархии пакетов.

Во втором случае дерево каталогов может быть скомпилированным определением модуля, заполненным отдельными файлами классов и ресурсов и содержащим файл module-info.class в корне, либо, во время компиляции, исходным определением модуля, заполненным отдельными исходными файлами и содержащим файл module-info.java в корне.

Путь модулей, как и другие виды путей, задаётся строкой из имён путей, разделённых символом-разделителем путей платформы (':' на большинстве платформ, ';' в Windows).

Пути модулей сильно отличаются от путей классов: пути классов служат для поиска определений отдельных типов и ресурсов, а пути модулей — для поиска определений целых модулей. Каждый элемент пути классов — это контейнер определений типов и ресурсов, т. е. либо JAR-файл, либо развёрнутое дерево каталогов, соответствующее иерархии пакетов. Каждый элемент пути модулей, напротив, — это определение модуля или каталог, каждый элемент которого является определением модуля, т. е. контейнером определений типов и ресурсов, т. е. либо модульным JAR-файлом, либо файлом JMOD, либо каталогом развёрнутого модуля.

В процессе разрешения система модулей находит модуль, выполняя поиск по нескольким различным путям в зависимости от этапа, а также среди скомпилированных модулей, встроенных в среду, в следующем порядке:

  • Путь модулей компиляции (задаётся параметром командной строки --module-source-path) содержит определения модулей в виде исходного кода (только во время компиляции).

  • Путь обновляемых модулей (--upgrade-module-path) содержит скомпилированные определения модулей, которые следует использовать вместо скомпилированных определений любых обновляемых модулей, присутствующих среди системных модулей или в пути модулей приложения (во время компиляции и во время выполнения).

  • Системные модули — это скомпилированные модули, встроенные в среду (во время компиляции и во время выполнения). Обычно к ним относятся модули Java SE и JDK, но в случае собственного скомпонованного образа к ним могут относиться также модули библиотек и приложений. Во время компиляции системные модули можно переопределить с помощью параметра --system, который задаёт образ JDK, из которого загружаются системные модули.

  • Путь модулей приложения (--module-path, или сокращённо -p) содержит скомпилированные определения модулей библиотек и приложений (на всех этапах). Во время компоновки этот путь может также содержать модули Java SE и JDK.

Определения модулей, присутствующие в этих путях, вместе с системными модулями образуют множество наблюдаемых модулей.

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

Корневые модули

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

Когда компилятор компилирует код в безымянном модуле или когда вызывается средство запуска java и главный класс приложения загружается из пути классов в безымянный модуль загрузчика классов приложения, набор корневых модулей по умолчанию для безымянного модуля в JDK 9 вычисляется следующим образом:

  • Модуль java.se является корневым, если он существует. Если его нет, корневым является каждый модуль java.* в пути обновляемых модулей или среди системных модулей, который экспортирует хотя бы один пакет без квалификации.

  • Каждый модуль, не относящийся к java.*, в пути обновляемых модулей или среди системных модулей, который экспортирует хотя бы один пакет без квалификации, также является корневым.

Обновление, июнь 2018 года: в JDK 11 набор корневых модулей по умолчанию для безымянного модуля изменился. Теперь набор по умолчанию вычисляется следующим образом:

  • Корневым является каждый модуль в пути обновляемых модулей или среди системных модулей, который экспортирует хотя бы один пакет без квалификации.

Модуль java.se по-прежнему существует в JDK 11 и более поздних версиях, но больше не является корневым.

В остальных случаях набор корневых модулей по умолчанию зависит от этапа:

  • во время компиляции это обычно набор компилируемых модулей (подробнее об этом ниже);

  • во время компоновки он пуст; и

  • во время выполнения это главный модуль приложения, заданный параметром средства запуска --module (или сокращённо -m).

Иногда необходимо добавить модули в корневой набор по умолчанию, чтобы гарантировать, что определённые модули платформы, библиотек или поставщиков сервисов будут присутствовать в итоговом графе модулей. На любом этапе параметр

--add-modules <module>(,<module>)*

где <module> — имя модуля, добавляет указанные модули в набор корневых модулей по умолчанию. Этот параметр можно указывать несколько раз.

Особый случай во время выполнения: если <module> равно ALL-DEFAULT, в корневой набор добавляется набор корневых модулей по умолчанию для безымянного модуля, определённый выше. Это полезно, когда приложение является контейнером, в котором размещаются другие приложения, которые, в свою очередь, могут зависеть от модулей, не требуемых самим контейнером.

Ещё один особый случай во время выполнения: если <module> равно ALL-SYSTEM, в корневой набор добавляются все системные модули, независимо от того, входят ли они в набор по умолчанию. Иногда это нужно тестовым средам. Этот параметр приводит к разрешению множества модулей; как правило, следует отдавать предпочтение ALL-DEFAULT.

И последний особый случай: как во время выполнения, так и во время компоновки, если <module> равно ALL-MODULE-PATH, в корневой набор добавляются все наблюдаемые модули, найденные в соответствующих путях модулей. ALL-MODULE-PATH допустим как во время компиляции, так и во время выполнения. Это предусмотрено для инструментов сборки, таких как Maven, которые уже гарантируют, что все модули в пути модулей необходимы. Это также удобный способ добавить в корневой набор автоматические модули.

Ограничение наблюдаемых модулей

Иногда полезно ограничить наблюдаемые модули, например, для отладки или чтобы уменьшить число разрешаемых модулей, когда главным модулем является безымянный модуль, определённый загрузчиком классов приложения для пути классов. Для этого на любом этапе можно использовать параметр --limit-modules. Его синтаксис:

--limit-modules <module>(,<module>)*

где <module> — имя модуля. Этот параметр ограничивает наблюдаемые модули теми, которые входят в транзитивное замыкание указанных модулей, плюс главным модулем, если он есть, плюс любыми другими модулями, заданными параметром --add-modules.

(Транзитивное замыкание, вычисляемое для интерпретации параметра --limit-modules, — временный результат, который используется только для вычисления ограниченного набора наблюдаемых модулей. Механизм разрешения будет вызван снова, чтобы вычислить фактический граф модулей.)

Расширение читаемости

При тестировании и отладке иногда нужно сделать так, чтобы один модуль читал другой, даже если первый модуль не зависит от второго через предложение requires в своём объявлении модуля. Это может понадобиться, например, чтобы тестируемый модуль мог обращаться к самой тестовой среде или к библиотекам, связанным с ней. Для этого как во время компиляции, так и во время выполнения можно использовать параметр --add-reads. Его синтаксис:

--add-reads <source-module>=<target-module>

где <source-module> и <target-module> — имена модулей.

Параметр --add-reads можно указывать несколько раз. Каждое его использование добавляет ребро читаемости от исходного модуля к целевому. По сути, это форма предложения requires в объявлении модуля для командной строки или вызов неограниченной формы метода Module::addReads. В результате код в исходном модуле сможет обращаться к типам в пакете целевого модуля как во время компиляции, так и во время выполнения, если этот пакет экспортирован предложением exports в объявлении исходного модуля, вызовом метода Module::addExports или использованием параметра --add-exports (описан ниже). Кроме того, такой код сможет обращаться к типам в пакете целевого модуля во время выполнения, если этот модуль объявлен открытым или если этот пакет открыт предложением opens в объявлении исходного модуля, вызовом метода Module::addOpens или использованием параметра --add-opens (также описан ниже).

Если, например, тестовая среда внедряет тестовый класс белого ящика в модуль java.management и этот класс расширяет экспортированный вспомогательный класс в (гипотетическом) модуле testng, то нужный ему доступ можно предоставить параметром

--add-reads java.management=testng

В особом случае, если <target-module> — это ALL-UNNAMED, рёбра читаемости добавляются от исходного модуля ко всем существующим и будущим безымянным модулям, включая модуль, соответствующий пути классов. Так код в модулях можно тестировать с помощью тестовых фреймворков, которые сами ещё не переведены в модульную форму.

Нарушение инкапсуляции

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

--add-exports <source-module>/<package>=<target-module>(,<target-module>)*

где <source-module> и <target-module> — имена модулей, а <package> — имя пакета.

Параметр --add-exports можно указывать несколько раз, но не более одного раза для каждого сочетания исходного модуля и имени пакета. Каждое его использование добавляет квалифицированный экспорт указанного пакета из исходного модуля в целевой модуль. По сути, это форма предложения exports в объявлении модуля для командной строки или вызов неограниченной формы метода Module::addExports. В результате код в целевом модуле сможет обращаться к открытым (public) типам в указанном пакете исходного модуля, если целевой модуль читает исходный модуль — либо благодаря предложению requires в своём объявлении модуля, либо вызову метода Module::addReads, либо использованию параметра --add-reads.

Если, например, модуль jmx.wbtest содержит тест белого ящика для неэкспортированного пакета com.sun.jmx.remote.internal модуля java.management, то нужный ему доступ можно предоставить параметром

--add-exports java.management/com.sun.jmx.remote.internal=jmx.wbtest

В особом случае, если <target-module> — это ALL-UNNAMED, исходный пакет экспортируется во все безымянные модули, независимо от того, существуют ли они изначально или создаются позже. Таким образом, доступ к пакету sun.management модуля java.management можно предоставить всему коду на пути классов параметром

--add-exports java.management/sun.management=ALL-UNNAMED

Параметр --add-exports открывает доступ к открытым (public) типам указанного пакета. Иногда нужно пойти дальше и открыть доступ ко всем непубличным элементам через метод setAccessible core reflection API. Для этого во время выполнения можно использовать параметр --add-opens. Его синтаксис такой же, как у параметра --add-exports:

--add-opens <source-module>/<package>=<target-module>(,<target-module>)*

где <source-module> и <target-module> — имена модулей, а <package> — имя пакета.

Параметр --add-opens можно указывать несколько раз, но не более одного раза для каждого сочетания исходного модуля и имени пакета. Каждое его использование добавляет квалифицированное открытие указанного пакета из исходного модуля для целевого модуля. По сути, это форма предложения opens в объявлении модуля для командной строки или вызов неограниченной формы метода Module::addOpens. В результате код в целевом модуле сможет с помощью core reflection API обращаться ко всем типам, открытым (public) и остальным, в указанном пакете исходного модуля, при условии что целевой модуль читает исходный модуль.

Во время компиляции открытые пакеты неотличимы от неэкспортированных, поэтому на этом этапе параметр --add-opens использовать нельзя.

Параметры --add-exports и --add-opens нужно использовать с большой осторожностью. С их помощью вы можете получить доступ к внутреннему API библиотечного модуля или даже самого JDK, но делаете это на свой страх и риск: если этот внутренний API изменится или будет удалён, ваша библиотека или приложение перестанут работать.

Изменение содержимого модулей

При тестировании и отладке иногда полезно заменить отдельные class-файлы или ресурсы определённых модулей альтернативными или экспериментальными версиями либо добавить совершенно новые class-файлы, ресурсы и даже пакеты. Это можно сделать с помощью параметра --patch-module как во время компиляции, так и во время выполнения. Его синтаксис:

--patch-module <module>=<file>(<pathsep><file>)*

где <module> — имя модуля, <file> — путь к определению модуля в файловой системе, а <pathsep> — символ-разделитель путей платформы.

Параметр --patch-module можно указывать несколько раз, но не более одного раза для каждого имени модуля. Каждое его использование меняет то, как система модулей ищет тип в указанном модуле. Прежде чем проверить сам модуль, входящий в систему или определённый на пути модулей, она сначала по порядку проверяет каждое определение модуля, переданное в параметре. Путь патча задаёт последовательность определений модулей, но не является путём модулей, поскольку у него «протекающая» семантика, похожая на семантику пути классов. Это позволяет, например, тестовой среде внедрять несколько тестов в один и тот же пакет, не копируя все тесты в один каталог.

С помощью параметра --patch-module нельзя заменить файлы module-info.class. Если файл module-info.class найден в определении модуля на пути патча, выдаётся предупреждение и файл игнорируется.

Если пакет, найденный в определении модуля на пути патча, ещё не экспортирован или не открыт этим модулем, то он по-прежнему не будет экспортирован или открыт. Его можно явно экспортировать или открыть через reflection API либо параметрами --add-exports или --add-opens.

Параметр --patch-module заменяет параметр -Xbootclasspath:/p, который удалён (см. ниже).

Параметр --patch-module предназначен только для тестирования и отладки. Использовать его в рабочих окружениях настоятельно не рекомендуется.

Время компиляции

Компилятор javac реализует описанные выше параметры, применимые во время компиляции: --module-source-path, --upgrade-module-path, --system, --module-path, --add-modules, --limit-modules, --add-reads, --add-exports и --patch-module.

Компилятор работает в одном из трёх режимов, в каждом из которых есть дополнительные параметры.

  • Режим совместимости (legacy mode) включается, когда окружение компиляции, заданное параметрами -source, -target и --release, не выше 8. Ни один из описанных выше модульных параметров использовать нельзя.

В режиме совместимости компилятор ведёт себя по сути так же, как в JDK 8.

  • Одномодульный режим включается, когда окружение компиляции — 9 или выше и параметр --module-source-path не используется. Остальные описанные выше модульные параметры использовать можно; существующие параметры -bootclasspath, -Xbootclasspath, -extdirs, -endorseddirs и -XXuserPathsFirst использовать нельзя.

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

$ javac -d classes -classpath classes -sourcepath src Foo.java

Если дескриптор модуля в виде файла module-info.java или module-info.class указан в командной строке или найден на пути исходного кода или на пути классов, то исходные файлы компилируются как члены модуля, названного в этом дескрипторе, и этот модуль будет единственным корневым модулем. Иначе, если указан параметр --module <module>, исходные файлы компилируются как члены <module>, который будет корневым модулем. Иначе исходные файлы компилируются как члены безымянного модуля, а корневые модули вычисляются, как описано выше.

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

  • Многомодульный режим включается, когда окружение компиляции — 9 или выше и используется параметр --module-source-path. Также необходимо использовать существующий параметр -d, задающий выходной каталог; остальные описанные выше модульные параметры использовать можно; существующие параметры -bootclasspath, -Xbootclasspath, -extdirs, -endorseddirs и -XXuserPathsFirst использовать нельзя.

Многомодульный режим служит для компиляции одного или нескольких модулей, исходный код которых размещён в каталогах развёрнутых модулей на пути исходного кода модулей. В этом режиме принадлежность типа модулю определяется положением его исходного файла в пути исходного кода модулей, поэтому каждый исходный файл, указанный в командной строке, должен находиться внутри одного из элементов этого пути. Множество корневых модулей — это множество модулей, для которых указан хотя бы один исходный файл.

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

В больших системах исходный код отдельного модуля может быть разнесён по нескольким разным каталогам. В самом JDK, например, исходные файлы модуля могут находиться в любом из каталогов src/<module>/share/classes, src/<module>/<os>/classes или build/gensrc/<module>, где <os> — имя целевой операционной системы. Чтобы выразить это в пути исходного кода модулей и при этом сохранить идентичность модулей, мы разрешаем в каждом элементе такого пути использовать фигурные скобки ({ и }) для списков альтернатив через запятую и одиночную звёздочку (*) для обозначения имени модуля. Тогда путь исходного кода модулей для JDK можно записать как

{src/*/{share,<os>}/classes,build/gensrc/*}

В обоих модульных режимах компилятор по умолчанию выдаёт различные предупреждения, связанные с системой модулей; их можно отключить параметром -Xlint:-module. Более точно управлять этими предупреждениями можно с помощью ключей exports, opens, requires-automatic и requires-transitive-automatic параметра -Xlint.

Новый параметр --module-version <version> можно использовать, чтобы задать строки версий компилируемых модулей.

Атрибуты class-файлов

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

ModuleTarget_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 os_arch_index; // index to a CONSTANT_utf8_info structure
}

Строка UTF-8 в пуле констант по индексу os_arch_index имеет формат <os>-<arch>, где <os> обычно одно из значений linux, macos, solaris или windows, а <arch> обычно одно из значений x86, amd64, sparcv9, arm или aarch64.

Упаковка: модульные JAR-файлы

Инструмент jar можно без изменений использовать для создания модульных JAR-файлов, поскольку модульный JAR-файл — это просто JAR-файл с файлом module-info.class в корневом каталоге.

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

  • --main-class=<class-name>, или сокращённо -e <class-name>, записывает <class-name> в файл module-info.class как класс, содержащий точку входа public static void main модуля. (Это не новый параметр: он уже записывает главный класс в манифест JAR-файла.)

  • --module-version=<version> записывает <version> в файл module-info.class как строку версии модуля.

  • --hash-modules=<pattern> записывает в файл module-info.class хэши содержимого тех модулей из определённого набора наблюдаемых модулей, которые зависят от данного модуля, чтобы позже использовать их при проверке зависимостей. Хэши записываются только для модулей, имена которых соответствуют регулярному выражению <pattern>. Если используется этот параметр, то нужно также использовать параметр ---module-path, или сокращённо -p, чтобы задать набор наблюдаемых модулей, по которому вычисляются модули, зависящие от данного модуля.

  • --describe-module, или сокращённо -d, выводит дескриптор модуля указанного JAR-файла, если он есть.

Параметр --help инструмента jar позволяет вывести полную сводку его параметров командной строки.

Определены два новых атрибута манифеста JAR-файла, специфичных для JDK, которые соответствуют параметрам командной строки --add-exports и --add-opens:

  • Add-Exports: <module>/<package>( <module>/<package>)*
  • Add-Opens: <module>/<package>( <module>/<package>)*

Значение каждого атрибута — это список пар «имя модуля/имя пакета», разделённых косой чертой, а пары разделены пробелами. Пара <module>/<package> в значении атрибута Add-Exports означает то же самое, что параметр командной строки --add-exports <module>/<package>=ALL-UNNAMED. Пара <module>/<package> в значении атрибута Add-Opens означает то же самое, что параметр командной строки --add-opens <module>/<package>=ALL-UNNAMED.

Каждый атрибут может встречаться не более одного раза, в основном разделе файла MANIFEST.MF. Одну и ту же пару можно указать несколько раз. Если указанный модуль не был разрешён или указанный пакет не существует, соответствующая пара игнорируется. Эти атрибуты интерпретируются только в главном исполняемом JAR-файле приложения, т. е. в JAR-файле, указанном в параметре -jar средства запуска среды выполнения Java; во всех остальных JAR-файлах они игнорируются.

Упаковка: файлы JMOD

Новый формат JMOD выходит за рамки JAR-файлов: он позволяет включать нативный код, файлы конфигурации и другие виды данных, которые плохо помещаются в JAR-файлы или не помещаются в них вовсе. Файлы JMOD используются для упаковки модулей самого JDK; при желании разработчики также могут использовать их для упаковки своих модулей.

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

Для создания, изменения и просмотра файлов JMOD предназначен новый инструмент командной строки jmod. Его общий синтаксис:

$ jmod (create|extract|list|describe|hash) <options> <jmod-file>

Для подкоманды create в <options> можно указывать описанные выше для инструмента jar параметры --main-class, --module-version, --hash-modules и ---module-path, а также:

  • --class-path <path> задаёт путь к классам, содержимое которого будет скопировано в итоговый файл JMOD.

  • --cmds <path> задаёт один или несколько каталогов с нативными командами, которые нужно скопировать.

  • --config <path> задаёт один или несколько каталогов с файлами конфигурации, которые нужно скопировать.

  • --exclude <pattern-list> задаёт исключаемые файлы, где <pattern-list> — список шаблонов вида <glob-pattern>, glob:<glob-pattern> или regex:<regex-pattern>, разделённых запятыми.

  • --header-files <path> задаёт один или несколько каталогов с заголовочными файлами C и C++, которые нужно скопировать.

  • --legal-notices <path> задаёт один или несколько каталогов с юридическими уведомлениями, которые нужно скопировать.

  • --libs <path> задаёт один или несколько каталогов с нативными библиотеками, которые нужно скопировать.

  • --man-pages <path> задаёт один или несколько каталогов со страницами руководства (man), которые нужно скопировать.

  • --target-platform <os>-<arch> задаёт целевые операционную систему и архитектуру, которые записываются в атрибут ModuleTarget файла module-info.class.

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

Подкоманда list выводит список содержимого указанного файла JMOD; подкоманда describe выводит дескриптор модуля указанного файла JMOD в том же формате, что и параметры --describe-module команд jar и java. Эти подкоманды не принимают параметров.

Подкоманда hash позволяет вычислить хэши для существующего набора файлов JMOD. Для неё обязательны оба параметра: --module-path и --hash-modules.

Параметр --help инструмента jmod позволяет вывести полную сводку его параметров командной строки.

Инструмент компоновки командной строки jlink подробно описан в JEP 282. В общих чертах его синтаксис таков:

$ jlink <options> ---module-path <modulepath> --output <path>

где параметр ---module-path задаёт набор наблюдаемых модулей, которые будет рассматривать компоновщик, а параметр --output задаёт путь к каталогу, в котором окажется итоговый образ среды выполнения. Остальные <options> могут включать описанные выше параметры ---limit-modules и ---add-modules, а также дополнительные параметры, специфичные для компоновщика.

Параметр --help инструмента jlink позволяет вывести полную сводку его параметров командной строки.

Время выполнения

Виртуальная машина HotSpot реализует описанные выше параметры в той мере, в какой они применимы ко времени выполнения: --upgrade-module-path, --module-path, --add-modules, --limit-modules, --add-reads, --add-exports, --add-opens и --patch-module. Эти параметры можно передавать средству запуска командной строки java, а также через JNI invocation API.

Дополнительный параметр, специфичный для этой фазы и поддерживаемый средством запуска:

  • --module <module>, или сокращённо -m <module>, задаёт главный модуль модульного приложения. Он будет корневым модулем по умолчанию при построении исходного графа модулей приложения. Если в дескрипторе главного модуля не указан главный класс, можно использовать синтаксис <module>/<class>, где <class> — имя класса, содержащего точку входа public static void main приложения.

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

  • --list-modules выводит имена и строки версий наблюдаемых модулей и завершает работу, так же как java --version.

  • --describe-module <module>, или сокращённо -d <module>, выводит дескриптор указанного модуля в том же формате, что и параметр jar -d и подкоманда jmod describe, и завершает работу.

  • --validate-modules проверяет все наблюдаемые модули на конфликты и другие возможные ошибки и завершает работу.

  • --dry-run инициализирует виртуальную машину и загружает главный класс, но не вызывает главный метод; это полезно для проверки конфигурации системы модулей.

  • --show-module-resolution заставляет систему модулей описывать свои действия при построении исходного графа модулей.

  • -Dsun.reflect.debugModuleAccessChecks выводит дамп потоков всякий раз, когда проверка доступа в API java.lang.reflect завершается неудачей с IllegalAccessException или InaccessibleObjectException. Это полезно при отладке, когда исходная причина неудачи скрыта, потому что исключение перехватывается и не выбрасывается повторно.

  • -Xlog:module+[load|unload][=[debug|trace]] заставляет VM записывать в журнал отладочные или трассировочные сообщения по мере того, как модули определяются и изменяются в графе модулей времени выполнения. Во время запуска эти параметры выводят очень много информации.

  • -verbose:module — сокращённая запись для -Xlog:module+load -Xlog:module+unload.

  • -Xlog:init=debug выводит трассировку стека, если инициализация системы модулей завершается неудачей.

  • --version, --show-version, --help и --help-extra выводят ту же информацию и работают так же, как существующие параметры -version, -show-version, -help и -Xhelp соответственно, но пишут текст справки в стандартный поток вывода, а не в стандартный поток ошибок.

Трассировки стека, формируемые для исключений во время выполнения, теперь включают имена и строки версий соответствующих модулей, если они есть. Строки с подробностями у таких исключений, как ClassCastException, IllegalAccessException и IllegalAccessError, тоже дополнены информацией о модулях.

Существующий параметр -jar улучшен: если файл манифеста запускаемого JAR-файла содержит атрибут Launcher-Agent-Class, то JAR-файл запускается одновременно и как приложение, и как агент для этого приложения. Так можно использовать java -jar foo.jar вместо более многословного java -javaagent:foo.jar -jar foo.jar.

Ослабленная строгая инкапсуляция

В этом выпуске строгая инкапсуляция некоторых пакетов JDK по умолчанию ослаблена, как это допускает спецификация платформы Java SE 9. Это ослабление управляется во время выполнения новым параметром средства запуска --illegal-access, который работает следующим образом:

  • --illegal-access=permit открывает каждый пакет каждого модуля образа среды выполнения для кода во всех безымянных модулях, т. е. для кода на пути к классам, если этот пакет существовал в JDK 8. Это разрешает как статический доступ, т. е. из скомпилированного байт-кода, так и глубокий рефлексивный доступ через различные API рефлексии платформы.

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

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

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

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

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

    В одном из будущих выпусков этот режим станет режимом по умолчанию.

Когда deny станет режимом недопустимого доступа по умолчанию, permit, скорее всего, будет поддерживаться ещё как минимум один выпуск, чтобы разработчики могли продолжить миграцию своего кода. Режимы permit, warn и debug со временем будут удалены, как и сам параметр --illegal-access. (Для совместимости со скриптами запуска неподдерживаемые режимы, скорее всего, будут просто игнорироваться после вывода соответствующего предупреждения.)

Режим по умолчанию, --illegal-access=permit, нужен для того, чтобы вы узнали, что на пути к классам у вас есть код, который хотя бы раз обращается через рефлексию к какому-либо внутреннему API JDK. Чтобы подготовиться к будущему, можно использовать режимы warn или debug и узнать обо всех таких обращениях. Для каждой библиотеки или фреймворка на пути к классам, которым нужен недопустимый доступ, у вас есть два варианта:

  • Если сопровождающие компонента уже выпустили новую, исправленную версию, которая больше не использует внутренние API JDK, можно рассмотреть переход на эту версию.

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

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

Чтобы убедиться, что ваше приложение готово к будущему, запустите его с --illegal-access=deny и всеми необходимыми параметрами --add-opens. Оставшиеся ошибки недопустимого доступа, скорее всего, вызваны статическими ссылками из скомпилированного кода на внутренние API JDK. Их можно выявить, запустив инструмент jdeps с параметром --jdk-internals. (Среда выполнения не выводит предупреждений о недопустимых операциях статического доступа, потому что для этого потребовались бы глубокие изменения в VM и снизилась бы производительность.)

Предупреждение, выводимое при обнаружении недопустимой операции рефлексивного доступа, имеет следующий вид:

WARNING: Illegal reflective access by $PERPETRATOR to $VICTIM

где:

  • $PERPETRATOR — полное имя типа, содержащего код, который вызвал данную рефлексивную операцию, плюс источник кода (т. е. путь к JAR-файлу), если он доступен, и

  • $VICTIM — строка, описывающая член, к которому выполняется доступ, включая полное имя объемлющего типа

В режиме по умолчанию, --illegal-access=permit, будет выдано не более одного из этих предупреждений, и оно будет сопровождаться дополнительным поясняющим текстом. Вот пример, полученный при запуске Jython:

$ java -jar jython-standalone-2.7.0.jar
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by jnr.posix.JavaLibCHelper (file:/tmp/jython-standalone-2.7.0.jar) to method sun.nio.ch.SelChImpl.getFD()
WARNING: Please consider reporting this to the maintainers of jnr.posix.JavaLibCHelper
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
Jython 2.7.0 (default:9987c746f838, Apr 29 2015, 02:25:11) 
[OpenJDK 64-Bit Server VM (Oracle Corporation)] on java9
Type "help", "copyright", "credits" or "license" for more information.
>>> ^D

Среда выполнения старается по возможности подавлять повторяющиеся предупреждения для одних и тех же $PERPETRATOR и $VICTIM.

Развёрнутый пример

Предположим, у нас есть модуль приложения com.foo.bar, который зависит от библиотечного модуля com.foo.baz. Если исходный код обоих модулей находится в каталоге пути модулей src:

src/com.foo.bar/module-info.java
src/com.foo.bar/com/foo/bar/Main.java
src/com.foo.baz/module-info.java
src/com.foo.baz/com/foo/baz/BazGenerator.java

то мы можем скомпилировать их вместе:

$ javac --module-source-path src -d mods $(find src -name '*.java')

Выходной каталог mods — это каталог пути модулей, содержащий скомпилированные определения двух модулей в распакованном виде:

mods/com.foo.bar/module-info.class
mods/com.foo.bar/com/foo/bar/Main.class
mods/com.foo.baz/module-info.class
mods/com.foo.baz/com/foo/baz/BazGenerator.class

Если класс com.foo.bar.Main содержит точку входа приложения, мы можем запустить эти модули как есть:

$ java -p mods -m com.foo.bar/com.foo.bar.Main

Либо мы можем упаковать их в модульные JAR-файлы:

$ jar --create -f mlib/com.foo.bar-1.0.jar \
      --main-class com.foo.bar.Main --module-version 1.0 \
      -C mods/com.foo.bar .
$ jar --create -f mlib/com.foo.baz-1.0.jar \
      --module-version 1.0 -C mods/com.foo.baz .

Каталог mlib — это каталог пути модулей, содержащий упакованные скомпилированные определения двух модулей:

$ ls -l mlib
-rw-r--r-- 1501 Sep  6 12:23 com.foo.bar-1.0.jar
-rw-r--r-- 1376 Sep  6 12:23 com.foo.baz-1.0.jar

Теперь мы можем запустить упакованные модули напрямую:

$ java -p mlib -m com.foo.bar

Улучшения jtreg

Тестовая среда jtreg поддерживает новый декларативный тег @modules, выражающий зависимости теста от модулей тестируемой системы. Он принимает набор аргументов, разделённых пробелами, каждый из которых может иметь вид

  • <module>, где <module> — имя модуля: указанный модуль должен присутствовать;

  • <module>/<package>: указанный модуль должен присутствовать, а указанный пакет должен экспортироваться в модуль теста; или

  • <module>/<package>:<flag>: указанный модуль должен присутствовать и, если флаг равен open, указанный пакет должен быть открыт для модуля теста, а если флаг равен +open, указанный пакет должен быть и экспортирован в модуль теста, и открыт для него.

Набор аргументов @modules по умолчанию, который будет использоваться для всех тестов в иерархии каталогов, не содержащих такого тега, можно задать как значение свойства modules в файле TEST.ROOT или в любом файле TEST.properties.

Существующий тег @compile принимает новый параметр /module=<module>. Он вызывает javac с описанным выше параметром --module <module>, чтобы скомпилировать указанные классы как члены указанного модуля.

Загрузчики классов

API платформы Java SE исторически определял два загрузчика классов: загрузчик классов начальной загрузки (bootstrap class loader), который загружает классы из пути классов начальной загрузки, и системный загрузчик классов, который является родителем по умолчанию для делегирования у новых загрузчиков классов и, как правило, загрузчиком классов, с помощью которого загружается и запускается приложение. Спецификация не предписывает ни конкретных типов этих загрузчиков классов, ни точной связи делегирования между ними.

Начиная с выпуска 1.2, в JDK реализована трёхуровневая иерархия загрузчиков классов, где каждый загрузчик делегирует следующему:

  • Загрузчик классов приложения, экземпляр java.net.URLClassLoader, загружает классы из пути классов и устанавливается в качестве системного загрузчика классов, если другой системный загрузчик не указан через системное свойство java.system.class.loader.

  • Загрузчик классов расширений, тоже экземпляр URLClassLoader, загружает классы, доступные через механизм расширений, а также некоторые ресурсы и поставщиков сервисов, встроенных в JDK. (Этот загрузчик явно не упоминается в спецификации API платформы Java SE.)

  • Загрузчик классов начальной загрузки, который реализован целиком внутри виртуальной машины и представлен значением null в API ClassLoader, загружает классы из пути классов начальной загрузки.

JDK 9 сохраняет эту трёхуровневую иерархию ради совместимости, но для реализации системы модулей вносит следующие изменения:

  • Загрузчик классов приложения больше не является экземпляром URLClassLoader, теперь это экземпляр внутреннего класса. Он является загрузчиком по умолчанию для именованных модулей, которые не относятся ни к модулям Java SE, ни к модулям JDK.

  • Загрузчик классов расширений больше не является экземпляром URLClassLoader, теперь это экземпляр внутреннего класса. Он больше не загружает классы через механизм расширений, который был удалён в JEP 220. Однако он определяет отдельные модули Java SE и JDK, о которых подробнее ниже. В новой роли этот загрузчик называется платформенным загрузчиком классов, он доступен через новый метод ClassLoader::getPlatformClassLoader, и его наличие будет требоваться спецификацией API платформы Java SE.

  • Загрузчик классов начальной загрузки реализован и в библиотечном коде, и внутри виртуальной машины, но ради совместимости он по-прежнему представлен значением null в API ClassLoader. Он определяет основные модули Java SE и JDK.

Платформенный загрузчик классов сохранён не только ради совместимости, но и для повышения безопасности. Типам, загруженным загрузчиком классов начальной загрузки, неявно предоставляются все разрешения безопасности (AllPermission), но многим из этих типов на самом деле не нужны все разрешения. Мы лишили привилегий модули, которым не нужны все разрешения: они определяются в платформенном загрузчике, а не в загрузчике классов начальной загрузки, и в файле политики безопасности по умолчанию им предоставляются только те разрешения, которые им действительно нужны. В платформенном загрузчике классов определены следующие модули Java SE и JDK:

java.activation*            jdk.accessibility
java.compiler*              jdk.charsets
java.corba*                 jdk.crypto.cryptoki
java.scripting              jdk.crypto.ec
java.se                     jdk.dynalink
java.se.ee                  jdk.incubator.httpclient
java.security.jgss          jdk.internal.vm.compiler*
java.smartcardio            jdk.jsobject
java.sql                    jdk.localedata
java.sql.rowset             jdk.naming.dns
java.transaction*           jdk.scripting.nashorn
java.xml.bind*              jdk.security.auth
java.xml.crypto             jdk.security.jgss
java.xml.ws*                jdk.xml.dom
java.xml.ws.annotation*     jdk.zipfs

(Звёздочка, '*', в этих списках обозначает обновляемый модуль.)

Модули JDK, которые предоставляют инструменты или экспортируют API инструментов, определены в загрузчике классов приложения:

jdk.aot                     jdk.jdeps
jdk.attach                  jdk.jdi
jdk.compiler                jdk.jdwp.agent
jdk.editpad                 jdk.jlink
jdk.hotspot.agent           jdk.jshell
jdk.internal.ed             jdk.jstatd
jdk.internal.jvmstat        jdk.pack
jdk.internal.le             jdk.policytool
jdk.internal.opt            jdk.rmic
jdk.jartool                 jdk.scripting.nashorn.shell
jdk.javadoc                 jdk.xml.bind*
jdk.jcmd                    jdk.xml.ws*
jdk.jconsole

Все остальные модули Java SE и JDK определены в загрузчике классов начальной загрузки:

java.base                   java.security.sasl
java.datatransfer           java.xml
java.desktop                jdk.httpserver
java.instrument             jdk.internal.vm.ci
java.logging                jdk.management
java.management             jdk.management.agent
java.management.rmi         jdk.naming.rmi
java.naming                 jdk.net
java.prefs                  jdk.sctp
java.rmi                    jdk.unsupported

Три встроенных загрузчика классов совместно загружают классы следующим образом:

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

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

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

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

Удалено: параметры пути классов начальной загрузки

В прежних выпусках параметр -Xbootclasspath позволяет переопределить путь классов начальной загрузки по умолчанию, а параметр -Xbootclasspath/p позволяет добавить набор файлов и каталогов в начало пути по умолчанию. Вычисленное значение этого пути сообщается через специфичное для JDK системное свойство sun.boot.class.path.

С появлением системы модулей путь классов начальной загрузки по умолчанию пуст, поскольку классы начальной загрузки загружаются из соответствующих модулей. Компилятор javac поддерживает параметр -Xbootclasspath только в режиме совместимости (legacy mode), программа запуска java больше не поддерживает ни один из этих параметров, а системное свойство sun.boot.class.path удалено.

Параметр компилятора --system позволяет указать другой источник системных модулей, как описано выше, а его параметр -release позволяет указать другую версию платформы, как описано в JEP 247 (Compile for Older Platform Versions). Во время выполнения упомянутый выше параметр --patch-module позволяет внедрить содержимое в модули исходного графа модулей.

Связанный параметр -Xbootclasspath/a позволяет добавлять файлы и каталоги в конец пути классов начальной загрузки по умолчанию. Этот параметр и связанный с ним API в пакете java.lang.instrument иногда используются агентами инструментирования, поэтому ради совместимости он по-прежнему поддерживается во время выполнения. Его значение, если оно задано, сообщается через специфичное для JDK системное свойство jdk.boot.class.path.append. Этот параметр можно передать программе запуска командной строки java, а также API вызова JNI.

Тестирование

Появление системы модулей затронуло многие существующие тесты. В JDK 9 описанный выше тег @modules был по мере необходимости добавлен в модульные и регрессионные тесты, а тесты, которые использовали параметр -Xbootclasspath/p или предполагали, что системный загрузчик классов является URLClassLoader, были обновлены.

Разумеется, для самой системы модулей существует обширный набор модульных тестов. В дереве исходного кода JDK 9 тесты времени выполнения находятся в каталоге test/jdk/modules репозитория jdk и в каталоге runtime/modules репозитория hotspot; тесты времени компиляции находятся в каталоге tools/javac/modules репозитория langtools.

Сборки Early Access (ранний доступ) с описанными здесь изменениями были доступны на протяжении всей разработки системы модулей. Участникам Java-сообщества настоятельно рекомендовалось тестировать свои инструменты, библиотеки и приложения на этих сборках, чтобы помочь выявить проблемы совместимости.

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

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

Изменения, вызванные прежде всего появлением Java Platform Module System (JSR 376), включают:

  • Модификатор public, применённый к элементу API, больше не гарантирует, что этот элемент будет доступен везде. Доступность теперь зависит также от того, экспортирует или открывает ли пакет, содержащий этот элемент, определяющий его модуль, и доступен ли этот модуль для чтения модулю с кодом, который пытается к нему обратиться. Например, код следующего вида может работать некорректно:

    Class<?> c = Class.forName(...);
    if (Modifier.isPublic(c.getModifiers()) {
        // Assume that c is accessible
    }
  • Если пакет определён и в именованном модуле, и в пути классов, пакет в пути классов будет проигнорирован. Поэтому путь классов больше нельзя использовать для дополнения пакетов, встроенных в среду. Например, пакет javax.transaction определён модулем java.transaction, поэтому типы из этого пакета в пути классов искаться не будут. Это ограничение важно, чтобы не допустить разделения пакетов между загрузчиками классов и между модулями. Во время компиляции и во время выполнения для обновления модулей, встроенных в среду, можно использовать путь обновляемых модулей. Для других разовых исправлений можно использовать параметр --patch-module.

  • Методы ClassLoader::getResource* больше нельзя использовать для поиска внутренних ресурсов JDK, кроме файлов классов. Приватные для модуля ресурсы, не являющиеся классами, можно прочитать с помощью методов Class::getResource*, метода Module::getResourceAsStream или через URL-схему jrt: и файловую систему, определённые в JEP 220.

  • Метод java.lang.reflect.AccessibleObject::setAccessible нельзя использовать для получения доступа к публичным членам пакетов, которые не экспортированы и не открыты определяющими их модулями, или к непубличным членам пакетов, которые не открыты определяющими их модулями; в обоих случаях будет выброшено исключение InaccessibleObjectException. Если библиотеке-фреймворку, например сериализатору, нужен доступ к таким членам во время выполнения, соответствующие пакеты должны быть открыты модулю фреймворка: для этого содержащий их модуль объявляется открытым, пакет объявляется открытым или пакет открывается с помощью параметра командной строки --add-opens.

  • Агенты JVM TI больше не могут инструментировать Java-код, который выполняется на ранней стадии запуска среды выполнения. В частности, событие ClassFileLoadHook больше не отправляется в начальной (primordial) фазе. Событие VMStart, которое сигнализирует о начале фазы start, отправляется только после того, как VM инициализирована настолько, что может загружать классы в модулях, отличных от java.base. Агенты, тщательно написанные для обработки событий на ранних этапах инициализации VM, могут добавить две новые возможности (capabilities): can_generate_early_class_hook_events и can_generate_early_vmstart. Подробности можно найти в обновлённом описании события class file load hook и события start.

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

Модули, определяющие API Java EE, или API, интересные прежде всего приложениям Java EE, получили статус Deprecated (устаревший) и будут удалены в одном из будущих выпусков. Для кода в пути классов они по умолчанию не разрешаются:

  • Набор корневых модулей по умолчанию для безымянного модуля в JDK 9 основан на модуле java.se, а не на модуле java.se.ee. Поэтому по умолчанию код в безымянном модуле не будет иметь доступа к API в следующих модулях:

    java.activation
    java.corba
    java.transaction
    java.xml.bind
    java.xml.ws
    java.xml.ws.annotation

    Это намеренный, хотя и болезненный, выбор, продиктованный двумя целями:

    • Избежать ненужных конфликтов с популярными библиотеками, которые определяют типы в некоторых из тех же пакетов. Например, широко используемая jsr305.jar определяет типы аннотаций в пакете javax.annotation, который также определяется модулем java.xml.ws.annotation.

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

    Эти модули по-прежнему входят в JDK 9. Коду в пути классов можно при необходимости предоставить доступ к одному или нескольким из этих модулей с помощью параметра --add-modules.

Поведение некоторых API Java SE во время выполнения изменилось, хотя и так, что их существующие спецификации по-прежнему соблюдаются:

  • Загрузчики классов приложения и платформы больше не являются экземплярами класса java.net.URLClassLoader, как отмечено выше. Существующий код, который вызывает ClassLoader::getSystemClassLoader и не глядя приводит результат к URLClassLoader или делает то же самое с родителем этого загрузчика классов, может работать некорректно.

  • Некоторые типы Java SE лишены привилегий и теперь загружаются загрузчиком классов платформы, а не начальным загрузчиком классов (bootstrap), как отмечено выше. Существующие пользовательские загрузчики классов, которые делегируют напрямую начальному загрузчику классов, могут работать некорректно; их следует изменить так, чтобы они делегировали загрузчику классов платформы, который легко получить с помощью нового метода ClassLoader::getPlatformClassLoader.

  • У экземпляров java.lang.Package, созданных встроенными загрузчиками классов для пакетов в именованных модулях, нет версий спецификации и реализации. В предыдущих выпусках эта информация читалась из манифеста rt.jar. Существующий код, который ожидает, что методы Package::getSpecification* или Package::getImplementation* всегда возвращают значения, отличные от null, может работать некорректно.

Есть несколько изменений API Java SE, несовместимых на уровне исходного кода:

  • Пакет java.lang включает два новых класса верхнего уровня, Module и ModuleLayer. Пакет java.lang неявно импортируется по требованию (т. е. import java.lang.*). Если код в существующем исходном файле импортирует по требованию какой-либо другой пакет, этот пакет объявляет тип Module или ModuleLayer, а существующий код ссылается на этот тип, то файл не скомпилируется без изменений.

  • Интерфейс java.lang.instrument.Instrumentation объявляет два новых абстрактных метода, redefineModule и isModifiableModule. Этот интерфейс не предназначен для реализации вне модуля java.instrument. Если существуют внешние реализации, они не скомпилируются на JDK 9 без изменений.

  • Метод transform с пятью параметрами, объявленный в интерфейсе java.lang.instrument.ClassFileTransformer, теперь является методом по умолчанию. Кроме того, интерфейс теперь объявляет новый метод transform, который делает соответствующий объект java.lang.reflect.Module доступным для трансформера при инструментировании классов во время загрузки. Существующий скомпилированный код продолжит работать, но существующий исходный код, который использует существующий метод transform с пятью параметрами как функциональный интерфейс, больше не будет компилироваться.

Наконец, изменения, вызванные переработкой API и инструментов, специфичных для JDK, включают:

  • Большинство внутренних API JDK по умолчанию недоступны во время компиляции, как описано в JEP 260. Существующий код, который в предыдущих выпусках компилировался с этими API с предупреждениями, больше не будет компилироваться. Обойти это можно, нарушив инкапсуляцию с помощью параметра --add-exports, определённого выше.

  • Отдельные критически важные внутренние API из пакетов sun.misc и sun.reflect перенесены в модуль jdk.unsupported, как описано в JEP 260. Некритичные внутренние API из этих пакетов, например sun.misc.BASE64{De,En}coder, либо перенесены, либо удалены.

  • Если присутствует менеджер безопасности, то для доступа к внутренним ресурсам JDK через методы ClassLoader::getResource* или Class::getResource* требуется разрешение времени выполнения accessSystemModules; в предыдущих выпусках требовалось разрешение на чтение файла ${java.home}/lib/rt.jar.

  • Параметры -Xbootclasspath и -Xbootclasspath/p удалены, как отмечено выше. Во время компиляции можно использовать новый параметр --release, чтобы указать другую версию платформы (см. JEP 247). Во время выполнения можно использовать новый параметр --patch-module, описанный выше, чтобы внедрять содержимое в системные модули.

  • Специфичное для JDK системное свойство sun.boot.class.path удалено, поскольку путь начальных классов (bootstrap class path) по умолчанию пуст. Существующий код, который использует это свойство, может работать некорректно.

  • Специфичная для JDK аннотация @jdk.Exported, введённая в JEP 179, удалена, поскольку передаваемая ею информация теперь записывается в объявлениях exports дескрипторов модулей. Мы не видели свидетельств того, что эту аннотацию используют инструменты вне JDK.

  • Файлы ресурсов META-INF/services, которые раньше находились в rt.jar и других внутренних артефактах, отсутствуют в соответствующих системных модулях, поскольку поставщики служб и зависимости теперь объявляются в дескрипторах модулей. Существующий код, который ищет такие файлы, может работать некорректно.

  • Специфичное для JDK системное свойство file.encoding можно, как и раньше, задать в командной строке с помощью параметра -D, но в нём должна быть указана кодировка, определённая в базовом модуле. Если указана любая другая кодировка, система времени выполнения не запустится. Существующие сценарии запуска, в которых указаны такие кодировки, могут работать некорректно.

  • API com.sun.tools.attach больше нельзя по умолчанию использовать для подключения агента к текущему процессу или к предку текущего процесса. Такие операции подключения можно разрешить, задав системное свойство jdk.attach.allowAttachSelf в командной строке.

  • Динамическая загрузка агентов JVM TI будет по умолчанию отключена в одном из будущих выпусков. Чтобы подготовиться к этому изменению, мы рекомендуем приложениям, которые допускают динамические агенты, начать использовать параметр -XX:+EnableDynamicAgentLoading, чтобы явно разрешить такую загрузку. Параметр -XX:-EnableDynamicAgentLoading отключает динамическую загрузку агентов.

Зависимости

JEP 200 (The Modular JDK) изначально, в качестве временной меры, определял модули, входящие в JDK, в XML-документе. Этот JEP перенёс эти определения в полноценные дескрипторы модулей, т. е. в файлы module-info.java и module-info.class, а файл modules.xml в корневом репозитории исходного кода был удалён.

Первоначальная реализация JEP 220 (Modular Run-Time Images) в JDK 9 использовала специальный инструмент времени сборки для создания образов JRE и JDK. Этот JEP заменил этот инструмент инструментом jlink.

Модульные JAR-файлы могут также быть Multi-Release JAR-файлами согласно JEP 238.