Статьи

Сервисы модуля Java 9

Проводка и поиск

В Java уже давно есть класс ServiceLoader . Он был представлен в 1.6, но с тех пор около Java 1.2 использовалась похожая технология. Некоторые программные компоненты использовали его, но использование не было широко распространено. Он может быть использован для модульности приложения (и даже больше) и для обеспечения возможности расширения приложения с помощью каких-либо плагинов, которые не зависят от времени компиляции приложения. Кроме того, конфигурация этих сервисов очень проста: просто укажите путь к классу / модулю. Мы увидим детали.

Загрузчик службы может найти реализации некоторых интерфейсов. В среде EE есть другие методы для настройки реализаций. В среде, не относящейся к EE, Spring стал вездесущим, что имеет аналогичное, хотя и не совсем то же решение, что и аналогичная, но не совсем та же проблема. Инверсия управления (IoC) и инъекции зависимостей (DI), предоставляемые Spring, являются решением для конфигурации проводки различных компонентов и представляют собой лучшую отраслевую практику, как отделить описание / код проводки от фактической реализации функций, которые классы должны выполнять.

Фактически, Spring также поддерживает использование загрузчика служб, так что вы можете связать реализацию, обнаруженную и созданную загрузчиком служб. Вы можете найти короткую и хорошо написанную статью об этом здесь .

ServiceLoader больше о том, как найти реализацию, прежде чем мы сможем внедрить ее в компоненты, которые в ней нуждаются. Младшие программисты иногда ошибочно смешивают их, и это не без причины: они сильно связаны.

Возможно, из-за этого большинство приложений, по крайней мере те, которые я видел, не разделяют проводку и обнаружение реализации. Эти приложения обычно используют конфигурацию Spring для поиска и подключения, и это просто нормально. Хотя это упрощение, мы должны жить с ним и быть счастливыми. Мы не должны разделять две функции только потому, что можем. Большинство приложений не должны разделять их. Они аккуратно сидят на простой строке XML-конфигурации приложения Spring.

Мы должны программировать на уровне абстракции, который необходим, но никогда не должен быть более абстрактным.

Да, это предложение является перефразировкой высказывания, приписываемого Эйнштейну. Если вы думаете об этом, вы также можете понять, что это утверждение — не что иное, как принцип KISS (пусть оно будет простым и глупым). Код, а не вы.

ServiceLoader находит реализацию определенного класса. Не все реализации, которые могут быть на пути к классам. Он находит только те, которые «рекламируются». (Я расскажу позже, что означает «рекламируемый».) Программа на Java не может пройти через все классы, которые находятся в пути к классам, или они могут?

Просмотр пути к классам

Этот раздел немного обходится, но важно понять, почему ServiceLoader работает так, как он работает, даже до того, как мы обсудим, как он работает.

Код Java не может запросить загрузчик классов, чтобы получить список всех классов, которые находятся в пути к классам. Вы можете сказать, что я лгу, потому что Spring просматривает классы и автоматически находит кандидатов на реализацию. Весна на самом деле обманывает. Я расскажу вам, как это происходит. Пока что признайте, что путь к классам не может быть просмотрен. Если вы посмотрите на документацию класса ClassLoader вы не найдете ни одного метода, который бы возвращал массив, поток или коллекцию классов. Вы можете получить массив пакетов, но вы не можете получить классы даже из пакетов.

Причиной этому является уровень абстракции того, как Java обрабатывает классы. Загрузчик классов загружает классы в JVM, и JVM все равно откуда. Это не предполагает, что фактические классы находятся в файлах. Есть много приложений, которые загружают классы, а не из файла. На самом деле, большинство приложений загружают некоторые классы с разных носителей. Также ваши программы, вы просто не можете знать это. Вы когда-нибудь использовали Spring, Hibernate или какой-то другой фреймворк? Большинство из этих сред создают прокси-объекты во время выполнения и загружают эти объекты из памяти, используя специальный загрузчик классов. Загрузчик классов не может сказать вам, будет ли когда-либо новый объект, созданный платформой, которую он поддерживает. В данном случае путь к классам не является статическим. Для этих специальных загрузчиков классов даже не существует classpath. Они находят классы динамически.

Ладно. Хорошо сказано и подробно описано. Но опять же: как Spring находит классы? Весна на самом деле делает смелое предположение. Предполагается, что загрузчик классов является специальным: URLClassLoader . ( Как пишет Николай Парлог в своей статье, с Java 9 это уже не так.) Он работает с classpath, который содержит URL-адреса, и может возвращать массив URL-адресов.

ServiceLoader не делает такого предположения и поэтому не просматривает классы.

Как ServiceLoader находит класс

ServiceLoader может находить и создавать экземпляры классов, которые реализуют определенный интерфейс. Когда мы вызываем статический метод ServiceLoader.load(interfaceKlass) , он возвращает «список» классов, которые реализуют этот интерфейс. Я использовал «список» между кавычками, потому что технически он возвращает экземпляр ServiceLoader , который сам реализует Iterable поэтому мы можем перебирать экземпляры классов, которые реализуют интерфейс. Итерация обычно выполняется в цикле for вызывая метод load() после двоеточия (:).

Для успешного поиска экземпляров файлы JAR, содержащие реализации, должны иметь специальный файл в каталоге META-INF/service имеющий полное имя интерфейса. Да, в имени есть точки, и нет никакого конкретного расширения имени файла, но, тем не менее, это должен быть текстовый файл. Он должен содержать полное имя класса, который реализует интерфейс в этом файле JAR.

ServiceLoader вызывает метод findResources для получения URL-адресов файлов и считывает имена классов, а затем снова запрашивает ClassLoader для загрузки этих классов. Классы должны иметь открытый конструктор с нулевым аргументом, чтобы ServiceLoader мог создавать каждый из них.

Наличие в этих файлах имен классов для совмещения загрузки классов и создания экземпляров с использованием загрузки ресурсов работает, но это не слишком элегантно.
Java 9, сохраняя надоедливое решение META-INF/services представила новый подход. С введением Jigsaw у нас есть модули, а модули имеют дескрипторы модулей. Модуль может определить сервис, который может загрузить ServiceLoader и модуль также может указать, какие сервисы ему могут понадобиться для загрузки через ServiceLoader . Этот новый способ обнаружения реализации интерфейса службы перемещается из текстовых ресурсов в код Java. Чистым преимуществом этого является то, что ошибки кодирования, связанные с неправильными именами, могут быть идентифицированы во время компиляции или во время загрузки модуля, чтобы ускорить сбой кода.

Чтобы сделать вещи более гибкими или просто сделать их бесполезно более сложными (будущее покажет), Java 9 также работает, если класс не является реализацией интерфейса службы, но имеет public static provider() метод public static provider() который возвращает экземпляр класса который реализует интерфейс. (Между прочим, в этом случае класс провайдера может даже реализовать интерфейс службы, если он этого хочет, но, как правило, это фабрика, так зачем это делать. Обратите внимание на SRP.)

Образец кода

Вы можете скачать мультимодульный проект Maven с https://github.com/verhas/module-test .

Этот проект содержит три модуля Consumer , Provider и ServiceInterface . Потребитель вызывает ServiceLoader и использует службу, которая определяется интерфейсом javax0.serviceinterface.ServiceInterface в модуле ServiceInterface и реализуется в модуле Provider . Структуру кода можно увидеть на следующем рисунке:

Файлы module-info содержат объявления:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
module Provider {
    requires ServiceInterface;
    provides javax0.serviceinterface.ServiceInterface
      with javax0.serviceprovider.Provider;
}
 
module Consumer {
    requires ServiceInterface;
    uses javax0.serviceinterface.ServiceInterface;
}
 
module ServiceInterface {
    exports javax0.serviceinterface;
}

Ловушки

Здесь я расскажу вам о некоторых глупых ошибках, которые я совершил во время создания этого очень простого примера, чтобы вы могли учиться на моих ошибках, а не повторять их. Прежде всего, в документации по Java 9 в ServiceLoader которое гласит:

Кроме того, если служба не находится в модуле приложения, то объявление модуля должно иметь директиву require, которая указывает модуль, который экспортирует службу.

Я не знаю, что он хочет сказать, но то, что это значит для меня, не соответствует действительности. Может быть, я неверно истолковал это предложение, что вполне вероятно.

В нашем примере модуль Consumer использует нечто, реализующее интерфейс javax0.serviceinterface.ServiceInterface . Это что-то, на самом деле является модулем Provider и его реализацией, но оно решается только во время выполнения и может быть заменено любой другой подходящей реализацией. Таким образом, он нуждается в интерфейсе и, следовательно, должен иметь директиву require в информационном файле модуля, для которого требуется модуль ServiceInterface . Для этого не требуется модуль Provider ! Модуль Provider аналогичным образом зависит от модуля ServiceInterface и должен требовать его. Модуль ServiceInterface ничего не требует. Он экспортирует только пакет, содержащий интерфейс.

Также важно отметить, что ни модуль Provider ни модуль Consumer не обязаны экспортировать какой-либо пакет. Provider предоставляет сервис, объявленный интерфейсом и реализованный классом, названным в честь ключевого слова with в файле информации модуля. Он предоставляет этот единственный класс для всего мира и ничего больше. Чтобы обеспечить только этот класс, было бы излишним экспортировать пакет, содержащий его, и это, возможно, излишне открыло бы классы, которые могут происходить в том же пакете, но являются внутренним модулем. Consumer вызывается из командной строки с помощью параметра –m , и он также не требует, чтобы модуль экспортировал какой-либо пакет.
Команда, как запустить программу

1
2
3
4
java -p Consumer/target/Consumer-1.0.0-SNAPSHOT.jar:
  ServiceInterface/target/ServiceInterface-1.0.0-SNA
  PSHOT.jar:Provider/target/Provider-1.0.0-SNAPSHOT.
  jar -m Consumer/javax0.serviceconsumer.Consumer

и это может быть выполнено после успешной команды установки mvn . Обратите внимание, что плагин компилятора maven должен быть как минимум версии 3.6, иначе ServiceInterface-1.0.0-SNAPSHOT.jar будет находиться в пути к классам вместо пути к модулю во время компиляции, и во время компиляции не будет найдена информация о module-info.class файл module-info.class .

Какой смысл

ServiceLoader можно использовать, когда приложение подключено к некоторым модулям только во время выполнения. Типичным примером является приложение с плагинами. Я сам столкнулся с этим упражнением, когда портировал ScriptBasic для Java с Java 7 на Java 9. Интерпретатор языка BASIC может быть расширен классами, содержащими общедоступные статические методы, и их необходимо аннотировать как BasicFunction . В последней версии требовалось, чтобы хост-приложение встраивало интерпретатор в список всех классов расширений, вызывающих API в коде. Это лишнее и не нужно. ServiceLoader может найти реализацию службы, для которой интерфейс ( ClassSetProvider ) определен в основной программе, а затем основная программа может вызывать реализации службы одну за другой и регистрировать классы, возвращаемые в наборах. Таким образом, хост-приложению не нужно ничего знать о классах расширений, достаточно, чтобы классы расширений помещались в путь модуля и каждый из них предоставлял услугу.

Сам JDK также использует этот механизм для определения местоположения регистраторов. Новый Java 9 JDK содержит класс System.LoggerFinder который может быть реализован как сервис любым модулем, и если есть реализация, ServiceLoader может найти метод System.getLogger() который найдет это. Таким образом, протоколирование не привязано к JDK, не привязано к библиотеке во время компиляции. Достаточно предоставить регистратор во время выполнения и приложение, библиотеки, которые использует приложение, и JDK будут использовать одно и то же средство ведения журнала.

Со всеми этими изменениями в механизме загрузки сервисов и превращением его в язык, который не поддерживается при загрузке ресурсов, можно надеяться, что этот тип обнаружения сервисов получит импульс и будет использоваться в более широком масштабе, как он использовался ранее.

Опубликовано на Java Code Geeks с разрешения Питера Верхаса, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Java 9 Module Services

Мнения, высказанные участниками Java Code Geeks, являются их собственными.