Введение в модули Java (JPMS)
Что такое модули в Java, какие проблемы решает JPMS и как это связано с classpath.
Java Platform Module System (JPMS), появившаяся в Java 9, добавляет уровень выше пакетов. Модуль — это именованная, самоописывающаяся группа пакетов, которая явно объявляет две вещи: что она требует от других модулей и что предоставляет им. Это объявление находится в единственном файле, module-info.java, в корне модуля. Данная часть книги посвящена этой теме; данная глава объясняет, зачем она существует.
Проблема, которую решает JPMS: classpath
До Java 9 все JAR-файлы помещались в один плоский classpath. Такой подход имел хронические проблемы:
- Отсутствие инкапсуляции. Каждый
public-класс в каждом JAR был доступен всем. Класс, предназначенный только для внутреннего использования (sun.misc.Unsafe,com.example.internal.*), мог использоваться кем угодно, поэтому его нельзя было безопасно изменить. - Отсутствие объявленных зависимостей. JAR никогда не указывал, какие другие JAR ему нужны. Отсутствующую зависимость обнаруживали только когда
NoClassDefFoundErrorвозникала во время выполнения — возможно, в продакшне. - JAR hell. Два JAR, содержащих один и тот же пакет, или две версии одной и той же библиотеки, молча объединялись в порядке classpath. Побеждал тот класс, который загружался первым.
Модули решают все три проблемы: модуль скрывает каждый пакет, который явно не экспортирует через export, объявляет каждый модуль, который requires, а JVM проверяет весь граф при запуске — отсутствующие или дублирующиеся модули вызывают быстрый сбой.
Модули, пакеты и JAR
Эти три понятия легко спутать:
| Понятие | Что группирует | Правило видимости |
|---|---|---|
| Package | классы | public/protected/package-private внутри JAR |
| JAR | packages + ресурсы | всё public видимо на classpath |
| Module | packages | только exported packages видимы другим модулям |
Модуль обычно упаковывается как JAR («модульный JAR» — обычный JAR с module-info.class в корне). Разница в дескрипторе: поместите JAR на classpath — правила игнорируются; поместите его на module path — JPMS их применяет.
Строгая инкапсуляция в одном предложении
Главное правило: пакет невидим для других модулей, если его модуль не экспортирует его — даже если его классы объявлены public. Теперь public означает «доступен коду, который может читать этот пакет», а чтение пакета требует наличия exports и requires. Именно поэтому сам JDK наконец смог скрыть свои внутренние детали: java.base экспортирует java.util, но не jdk.internal.misc.
JDK тоже модульный
Начиная с Java 9 JDK разделён примерно на 70 модулей (java.base, java.sql, java.xml, java.net.http, …). java.base особенный: он неявно требуется каждым модулем и содержит языковые основы (java.lang, java.util, java.io). Каждый класс, который вы когда-либо использовали, находится в одном из этих модулей — что наглядно показывает приведённый ниже пример.
Практический пример: инспекция модулей во время выполнения
Чтобы увидеть модули, не обязательно писать модуль: Module API во время выполнения сообщает модуль любого класса. Эта программа спрашивает несколько классов, к какому модулю они принадлежат, проверяет собственный модуль и заглядывает в boot layer, с которым запустилась JVM.
Что следует вынести из запуска:
String,ArrayListиHttpClientсообщилиjava.base,java.baseиjava.net.http. Каждый класс принадлежит ровно одному модулю, иgetModule()сообщает какому — языковые типы живут вjava.base, аHttpClientнаходится в собственном модуле, для использования которого потребовался быrequires java.net.http.- Собственный класс программы сообщил
isNamed() == falseиgetName()равныйnull. Код, запускаемый с classpath, попадает в unnamed module — бакет совместимости, который ничего явно не требует и читает каждый другой модуль. Именно поэтому программы на classpath по-прежнему компилируются и работают без изменений на Java 9+. ModuleLayer.boot()открыл граф модулей, которые JVM разрешила при запуске — подсчёт модулейjava.*показывает, что JDK действительно разделён на множество модулей, а не является монолитом.java.baseне является открытым (isOpen() == false), но экспортирует многие пакеты; он открываетjava.langиjava.utilдля всех, скрывая при этомjdk.internal.*. Экспорт пакета и открытие модуля — разные механизмы; в следующих главах мы вернёмся кopens.- Ничего из этого не потребовало
module-info.java. Module API — это рефлективные метаданные, доступные любой программе; написание собственного модуля (следующая глава) — это то, что позволяет вам объявлять эти правила, а не просто наблюдать за правилами JDK.
Что охватывает остаток этой части
module-info.java— директивы:requires,exports,opens,uses,provides.- Типы модулей — именованные, автоматические и безымянные модули и способы их совместного использования.
- Сервисы — разделение интерфейса и его реализации с помощью
uses/providesиServiceLoader. - Миграция — перенос существующего приложения с classpath на module path без масштабной переработки.
Модули необязательны: Java-приложение может работать на classpath вечно. Но их понимание объясняет современный JDK, открывает возможности для кастомных рантаймов через jlink и даёт библиотекам настоящую инкапсуляцию. Следующая глава, объявление module-info.java, описывает дескриптор, который делает модуль модулем.