Миграция на модули Java
Стратегии миграции приложений на основе classpath в систему модулей платформы Java.
Вам не обязательно переходить на модульность при миграции на Java 9+. Приложение на classpath работает без изменений — всё становится одним большим безымянным модулем. Миграция — это необязательный шаг, который имеет смысл, когда вы хотите строгой инкапсуляции, пользовательского рантайма jlink или более чёткого графа зависимостей. Главное — делать это постепенно, поскольку три вида модулей — именованные, автоматические и безымянные — позволяют модульному и немодульному коду сосуществовать.
В этой главе рассматривается, когда миграция оправдана, как сначала запустить существующее приложение на современном JDK, два направления модуляризации (снизу вверх и сверху вниз), инструменты, выполняющие основную работу, а также работающий пример, который исследует граф модулей так же, как это делает jdeps. Если приведённые термины незнакомы, начните с введения в модули и объявления модуля.
Сначала: просто запустите приложение на Java 9+ (без модулей)
Прежде чем добавлять module-info.java, перекомпилируйте и запустите существующее приложение на новом JDK, разместив всё на classpath. Вероятные ошибки не имеют ничего общего с вашими модулями — они связаны с теперь уже модульным JDK:
- Удалённые модули Java EE —
java.xml.bind(JAXB),java.activation, CORBA и другие были удалены из JDK. Добавьте их обратно в виде обычных зависимостей. - Инкапсулированные внутренние API —
sun.misc.Unsafeи подобные классы больше недоступны. Флаги--add-exports/--add-opensслужат аварийным выходом на время удаления этих вызовов. - Разделённые пакеты — два JAR-файла, содержащие одинаковый пакет, конфликтуют на пути к модулям (но не на classpath).
Сначала добейтесь успешного запуска приложения на classpath. Только потом начинайте модуляризацию.
Миграция снизу вверх (bottom-up)
Предпочтительная стратегия, когда вы контролируете всю кодовую базу:
- Начните с листовых библиотек — модулей, которые ни от чего вашего не зависят.
- Добавьте каждой из них
module-info.javaи переместите на путь к модулям. - Зависящие от них модули теперь могут объявить
requires. Работайте вверх по дереву зависимостей к точке входа приложения.
Это работает, потому что именованный модуль может требовать другие именованные и автоматические модули — так что пока собственные зависимости листовой библиотеки хотя бы автоматические, её можно модуляризировать. Каждый шаг сохраняет работоспособность сборки. Если листовой модуль предоставляет подключаемое поведение, это также удобный момент для введения границы сервисов, чтобы зависящие от него модули использовали интерфейс, а не конкретный класс.
Миграция сверху вниз (top-down)
Когда вы не контролируете библиотеки (JAR-файлы сторонних разработчиков без дескрипторов), идите обратным путём:
- Сначала модуляризируйте собственный код верхнего уровня.
- Разместите немодульные JAR-файлы сторонних разработчиков на пути к модулям, где они станут автоматическими модулями.
- Объявите
requiresпо их автоматическому имени (из имени JAR-файла илиAutomatic-Module-Name). - Когда каждая библиотека поставит настоящий
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, что служит конкретной проверкой того, насколько далеко продвинулась миграция.
Что следует вынести из запуска:
- Программа сообщила, что работает как 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, строят анализ и сжатие приложений.
Разумный порядок миграции, резюме
- Запустите на classpath на новом JDK; исправьте ошибки, связанные с удалёнными модулями и внутренними API.
- Используйте
jdepsдля составления карты зависимостей и поиска разделённых пакетов. - Добавьте
Automatic-Module-Nameво все публикуемые вами библиотеки. - Модуляризируйте снизу вверх, если вы владеете деревом, или сверху вниз (опираясь на автоматические модули), если нет.
- Добавьте
opensтам, где фреймворки используют рефлексию; проверяйте во время выполнения, а не только во время компиляции.
На этом часть про систему модулей платформы Java завершена: теперь вы знаете что такое модули, как объявить модуль, три вида модулей и как они сосуществуют, как сервисы разделяют модули и как перенести существующее приложение на путь к модулям без переписывания с нуля.