W3docs

Введение в модули 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.

java— editable, runs on the server

Что вынести из запуска:

  • 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'?