Java Basics: Модули

Модули

При компиляции указывается путь к зависимостям (модулям) посредством флага -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
2
3
module ${module-name} {
exports my\.module.package;
}

Файл должен располагаться в корне модуля (в корне jar).

Использование стороннего модуля

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

1
2
3
module ${module-name} {
requires my\.first.module;
}

Module-info

При директиве exports можно указывать не только пакеты целиком, но и классы выборочно.

Так же, экспорт пакетов можно ограничить для одного или нескольких модулей. Для этого в конец директивы exports следует добавить наименование тех модулей, котором следует предоставить доступ к экспортируемому пакету (или классу).

Области видимости java типов и переменных для других модулей будут работать так же, как и для единого проектного кода. Для private и package-private, правда, доступ закрыт полностью (в т.ч. для и наследников).

requires transitive

Данное “улучшение” обязательности модуля выражаются в том, что все зависимые модули так же получат transitive зависимости:

requires 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
2
3
java.base@11.0.2
java.compiler@11.0.2
java.datatransfer@11.0.2

При этом, если указать директорию с модулями будет выдана инфо и по ним:

1
java -p mod-dir --list-modules

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

Утилита jdeps

Используется для анализа зависимостей модулей. Позволяет просмотреть зависимости модуля. Вывод утилиты может быть, например, таким:

1
2
3
4
5
6
7
my.package.jar -> java.base
my.package.jar -> jdk.unsupported
my.package.jar -> java.lang
my.package.jar -> java.time, java.base
my.package.jar -> java.time, java.base
my.package.jar -> java.util, java.base
my.package.jar -> sun.misc, JDK internal API (jdk.unsupported)

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

Использование модуля jdk.unsupported лучше избегать, поскольку это может привести к нарушениям работы при переходе на новые версии Java. Чтобы было проще отловить такие случаи, предусмотрен флаг --jdk-internals:

1
2
3
4
5
my.pack.jar -> jdk.unsupported
my.pack.some.package.UnsafeExample -> sun.misk.Unsafe JDK internal API (jdk.unsupported)
Warning: Commited warnings
JDK Internal API Suggested Replacement
sun.misc.Unsafe See http://...

Утилита 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} является именем модуля. При этом, любой специальный символ кроме “.“ будет преобразован в “.“. Специальные символы в начале и в конце имени будут удалены. Итак, алгоритм именования следующий:

  1. Поиск META-INF/MANIFEST.MF. Если он найден, выполняется получение из него Automatic-Module-Name. Если нет - выполняются действия согласно алгоритму ниже;
  2. Отбрасывается расширение файла (.jar);
  3. Удаляется версия, типа -1.14.2, -4.2.0;
  4. Удаляются все специальные символы с обеих сторон имени модуля;
  5. Все специальные символы конвертируются в “.“;
1
2
commons-abs-1.0.0.jar -> name: commons.abc / version: 1.0.0
mod_$-1.0.2.jar -> name: mod / version: 1.0.2

Неименованный (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 (миграция снизу вверх). Самая простая стратегия миграции:

  1. Выбор цели, которая содержит минимальное количество зависимостей (в особенности, от нестандартных модулей);
  2. Добавление module-info.java с указанием exports на те пакеты, которые следует открыть другим модулям;
  3. Перенос модуля в module-path;
  4. Повторение действий 1-3 для всех вышестоящих библиотек (модулей);
  5. Провести обзор можно ли добавить к requires директиву transitive. Лишние requires тогда можно убрать.

Top-Down (миграция свеху вниз). Эффективна тогда, когда нет возможности изменить каждый имеющийся модуль (jar):

  1. Поместить все jar в module-path
  2. Для наиболее зависимых модулей добавлять module-info.java туда, где это возможно. Если такой возможности нет - модуль следует просто проигнорировать.
  3. Двигаясь от наиболее зависимых к менее зависимым, выполнять пункты 1-2

Разбиение монолита на модули

Первым делом проводится декомпозиция и определяются связи между пакетами.

В случае возникновения цикличных зависимостей, стандартным решением является выделение дополнительного модуля, который может содержать только интерфейсную часть (если не удастся включить реализацию).

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

1
myPackModule/module-info.java:4: error: cyclic dependence involving my.pack.cycle.mod requires my.pack.cycle.mod

Важно отметить, что импорт пакетов циклично между модулями не запрещен (как и в рамках одного модуля)!

Сервисы

Для создания сервиса в модуле необходимо:

  1. В module-info.java того модуля, в котором предполагается использовать некий сервис, указать директиву uses с именем сервиса:
1
2
3
4
5
6
7
// module-info.java
module my.package.executor.run {
exports my\.package.executor.run;
exports my\.package.executor.main;
requires my\.package.service;
uses my\.package.service.api.MyService;
}
  1. В импортируемом модуле создать сервис (обычный POJO, который может быть интерфейсом):
1
2
3
4
package my.package.service.api;
public interface MyService {
void doSomething();
}
  1. В модуле, который использует сервис, необходимо найти его среди многих:
1
2
ServiceLoader<MyService> svc = ServiceLoader.load(MyService.class);
svc.iterator().next().doSomething();
  1. В каком-то из модулей добавить реализацию сервиса и указать его в module-info.java:
1
2
3
4
5
6
7
8
9
10
11
module my.package.service.api.simpleimpl {
requires my\.package.service.api;
provides my\.package.service.api.MyService with my.package.service.api.simpleimpl.svc.MyServiceImpl;
}

package my.package.service.api.simpleimpl.svc;

public class MyServiceImpl implements MyService {
@Override
public void doSomething() {...}
}

В одном модуле допускается ТОЛЬКО одна реализация сервиса. Если необходимо наличие нескольких реализаций - придется создавать дополнительные модули.

  1. Все модули с реализацией поместить в module-path. Java найдет их самостоятельно.

Директива uses обязательна в module-info.java если в коде будет вызов ServiceLoader.load. Даже если сам модуль имеет в себе реализацию запрашиваемого сервиса. Без этой директивы поиск будет безрезультатным (хотя фактически, сам модуль и содержит реализацию).

 Comments
Comment plugin failed to load
Loading comment plugin