Модули
При компиляции указывается путь к зависимостям (модулям) посредством флага -p
или --module-path
:
1 | javac -p modules-directory/ext -d output src/my/package/*.java src/module-info.java |
Запуск класса модуля
Для этого используется путь к модулям, а так же указание - какой класс, в каком модуле запускать:
1 | java --module-path modules-directory/ext --module some.module/some.class.InModule |
Так запустится класс InModule
внутри пакета some.class
в составе модуля some.module
. Для --module
существует короткая форма, -m
(как и для директории с модулями).
1 | java -p modules-directory/ext -m some.module/some.class.InModule |
Установка в jar
Используется стандартная установка:
1 | jar -cvf mods/some.module.first.jar -c modules-directory/ext |
Открытие модуля извне
Для этого используется дополнительная запись в module-info.java
- файле, объявляющем модуль:
1 | module ${module-name} { |
Файл должен располагаться в корне модуля (в корне jar).
Использование стороннего модуля
Чтобы использовать сторонний модуль, необходимо его прописать в спецификации модуля - module-info.java
.
1 | module ${module-name} { |
Module-info
При директиве exports
можно указывать не только пакеты целиком, но и классы выборочно.
Так же, экспорт пакетов можно ограничить для одного или нескольких модулей. Для этого в конец директивы exports
следует добавить наименование тех модулей, котором следует предоставить доступ к экспортируемому пакету (или классу).
Области видимости java
типов и переменных для других модулей будут работать так же, как и для единого проектного кода. Для private
и package-private
, правда, доступ закрыт полностью (в т.ч. для и наследников).
requires transitive
Данное “улучшение” обязательности модуля выражаются в том, что все зависимые модули так же получат transitive
зависимости:
Это позволяет сократить количество лишних упоминаний requires
в спецификациях module-info
.
Описание модуля
Описание можно получить посредством флага --describe-module
(допускается использование не только java
, но и jar
):
1 | java -p mods-dir --describe-module my.module.first |
В нем может содержаться подобная строка:
1 | requires java\.base mandated |
Модуль java.base
- это стандартный набор java
пакетов, который не требует import
классов. Например, String
, Integer
и т.д.
Список доступных модулей
Используется команда java
с флагом --list-modules
:
1 | java.base@11.0.2 |
При этом, если указать директорию с модулями будет выдана инфо и по ним:
1 | java -p mod-dir --list-modules |
Выведены будут все доступные по умолчанию (как в примере ранее), а так же модули, которые расположены в соответствующей директории с модулями. Путь к jar
файлам модулей будет так же указан в описании (чего нельзя сказать про стандартные библиотеки).
Утилита jdeps
Используется для анализа зависимостей модулей. Позволяет просмотреть зависимости модуля. Вывод утилиты может быть, например, таким:
1 | my.package.jar -> java.base |
Данный вывод включает описание тех библиотек, которые используются. Если данная информация избыточна, можно добавить флаг -s
, сокращающий вывод.
Использование модуля jdk.unsupported
лучше избегать, поскольку это может привести к нарушениям работы при переходе на новые версии Java. Чтобы было проще отловить такие случаи, предусмотрен флаг --jdk-internals
:
1 | my.pack.jar -> jdk.unsupported |
Утилита jmod
Позволяет работать с нативным кодом и библиотеками в связке с модулями. Оперирует с JMOD файлами.
Основные директивы:
exports {package}
- открывает доступ к пакету;exports {package} to {module}
- открывает доступ к пакету для модуля;requires {module}
- показывает зависимость модуля от другого модуля;requires transitive {module}
- добавляет сквозную зависимость;uses {interface}
;provides {interface} with {class}
;
Именованный (named) модуль
Сторонний модуль, используемый в приложении или частный модуль, созданный разработчиком. Должен включать module-info.java
.
Должен присутствовать в module-path
(указывается как аргумент java
или jar
команды).
Automatic-модуль
По умолчанию, Java может распознать любой jar
, находящийся в module-path
. При этом, существуют следующие шаблоны именования: {module-name}-{module-version}.jar
. Для {module-version}
допускается использование точки (“.“) в качестве разделителя.
{module-name}
является именем модуля. При этом, любой специальный символ кроме “.“ будет преобразован в “.“. Специальные символы в начале и в конце имени будут удалены. Итак, алгоритм именования следующий:
- Поиск
META-INF/MANIFEST.MF
. Если он найден, выполняется получение из негоAutomatic-Module-Name
. Если нет - выполняются действия согласно алгоритму ниже; - Отбрасывается расширение файла (
.jar
); - Удаляется версия, типа
-1.14.2
,-4.2.0
; - Удаляются все специальные символы с обеих сторон имени модуля;
- Все специальные символы конвертируются в “.“;
1 | commons-abs-1.0.0.jar -> name: commons.abc / version: 1.0.0 |
Неименованный (unnamed) модуль
Расположен в classpath
. Даже если содержит в себе module-info.java
, он будет проигнорирован!
Сравнение различных видов модулей
- Named
- Содержит
module-info.java
; - Помещен в
module-path
; - Экспортируется только в те модули, которые перечислены в
module-info
; - Публичен для библиотек в
classpath
;
- Содержит
- Automatic
- Помещен в
module-path
; - Виден всем модулям;
- Публичен для библиотек в
classpath
;
- Помещен в
- Unnamed
- Помещен в
classpath
; - Виден всем модулям;
- Публичен для библиотек в
classpath
; - Даже если содержит
module-info.java
, будет проигнорирован;
- Помещен в
Стандартные библиотеки и модули
Модульная реализация всегда, для любого модуля включает в себя java.base
. Данный модуль относится к стандартным, поставляемым с любой JDK. Ниже представлены дополнительные стандартные модули, которые требуют подключения:
java.desktop
- пакеты AWT и Swing;java.logging
- пакеты для LoggingAPI;java.sql
- все для работы с JDBC;java.xml
- все для работы с XML;java.*
- другие пакеты, доступные для разработчика и которые могут ему понадобиться для импорта библиотек и классов;jdk.*
- включает библиотеки для более глубокой разработки (например, для кастомизации компилятора, отладки, управления JDK);jdk.net
;jdk.xml.dom
;jdk.httpserver
;jdk.charset
.
Миграция и модульность
Bottom-Up (миграция снизу вверх). Самая простая стратегия миграции:
- Выбор цели, которая содержит минимальное количество зависимостей (в особенности, от нестандартных модулей);
- Добавление
module-info.java
с указаниемexports
на те пакеты, которые следует открыть другим модулям; - Перенос модуля в
module-path
; - Повторение действий 1-3 для всех вышестоящих библиотек (модулей);
- Провести обзор можно ли добавить к
requires
директивуtransitive
. Лишниеrequires
тогда можно убрать.
Top-Down (миграция свеху вниз). Эффективна тогда, когда нет возможности изменить каждый имеющийся модуль (jar
):
- Поместить все
jar
вmodule-path
- Для наиболее зависимых модулей добавлять
module-info.java
туда, где это возможно. Если такой возможности нет - модуль следует просто проигнорировать. - Двигаясь от наиболее зависимых к менее зависимым, выполнять пункты 1-2
Разбиение монолита на модули
Первым делом проводится декомпозиция и определяются связи между пакетами.
В случае возникновения цикличных зависимостей, стандартным решением является выделение дополнительного модуля, который может содержать только интерфейсную часть (если не удастся включить реализацию).
В случае обнаружения цикличных зависимостей, компилятор укажет на данную проблему:
1 | myPackModule/module-info.java:4: error: cyclic dependence involving my.pack.cycle.mod requires my.pack.cycle.mod |
Важно отметить, что импорт пакетов циклично между модулями не запрещен (как и в рамках одного модуля)!
Сервисы
Для создания сервиса в модуле необходимо:
- В
module-info.java
того модуля, в котором предполагается использовать некий сервис, указать директивуuses
с именем сервиса:
1 | // module-info.java |
- В импортируемом модуле создать сервис (обычный POJO, который может быть интерфейсом):
1 | package my.package.service.api; |
- В модуле, который использует сервис, необходимо найти его среди многих:
1 | ServiceLoader<MyService> svc = ServiceLoader.load(MyService.class); |
- В каком-то из модулей добавить реализацию сервиса и указать его в
module-info.java
:
1 | module my.package.service.api.simpleimpl { |
В одном модуле допускается ТОЛЬКО одна реализация сервиса. Если необходимо наличие нескольких реализаций - придется создавать дополнительные модули.
- Все модули с реализацией поместить в
module-path
. Java найдет их самостоятельно.
Директива uses
обязательна в module-info.java
если в коде будет вызов ServiceLoader.load
. Даже если сам модуль имеет в себе реализацию запрашиваемого сервиса. Без этой директивы поиск будет безрезультатным (хотя фактически, сам модуль и содержит реализацию).