Типы модулей Java
Именованные, автоматические и безымянные модули в Java и их взаимодействие при компиляции и выполнении.
Java Platform Module System (JPMS) различает три вида модулей. Только один из них — «настоящий», который вы создаёте сами; два других существуют для того, чтобы миллионы JAR-файлов, созданных до Java 9, продолжали работать. Ключ к безболезненной миграции — понять, каким видом становится конкретный JAR и что это полностью зависит от места его размещения. На этой странице описаны все три вида, показаны правила доступа между ними и приведена запускаемая программа, подтверждающая эти категории.
Именованные модули
Именованный (явный) модуль — это модуль с файлом module-info.class, помещённый на module path (--module-path / -p). Это полноправный модуль:
- Он имеет имя, заданное в дескрипторе.
- Он читает только те модули, которые указаны в
requires. - Он раскрывает только пакеты, перечисленные в
exports.
Это и есть строго инкапсулированный модуль, описанный в главе о декларации модуля. Все гарантии JPMS — объявленные зависимости, скрытые внутренности, раннее обнаружение ошибок — применяются именно к именованным модулям.
Автоматические модули
Автоматический модуль — это обычный JAR-файл (без module-info), помещённый на module path. JPMS оборачивает его в модуль, чтобы именованные модули могли указывать requires на него в процессе миграции — не дожидаясь, пока автор библиотеки добавит дескриптор. Автоматический модуль:
- Получает имя, производное от имени JAR-файла (например,
guava-32.1.jar→guava), если только в манифесте JAR не заданоAutomatic-Module-Name. - Экспортирует все пакеты — у него нет директивы
exports, поэтому все его пакеты открыты для всех. - Читает все остальные модули, включая безымянный, поэтому по-прежнему видит JAR-файлы из classpath.
Это мост: он позволяет начать писать именованные модули, зависящие от ещё не модуляризованных библиотек. Цена — полный отказ от инкапсуляции, а автоматически производное имя может измениться при переименовании JAR. Именно поэтому указание Automatic-Module-Name в манифесте — ответственное решение для библиотеки.
Безымянный модуль
Безымянный модуль — это «мусорное ведро» для classpath. Каждый класс, загружаемый из classpath, принадлежит безымянному модулю своего загрузчика классов. Он:
- Не имеет имени (
getName()возвращаетnull,isNamed()равноfalse). - Читает все остальные модули в системе.
- Экспортирует все свои пакеты другим безымянным/автоматическим модулям.
Но существует намеренная односторонняя стена: именованный модуль не может указать requires на безымянный модуль. Вы не можете его назвать, значит, не можете от него зависеть. Именно это правило задаёт порядок миграции — именованный модуль может зависеть только от других именованных или автоматических модулей, но никогда от кода в classpath.
Матрица доступа
Кто может читать кого — это небольшая таблица:
| От ↓ / К → | Named | Automatic | Unnamed |
|---|---|---|---|
| Named | только если requires | только если requires | никогда |
| Automatic | да | да | да |
| Unnamed | да | да | да |
Единственная ограничительная ячейка — именованный код не может обращаться к безымянному коду — и есть вся суть того, почему миграция идёт снизу вверх (об этом следующая глава).
Практический пример: определение вида модуля во время выполнения
Module API позволяет для любого класса узнать, является ли его модуль именованным и был ли он создан автоматически. Эта программа исследует три ссылки — собственный класс (classpath → безымянный), тип JDK (именованный) и загрузочный слой — чтобы сделать категории наглядными.
Что следует из результатов выполнения:
- Собственный класс программы классифицировался как UNNAMED с именем
null, тогда какjava.util.ListиHttpClientклассифицировались как NAMED (java.base,java.net.http). При запуске из classpath ваш код всегда безымянный; JDK всегда представляет собой набор именованных модулей. Вид модуля определяется способом его загрузки, а не чем-либо в самом классе. java.base.canRead(self)вернулfalse, аself.canRead(java.base)вернулtrue. Это и есть односторонняя стена в действии: безымянный модуль читает всё, но ни один именованный модуль не читает безымянный. Эта асимметрия — именно причина того, почему именованный код не может указыватьrequiresна код из classpath.classify()различил автоматический и именованный модули поdescriptor.isAutomatic(). Здесь вы не увидитеtrue(ничего не было помещено на module path в виде обычного JAR), но эта проверка — именно то, как инструментарий сообщает об автоматическом модуле: реальный объект модуля с синтезированным, полностью открытым дескриптором.isExported("java.util")вернулtrue, аisExported("jdk.internal.misc")вернулfalse, хотя оба являются реальными пакетами внутриjava.base. Директиваexportsименованного модуля — это список разрешений; неэкспортированные (или только квалифицированно экспортированные) пакеты невидимы для внешнего кода, даже если являютсяpublic. Безымянный модуль, напротив, экспортирует всё, что содержит.- Для наблюдения за всем этим не потребовалось никакого
module-info.java. Три категории — это факты времени выполнения о том, как был загружен класс;getModule()иgetDescriptor()раскрывают их — те же самые вызовы, на которые опираются инструменты миграции, чтобы понять, с чем они работают.
Почему существуют три вида
Два вида совместимости — автоматический и безымянный — означают, что Java 9+ запускает немодифицированные приложения Java 8. Вы переходите на строгую инкапсуляцию по одному JAR за раз: оставьте всё в classpath (всё безымянное) — ничего не изменится; переместите библиотеку на module path без дескриптора — она станет автоматической; добавьте module-info.java — она станет именованной. Далее, службы модулей показывают механизм uses/provides, который развязывает модули, а миграция модулей проводит реальный проект через эти три состояния.