W3docs

Java-модули: сервисы

Паттерн ServiceLoader в модульной системе Java с помощью директив uses и provides.

Одна из ключевых целей модулей — разделение зависимостей: код, использующий некоторую возможность, не должен знать, какой класс её реализует. Java Platform Module System (JPMS) превращает это в полноценную возможность с помощью двух директив — uses и provides — которые во время выполнения связывает вместе ServiceLoader. Это модульная замена старого соглашения classpath: помещать файл META-INF/services в JAR.

В этой главе предполагается, что вы уже знаете, как писать дескриптор module-info.java; здесь мы добавляем к нему сервисные директивы.

Три роли

Сервис состоит из трёх частей, в идеале расположенных в трёх разных модулях:

  1. Интерфейс сервиса (или абстрактный класс) — контракт, например PricingRule. Он находится в модуле, который его экспортирует.
  2. Потребитель — код, запрашивающий реализации. Его модуль объявляет uses com.acme.PricingRule; и вызывает ServiceLoader.load(PricingRule.class).
  3. Один или несколько провайдеров — модули с реализациями. Каждый объявляет provides com.acme.PricingRule with com.acme.impl.StandardPricing;.

Потребитель никогда не импортирует StandardPricing. Он знает только интерфейс. Добавьте новый провайдер-модуль в module path — и потребитель его подхватит: без перекомпиляции, без изменений кода.

Директивы в module-info.java

// module com.acme.api
module com.acme.api {
    exports com.acme;            // export the PricingRule interface
}

// module com.acme.app (the consumer)
module com.acme.app {
    requires com.acme.api;
    uses com.acme.PricingRule;   // "I will ServiceLoader.load this"
}

// module com.acme.standard (a provider)
module com.acme.standard {
    requires com.acme.api;
    provides com.acme.PricingRule
        with com.acme.standard.StandardPricing;
}

uses сообщает резолверу, что потребитель будет искать этот сервис — тем самым модулю разрешается вызывать ServiceLoader.load. provides … with … регистрирует реализацию. Класс провайдера должен либо иметь публичный конструктор без аргументов, либо публичный статический метод provider(), возвращающий экземпляр.

Использование через ServiceLoader

ServiceLoader<PricingRule> loader = ServiceLoader.load(PricingRule.class);
for (PricingRule rule : loader) {
    System.out.println(rule.describe());
}
// or pick the first available
PricingRule rule = ServiceLoader.load(PricingRule.class)
    .findFirst()
    .orElseThrow();

ServiceLoader работает лениво — каждый провайдер инстанцируется только тогда, когда до него доходит итератор — и кэширует экземпляры. Он реализует Iterable, поэтому цикл for-each обходит все зарегистрированные провайдеры.

Практический пример: ServiceLoader со стандартным сервисом JDK

В JDK уже есть сервис, который можно загрузить без создания трёх модулей: java.util.spi.ToolProvider. Компилятор (javac), инструмент JAR (jar) и другие зарегистрированы как провайдеры этого интерфейса внутри модулей JDK. Следующая программа загружает их через ServiceLoader — именно тот код потребителя, который описан выше, но применённый к сервису, который уже настроен.

java— editable, runs on the server

Что можно вынести из этого примера:

  • Цикл for-each обошёл всех ToolProvider, зарегистрированных средой выполнения (обычно javac, jar, javadoc, …), и программа ни разу не импортировала ни один из классов реализаций. В этом весь смысл сервисов: потребитель зависит от ToolProvider — интерфейса — и обнаруживает конкретных провайдеров во время выполнения.
  • Каждый провайдер вывел другое имя класса реализации, хотя все они разделяют один интерфейс. Модули JDK объявили provides java.util.spi.ToolProvider with … в своих дескрипторах; ServiceLoader собрал их все. Добавление нового провайдера-модуля сделает его видимым в этом же цикле без каких-либо изменений здесь.
  • ToolProvider.findFirst("javac") вернул Optional, и код обработал оба варианта. Поиск сервисов по своей природе предполагает «может отсутствовать» — минимальная среда выполнения может не иметь ни одного провайдера инструментов — поэтому API заставляет планировать пустой случай, а не предполагать наличие реализации.
  • Запуск javac --version через загруженный провайдер доказывает, что объект полностью функционален и достигается исключительно через сервисный контракт. Потребитель вызвал реальное поведение без компиляционной зависимости от классов компилятора.
  • ServiceLoader инстанцирует лениво и только то, до чего вы итерируете; в настоящей трёхмодульной конфигурации module-info потребителя потребует uses java.util.spi.ToolProvider; для разрешения вызова. Собственные модули JDK уже это объявляют — поэтому пример работает без изменений.

Зачем использовать сервисы

  • Архитектуры плагинов — поместите провайдер JAR на module path, чтобы расширить приложение.
  • Опциональные реализации — выбирайте SSL, логирование или драйвер базы данных во время выполнения в зависимости от того, какой провайдер-модуль присутствует.
  • Инверсия зависимостей — высокоуровневый модуль-потребитель зависит от модуля интерфейса, но никогда от низкоуровневых провайдеров, поэтому стрелки зависимостей указывают на стабильный контракт.

Этот же механизм JDK использует для DriverManager, провайдеров кодировок и хронологий java.time. Последняя глава этой части, Переход на Java-модули, объединяет всё вместе: как перенести существующее приложение на classpath на module path шаг за шагом.

Практика

Практика
Модуль-потребитель вызывает 'ServiceLoader.load(PaymentGateway.class)', но цикл не находит ни одного провайдера во время выполнения, хотя провайдер-модуль с объявлением 'provides PaymentGateway with StripeGateway' присутствует на module path. Потребитель компилируется и запускается нормально. Какова наиболее вероятная причина?
Модуль-потребитель вызывает 'ServiceLoader.load(PaymentGateway.class)', но цикл не находит ни одного провайдера во время выполнения, хотя провайдер-модуль с объявлением 'provides PaymentGateway with StripeGateway' присутствует на module path. Потребитель компилируется и запускается нормально. Какова наиболее вероятная причина?
Was this page helpful?