W3docs

Миграция на модули Java

Стратегии миграции приложений на основе classpath в систему модулей платформы Java.

Вам не обязательно переходить на модульность при миграции на Java 9+. Приложение на classpath работает без изменений — всё становится одним большим безымянным модулем. Миграция — это необязательный шаг, который имеет смысл, когда вы хотите строгой инкапсуляции, пользовательского рантайма jlink или более чёткого графа зависимостей. Главное — делать это постепенно, поскольку три вида модулей — именованные, автоматические и безымянные — позволяют модульному и немодульному коду сосуществовать.

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

Сначала: просто запустите приложение на Java 9+ (без модулей)

Прежде чем добавлять module-info.java, перекомпилируйте и запустите существующее приложение на новом JDK, разместив всё на classpath. Вероятные ошибки не имеют ничего общего с вашими модулями — они связаны с теперь уже модульным JDK:

  • Удалённые модули Java EEjava.xml.bind (JAXB), java.activation, CORBA и другие были удалены из JDK. Добавьте их обратно в виде обычных зависимостей.
  • Инкапсулированные внутренние APIsun.misc.Unsafe и подобные классы больше недоступны. Флаги --add-exports / --add-opens служат аварийным выходом на время удаления этих вызовов.
  • Разделённые пакеты — два JAR-файла, содержащие одинаковый пакет, конфликтуют на пути к модулям (но не на classpath).

Сначала добейтесь успешного запуска приложения на classpath. Только потом начинайте модуляризацию.

Миграция снизу вверх (bottom-up)

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

  1. Начните с листовых библиотек — модулей, которые ни от чего вашего не зависят.
  2. Добавьте каждой из них module-info.java и переместите на путь к модулям.
  3. Зависящие от них модули теперь могут объявить requires. Работайте вверх по дереву зависимостей к точке входа приложения.

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

Миграция сверху вниз (top-down)

Когда вы не контролируете библиотеки (JAR-файлы сторонних разработчиков без дескрипторов), идите обратным путём:

  1. Сначала модуляризируйте собственный код верхнего уровня.
  2. Разместите немодульные JAR-файлы сторонних разработчиков на пути к модулям, где они станут автоматическими модулями.
  3. Объявите requires по их автоматическому имени (из имени JAR-файла или Automatic-Module-Name).
  4. Когда каждая библиотека поставит настоящий module-info, замените автоматическую зависимость на именованную — изменений в вашем requires не потребуется.

Автоматические модули — это строительные леса, делающие подход top-down возможным: они позволяют именованному коду зависеть от JAR-файлов, которые пока не являются модульными.

Инструменты и подводные камни

  • jdeps анализирует реальные зависимости JAR-файла и даже может сгенерировать первоначальный module-info.java (jdeps --generate-module-info). Начните с этого, а не пишите директивы вручную.
  • Automatic-Module-Name — если вы публикуете библиотеку, добавьте эту запись манифеста до написания полного дескриптора. Это фиксирует стабильное имя модуля, чтобы пользователи автоматических модулей не сломались, когда вы позже переименуете JAR-файл.
  • Разделённые пакеты необходимо объединять. Путь к модулям запрещает двум модулям владеть одним пакетом; classpath это допускал. Это наиболее распространённое препятствие при миграции.
  • opens для рефлексии. Фреймворки, использующие рефлексию над вашими классами (Jackson, JPA, Spring), требуют opens, иначе во время выполнения вы получите InaccessibleObjectException, даже если компиляция прошла успешно.
Внимание

Разделённые пакеты — это ошибка, которая удивляет людей чаще всего. На classpath два JAR-файла, содержащие классы в com.example.util, просто объединялись; на пути к модулям резолвер отклоняет их с ошибкой «module reads package ... from both». Не существует флага, который делает это допустимым — необходимо объединить дублирующийся пакет в один модуль (или переименовать один из них). Проверьте наличие разделённых пакетов с помощью jdeps до того, как начнёте писать дескрипторы.

Практический пример: исследование графа зависимостей, как делает jdeps

Планирование миграции начинается с вопроса «что от чего зависит». Эта программа читает дескрипторы модулей загрузочного слоя во время выполнения — ту же информацию requires, которую предоставляет jdeps — и также определяет, работает ли она сама как именованный модуль или на classpath, что служит конкретной проверкой того, насколько далеко продвинулась миграция.

java— editable, runs on the server

Что следует вынести из запуска:

  • Программа сообщила, что работает как UNNAMED модуль — именно то, чего и следует ожидать от кода на classpath, и сигнал во время выполнения, что эта кодовая база ещё не модуляризирована. Повторный запуск после добавления module-info и перемещения на путь к модулям изменит это на NAMED, давая конкретную проверку «дошли ли мы до цели» в процессе миграции.
  • inspect("java.sql") вывел реальный список requires, включая транзитивную зависимость в стиле java.transaction.xa или java.logging. Это та же информация, которую предоставляет jdeps — знание реальных зависимостей модуля является первым шагом при написании его module-info.java, и дескриптор уже содержит эти данные.
  • Некоторые requires выводили (transitive). Это зависимости, которые модуль реэкспортирует; когда вы объявляете requires java.sql, вы автоматически читаете и его транзитивные зависимости. Выявление таких зависимостей подсказывает, какие строки requires transitive нужно скопировать при модуляризации кода, который оборачивает подобный модуль.
  • findModule вернул Optional, подчёркивая, что модуль может попросту отсутствовать в конкретном рантайме — образ, урезанный jlink, может полностью исключить java.desktop. Планы миграции должны учитывать, какие модули реально присутствуют в целевом рантайме.
  • Каждый модуль в конечном счёте зависит от java.base, и он никогда не фигурирует в списке requires, потому что это зависимость неявная. Общее количество загрузочных модулей показывает, что JDK — это граф, который можно запрашивать программно, — фундамент, на котором такие инструменты, как jdeps и jlink, строят анализ и сжатие приложений.

Разумный порядок миграции, резюме

  1. Запустите на classpath на новом JDK; исправьте ошибки, связанные с удалёнными модулями и внутренними API.
  2. Используйте jdeps для составления карты зависимостей и поиска разделённых пакетов.
  3. Добавьте Automatic-Module-Name во все публикуемые вами библиотеки.
  4. Модуляризируйте снизу вверх, если вы владеете деревом, или сверху вниз (опираясь на автоматические модули), если нет.
  5. Добавьте opens там, где фреймворки используют рефлексию; проверяйте во время выполнения, а не только во время компиляции.

На этом часть про систему модулей платформы Java завершена: теперь вы знаете что такое модули, как объявить модуль, три вида модулей и как они сосуществуют, как сервисы разделяют модули и как перенести существующее приложение на путь к модулям без переписывания с нуля.

Практика

Практика
Ваша команда владеет всем исходным деревом приложения и хочет строгой инкапсуляции на всех уровнях. Библиотека A ни от чего вашего не зависит; библиотека B зависит от A; исполняемый модуль зависит от B. Какой порядок миграции сохраняет работоспособность каждой промежуточной сборки с наименьшим количеством автоматических обходных путей?
Ваша команда владеет всем исходным деревом приложения и хочет строгой инкапсуляции на всех уровнях. Библиотека A ни от чего вашего не зависит; библиотека B зависит от A; исполняемый модуль зависит от B. Какой порядок миграции сохраняет работоспособность каждой промежуточной сборки с наименьшим количеством автоматических обходных путей?
Was this page helpful?