Java-модули: сервисы
Паттерн ServiceLoader в модульной системе Java с помощью директив uses и provides.
Одна из ключевых целей модулей — разделение зависимостей: код, использующий некоторую возможность, не должен знать, какой класс её реализует. Java Platform Module System (JPMS) превращает это в полноценную возможность с помощью двух директив — uses и provides — которые во время выполнения связывает вместе ServiceLoader. Это модульная замена старого соглашения classpath: помещать файл META-INF/services в JAR.
В этой главе предполагается, что вы уже знаете, как писать дескриптор module-info.java; здесь мы добавляем к нему сервисные директивы.
Три роли
Сервис состоит из трёх частей, в идеале расположенных в трёх разных модулях:
- Интерфейс сервиса (или абстрактный класс) — контракт, например
PricingRule. Он находится в модуле, который его экспортирует. - Потребитель — код, запрашивающий реализации. Его модуль объявляет
uses com.acme.PricingRule;и вызываетServiceLoader.load(PricingRule.class). - Один или несколько провайдеров — модули с реализациями. Каждый объявляет
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 — именно тот код потребителя, который описан выше, но применённый к сервису, который уже настроен.
Что можно вынести из этого примера:
- Цикл 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 шаг за шагом.