Введение в модули Java (JPMS)
Что такое модули в Java, какие проблемы решает JPMS и как он соотносится с classpath.
Введение в модули Java (JPMS)
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-ад. Два JAR, поставляющие один и тот же пакет, или две версии одной библиотеки молча сливались в порядке classpath. Побеждал тот класс, который загружался первым.
Модули атакуют все три: модуль скрывает каждый пакет, который он явно не export-ирует, объявляет каждый модуль, который он requires, а JVM верифицирует весь граф при запуске — отсутствующие или дублированные модули падают быстро.
Модули против пакетов против JAR
Эти три легко спутать:
| Понятие | Что оно группирует | Правило видимости |
|---|---|---|
| Пакет | классы | public/protected/пакетно-приватный внутри JAR |
| JAR | пакеты + ресурсы | всё public видно в classpath |
| Модуль | пакеты | только exported-пакеты видны другим модулям |
Модуль обычно упаковывается как JAR («модульный JAR» — обычный JAR с module-info.class в его корне). Разница в дескрипторе: положите JAR в classpath — правила игнорируются; поместите его на module path — JPMS их принуждает.
Сильная инкапсуляция, в одном предложении
Главное правило: пакет невидим для других модулей, если его модуль его не export-ирует — даже если его классы 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). Каждый класс, который вы когда-либо использовали, живёт в одном из этих модулей — что делает видимым проработанный пример ниже.
Проработанный пример: инспекция модулей во время выполнения
Вам не нужно писать модуль, чтобы увидеть модули: API модулей времени выполнения сообщает модуль любого класса. Эта программа спрашивает несколько классов, какому модулю они принадлежат, проверяет собственный модуль и заглядывает в загрузочный слой, с которым стартовала JVM.
Что вынести из запуска:
String,ArrayListиHttpClientсообщилиjava.base,java.baseиjava.net.http. Каждый класс принадлежит ровно одному модулю, иgetModule()говорит вам, какому — языковые типы все живут вjava.base, тогда какHttpClientнаходится в собственном модуле, который вам пришлось бы подключить черезrequires java.net.http, чтобы использовать.- Собственный класс программы сообщил
isNamed() == falseиgetName(), равныйnull. Код, запущенный из classpath, попадает в безымянный модуль, ведро совместимости, которое явно ничего не требует и читает каждый другой модуль. Вот почему программы classpath по-прежнему компилируются и работают без изменений на Java 9+. ModuleLayer.boot()раскрыл граф модулей, которые JVM разрешила при запуске — подсчётjava.*показывает, что JDK действительно разбито на множество модулей, а не один монолит.java.baseне открыт (isOpen() == false), но при этом экспортирует много пакетов; он открываетjava.langиjava.utilвсем, держаjdk.internal.*скрытыми. Экспортировать пакет и открыть модуль — это разные переключатели; более поздняя глава вернётся кopens.- Ничего из этого не требовало
module-info.java. API модулей — это рефлективные метаданные, доступные любой программе; написание собственного модуля (следующая глава) — это то, что позволяет вам объявлять эти правила, а не просто наблюдать правила JDK.
Что охватывает остаток этой части
module-info.java— директивы:requires,exports,opens,uses,provides.- Типы модулей — именованные, автоматические и безымянные модули и как они сочетаются.
- Сервисы — развязывание интерфейса от его реализации с помощью
uses/providesиServiceLoader. - Миграция — перенос существующего приложения classpath на module path без масштабного переписывания.
Модули необязательны: приложение Java может вечно работать на classpath. Но их понимание объясняет современное JDK, разблокирует пользовательские среды выполнения jlink и даёт библиотекам настоящую инкапсуляцию. Следующая глава пишет дескриптор, который делает модуль модулем.
Практика
Библиотечный JAR содержит 'public'-класс в пакете 'com.acme.internal', который авторы предназначают только для использования внутри библиотеки. JAR собран как модульный JAR, чей 'module-info.java' экспортирует 'com.acme.api', но не 'com.acme.internal'. Что происходит, когда этот JAR помещают на MODULE PATH и другой модуль пытается импортировать 'com.acme.internal.Helper'?