Объявление модуля Java: module-info.java
Объявление модуля Java в module-info.java: директивы requires, exports, opens, uses, provides — с примерами.
Модуль объявляется в специальном файле module-info.java, расположенном в корне дерева исходников модуля (рядом с верхним пакетом, но не внутри него). Этот файл компилируется в module-info.class. В нём нет обычного кода — только блок module со списком директив, описывающих границу модуля.
Структура файла
module com.acme.orders {
requires com.acme.common; // I depend on this module
requires transitive java.sql; // ...and so does anyone who requires me
exports com.acme.orders.api; // public to everyone
exports com.acme.orders.spi to com.acme.web; // public to one module only
opens com.acme.orders.model; // deep reflective access (e.g. for Jackson)
uses com.acme.orders.PricingRule; // I consume this service
provides com.acme.orders.PricingRule // I supply an implementation
with com.acme.orders.StandardPricing;
}Имя модуля (com.acme.orders) — это точечный идентификатор, который по соглашению является обратным DNS-префиксом содержащихся в нём пакетов. Это не пакет — у него собственное пространство имён, и два модуля не могут совместно использовать один пакет.
requires — объявление зависимостей
requires <module> означает «мне нужны экспортируемые пакеты этого модуля для компиляции и выполнения». Если требуемый модуль отсутствует, резолвер завершит работу с ошибкой при запуске. Два важных модификатора:
requires transitive— реэкспортирует зависимость. Любой модуль, зависящий от вас, автоматически читает её тоже. Используйте его, когда типы требуемого модуля присутствуют в ваших собственных публичных сигнатурах (метод, возвращающийjava.sql.Connection, вынуждает вызывающих видетьjava.sql).requires static— зависимость только на этапе компиляции, необязательная во время выполнения (для процессоров аннотаций, необязательных интеграций).
java.base подключается неявно — его никогда не нужно писать явно.
exports — объявление публичного API
exports <package> делает public-типы этого пакета видимыми для других модулей. Всё, что не экспортировано, надёжно инкапсулировано — невидимо, даже если помечено как public. Квалифицированная форма, exports <package> to <module>, <module>, сужает видимость до именованного списка разрешений, что удобно для пакетов SPI, которые разделяются только между вашими собственными модулями.
Обратите внимание: exports работает на уровне пакета и не является рекурсивным — экспорт com.acme.api не экспортирует com.acme.api.internal.
opens — разрешение глубокого рефлексивного доступа
exports предоставляет доступ на этапе компиляции к public-членам, но не предоставляет рефлексивного доступа к непубличным членам. Такие фреймворки, как Jackson, Hibernate и Spring, используют setAccessible(true) для доступа к private-полям — для этого нужна директива opens:
opens <package>— предоставляет рефлексивный доступ во время выполнения (включаяprivate-члены) всем модулям.opens <package> to <module>— квалифицированная форма, только для указанных модулей.open module com.acme.orders { … }— открывает каждый пакет (грубый инструмент для миграции).
Различие важно: пакет API вы exports-уете, но пакет классов данных, над которым должен работать сериализатор, вы opens-аете, не делая его частью API времени компиляции.
uses / provides — сервисы
Эти директивы реализуют паттерн ServiceLoader: uses <Service> объявляет, что вы потребляете интерфейс сервиса, а provides <Service> with <Impl> объявляет реализацию. У них есть отдельная глава — см. Сервисы модулей — здесь лишь отметим, что они находятся в том же дескрипторе.
Практический пример: построение дескриптора через API
Обычно вы пишете module-info.java и позволяете javac создать дескриптор. Но та же структура доступна программно через ModuleDescriptor.newModule(...), что является точным отражением синтаксиса директив — построение такого дескриптора — наглядный способ увидеть, во что превращается каждая директива.
Что можно вынести из результата выполнения:
- Названия методов конструктора совпадают один к одному с директивами:
.requires(...),.exports(...),.opens(...),.uses(...),.provides(...). Читая вывод, можно убедиться, что дескриптор содержит ровно ту информацию, что иmodule-info.java— доказательство того, что файл представляет собой чистые метаданные, а не исполняемый код. - Зависимость
java.sqlвыведена с модификатором[TRANSITIVE], аcom.acme.common— без него. Именно этот модификатор реэкспортируетjava.sqlв нижестоящие модули; обычная зависимость оставляет её приватной для данного модуля. - Два экспорта выведены по-разному:
com.acme.orders.apiкак "(to all)", аcom.acme.orders.spiкак "to [com.acme.web]". Квалифицированный экспорт хранит список целевых модулей внутри дескриптора — резолвер применяет его принудительно, поэтому ни один другой модуль не может прочитать SPI-пакет. opensпоявился в отдельной секции, независимо отexports. Дескриптор хранит доступ во время компиляции и рефлексивный доступ во время выполнения как различные факты — именно поэтому сериализатору нужна директиваopens, даже если пакет уже экспортирован.usesиprovidesзаписаны вместе с остальными директивами — объявления сервисов являются частью границы модуля, а не отдельным конфигурационным файлом, как это было на classpath (META-INF/services). В главе Сервисы модулей эти директивы превращаются в работающийServiceLoader.
Распространённые ошибки
- Размещение
module-info.javaвнутри директории пакета — он должен находиться в корне исходников. - Путаница между
exports(доступ во время компиляции, публичные члены) иopens(доступ во время выполнения, все члены).JsonMappingExceptionо недоступном поле почти всегда означает отсутствующую директивуopens. - Забытый
requires transitive, когда публичные методы раскрывают типы другого модуля, вынуждая каждого вызывающего добавлять зависимость вручную.
В следующей главе, Виды модулей, рассматриваются три вида модулей — именованный, автоматический и безымянный — и то, как наполовину модуляризованное приложение продолжает работать в процессе миграции на модули. Для общего понимания того, зачем нужна система модулей, см. введение в модули.