W3docs

Введение в модули 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
JARpackages + ресурсывсё public видимо на classpath
Modulepackagesтолько 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.

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, попадает в 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, описывает дескриптор, который делает модуль модулем.

Практика

Практика
Библиотечный JAR содержит класс 'public' в пакете 'com.acme.internal', который авторы предназначили только для внутреннего использования. JAR собран как модульный JAR, чей 'module-info.java' экспортирует 'com.acme.api', но не 'com.acme.internal'. Что произойдёт, если этот JAR поместить на MODULE PATH и другой модуль попытается импортировать 'com.acme.internal.Helper'?
Библиотечный JAR содержит класс 'public' в пакете 'com.acme.internal', который авторы предназначили только для внутреннего использования. JAR собран как модульный JAR, чей 'module-info.java' экспортирует 'com.acme.api', но не 'com.acme.internal'. Что произойдёт, если этот JAR поместить на MODULE PATH и другой модуль попытается импортировать 'com.acme.internal.Helper'?
Was this page helpful?