Статьи

От OSGi до Jigsaw

В моем последнем посте я представил новую систему Java Platform Module с небольшим примером. По состоянию на сентябрь, есть игра с кодовым названием Jigsaw. После нескольких вводных игрушечных примеров я сделал то, что делает любой любопытный кодер: перенес знакомую (и не совсем тривиальную) кодовую базу на новую блестящую технологию. В этом случае я взял небольшую динамическую панель мониторинга на основе OSGi и реализовал ее с помощью предложенной системы модулей платформы Java.

Если вы хотите получить представление о том, что влечет за собой новая модульная система платформы Java (далее именуемая JPMS) и как начать работу, прочитайте «Система модулей Java: первый взгляд» . В этом посте предполагается, что вы знакомы с основами предлагаемой системы модулей. И если вы тот человек, который просто хочет увидеть код: вот, пожалуйста .

Оригинальное приложение OSGi

Прежде чем погрузиться в порт Jigsaw, давайте посмотрим, что представляет собой оригинальное приложение:

Название изображения

Как видите, он имитирует приборную панель автомобильной развлекательной системы. Однако требуется, чтобы эта автомобильная развлекательная система могла динамически расширяться и обновляться. Каждое из «приложений» происходит из отдельного пакета OSGi. Панель мониторинга собирает только те приложения, которые в данный момент подготовлены для среды выполнения OSGi, и показывает их. Вы можете нажать на значки, чтобы получить доступ к базовому «приложению» (музыкальный проигрыватель, навигация, телефон), которое происходит из того же пакета, что и значок панели инструментов. Этот пример послужил демонстрацией для выступления на конференции с моим коллегой  Полом Баккером  «Обеспечение IoT». В этом разговоре мы используем  Apache ACE динамически обновлять и предоставлять пакеты OSGi для запуска экземпляров автомобильной развлекательной системы на нескольких устройствах. Это действительно здорово видеть обновление вашей системы в режиме реального времени без перезагрузки. Если вы хотите увидеть это в действии, я рекомендую  посмотреть выступление . Демо начинается около 11 минут.

Карпров архитектура

Технически динамическая панель поиска просматривает все экземпляры  App интерфейса в реестре службы OSGi. Этот интерфейс является практически единственным фрагментом кода, который публично распространяется между пакетами. В свою очередь, пакеты, содержащие  App реализацию, регистрируются при запуске пакета и отменяются при остановке пакета. Это в полной мере использует динамический жизненный цикл, предоставляемый OSGi. Сводная панель получает  App экземпляры из реестра служб без необходимости знать о классах реализации. Инверсия контроля в действии! Каждый пакет реализации приложения также предоставляет свои собственные ресурсы, такие как изображения. Вы можете проверить оригинальное приложение  на GitHub .

Нахождение правильных модулей

Вопрос в том, насколько сложно портировать это модульное приложение OSGi в JPMS с использованием прототипа Jigsaw? Быть таким же динамичным, как OSGi, не является целью JPMS. Поэтому, чтобы сдержать ожидания, я уже рад, если мы сможем портировать модуль и структуру сервиса при запуске. Динамическое добавление и удаление новых модулей придется подождать пока.

Наша задача — перевести пакеты OSGi в эквивалентные модули Jigsaw. Первым шагом для воссоздания этого примера в JPMS является выяснение того, что должно входить в  module-info.javaдескрипторы. Эти дескрипторы модулей содержат информацию о зависимостях для модулей Java. Это похоже на метаданные OSGi в файле манифеста jar-файлов OSGi.

Самое простое определение модуля — это определение для пакета API:

module carprov.dashboard.api {
   exports carprov.dashboard.api;
   requires public javafx.graphics;
}

Вы можете найти полный код Jigsaw-версии панели инструментов  на GitHub,  если хотите следовать. Он компилируется и запускается на  сборке b86  JDK с поддержкой Jigsaw.

Мы объявляем модуль с именем  carprov.dashboard.api, экспортируя пакет с тем же именем. Это означает, что интерфейс и вспомогательный класс внутри этого пакета видны всем модулям, которые импортируют этот модуль. Далее нам нужно объявить, что нужно этому модулю с точки зрения зависимостей. Поскольку  App интерфейс использует типы JavaFX, они должны как-то требоваться. Важной целью JPMS является также модульность самого JDK. Поэтому мы не можем просто импортировать типы из JDK, не указав, из какого они модуля. Обратите внимание, что в отличие от предложения export, предложение require принимает имя модуля, а не имя пакета.

Итак, как найти нужный модуль среди ~ 80 модулей, которые в настоящее время составляют JDK в прототипе Jigsaw? К счастью, мы можем сделать лучше, чем проб и ошибок. JDK предоставляет инструмент под названием jdeps, который анализирует зависимости существующего Java-класса. Вы предоставляете имя класса и соответствующий путь к классу, который содержит класс:

$ jdeps -module -cp carprov.dashboard.api.jar carprov.dashboard.api.App
carprov.dashboard.api -> java.base
carprov.dashboard.api -> javafx.graphics
   carprov.dashboard.api (carprov.dashboard.api)
      -> java.lang                                          java.base
      -> javafx.scene                                       javafx.graphics

Последние две строки указывают, что интерфейс приложения импортируется из пакетов java.lang и javafx.scene. Предоставляя эту  -module опцию, jdeps также выводит исходные модули (справа). Таким образом, вы можете идентифицировать модули, предоставляющие пакеты, от которых зависит анализируемый класс. В этом случае модуль сводной панели должен требовать модуль java.base и модуль javafx.graphics из JDK. Это именно то, что мы делали в дескрипторе module-info.java ранее. За исключением, модуль java.base всегда неявно требуется для всех модулей. Вы не можете жить без этого.

Другой вариант поиска подходящих модулей — это  просмотреть  страницу обзора модулей в ранней сборке Jigsaw. Он дает исчерпывающий обзор всех модулей JDK и их зависимостей. Чтобы почувствовать новую модульную платформу Java, это необходимо.

Там же на последнем твисте: что делает  public в  requires public среднем в дескрипторе модуля? Давайте посмотрим на интерфейс приложения:

import javafx.scene.Node;

public interface App {
   String getAppName();
   int getPreferredPosition();   
   Node getDashboardIcon();
   Node getMainApp();
}

Если только модуль carprov.dashboard.api будет экспортирован модулем API Dashboard, что произойдет, если другой модуль импортирует его и попытается его использовать? Этот потребительский модуль затем вынужден также требовать модуль, содержащий javafx.scene.Node (в данном случае javafx.graphics). Поскольку Node используется как тип возвращаемого значения в App, интерфейс также не может использоваться без доступа к этому классу. Вы можете задокументировать это как часть модуля API Dashboard, но это подвержено ошибкам и в целом неудовлетворительно. public Ключевое слово в пунктах require решает это. По сути, он повторно экспортирует общедоступные пакеты из требуемого модуля как часть модуля API Dashboard. Теперь для модулей реализации приложения может потребоваться модуль API Dashboard, не беспокоясь о необходимости javafx.graphics. Без ключевого слова public компиляция завершится неудачно, если только модуль-потребитель не импортирует модуль javafx.graphics.

Этот механизм реэкспорта решает ту же проблему, которую решает OSGi «использует ограничения». Это идет немного дальше, хотя. С помощью механизма реэкспорта в JPMS вы можете создать «пустой» модуль, который действует как фасад. Открытый экспорт в дескрипторе модуля этого пустого модуля может объединять несколько других модулей. В качестве примера, вы можете использовать этот механизм для разделения модуля на несколько модулей без разбивки потребителей. Им по-прежнему требуется один и тот же модуль, только теперь он «делегирует» другим модулям.

Тем не менее, мы сбились с пути. Вернемся к примеру портирования панели. Как приложения на самом деле оказываются на панели инструментов с использованием JPMS?

Сервисы с ServiceLoader

До сих пор мы говорили об одном модуле и его зависимостях: API панели инструментов. Тем не менее, на рисунке выше показано 5 модулей в примере приложения. А как насчет модуля реализации панели мониторинга и модулей реализации приложения? Мы явно не хотим, чтобы панель мониторинга знала о конкретных классах реализации приложения. Ему просто нужно собрать экземпляры этих классов реализации, не выполняя саму реализацию. Слабая связь, помнишь?

Это означает, что нам не требуются какие-либо модули реализации приложения в информации модуля:

module carprov.dashboard.jfx {
   requires carprov.dashboard.api;
   requires javafx.base;
   requires javafx.controls;
   requires javafx.swing;

   uses carprov.dashboard.api.App;
}

Интересная часть последней строки дескриптора модуля:  uses carprov.dashboard.api.App;. С этим условием использования мы сообщаем JPMS, что нас интересуют экземпляры интерфейса приложения. Впоследствии панель мониторинга может использовать  API ServiceLoader  для получения этих экземпляров:

Iterable<App> apps = ServiceLoader.load(App.class);

for(App app: apps) {
  renderDashboardIcon(app);
}

Экземпляры создаются модульной системой. Конечно, большой вопрос: как модульная система находит поставщиков услуг?

Давайте рассмотрим пример модуля, предоставляющего сервис приложения. Модуль Phone предоставляет реализацию своего приложения следующим образом:

module carprov.phone {
   requires carprov.dashboard.api;
   requires javafx.controls;

   provides carprov.dashboard.api.App with carprov.phone.PhoneApp;
}

Волшебство происходит в последней строке. Это указывает на то, что мы хотим предоставить экземпляр App, используя конкретный класс реализации PhoneApp. Обратите внимание, что пакет PhoneApp не экспортируется. Никто не может создать его экземпляр, кроме JPMS или другого класса внутри того же модуля. Существует одно требование для класса обслуживания: он должен иметь конструктор по умолчанию без аргументов. Вы даже можете предоставлять услуги и использовать их в одном модуле. См.  Фактический источник  реализации Dashboard для примера использования и предложения-предложений в одном и том же дескрипторе модуля.

Теперь JPMS знает, что модуль реализации панели мониторинга хочет видеть экземпляры приложения, а модуль Phone (и другие) предоставляет эти экземпляры. Если в какой-то момент на путь к модулю будет добавлен дополнительный сервис, реализующий интерфейс приложения, панель мониторинга подберет его без изменений в дескрипторе модуля. Это не так динамично, как в оригинальном приложении OSGi. Эти новые модули загружаются только после перезапуска всего приложения (JVM).

Для тех, кто знает модель сервиса OSGi, статическое описание сервисных зависимостей — большая разница. Услуги OSGi приходят и уходят во время выполнения. С одной стороны, это более мощный и динамичный. С другой стороны, подход JPMS может обеспечить ошибки в случае невозможности подключения во время запуска. Кстати, текущий прототип, похоже, этого не делает. Объявление условия использования на интерфейсе без каких-либо реализаций в модульном пути во время выполнения не вызывает никаких предупреждений или ошибок. Это все еще в моем списке, чтобы экспериментировать с   конструкцией Layer JPMS. Давайте посмотрим, насколько это может приблизить нас к загрузке дополнительных модулей на лету.

Короче говоря, механизм ServiceLoader позволяет нам скрывать реализации в модульном мире. Это не совсем инъекция зависимости,   но это форма инверсии контроля. Я уверен, что модели внедрения зависимости будут построены на этом фундаменте.

Ресурсы

Модули могут заключать в себе больше, чем просто код. В этом приложении нам также нужны изображения. Загрузка ресурсов с использованием  Class.getResourceAsStream  все еще работает, с некоторыми оговорками. Класс, вызывающий этот метод, должен находиться в том же модуле, который содержит ресурс. В противном случае возвращается ноль.

Исходная реализация OSGi делегировала загрузку ресурсов вспомогательному классу в комплекте Dashboard API. Это было сделано путем передачи  BundleContext  запрашивающего пакета в этот вспомогательный класс. BundleContext обеспечивает доступ к пакету и его метаданным.

public static ImageView getImageByFullname(BundleContext bundleContext, String name) {
  URL entry = bundleContext.getBundle().getEntry(name);
  try {
    Image image = new Image(entry.openStream());
    ImageView view = new ImageView(image);
    view.setPreserveRatio(true);
    return view;
  } catch (IOException e) {
    throw new RuntimeException(e);
  }
}

Я попытался эмулировать это путем передачи объекта Class из запрашивающего модуля в аналогичный вспомогательный класс в версии JPMS:

public static ImageView getImageT(Class<?> loadingContext, String name) {
  Image image = new Image(loadingContext.getResourceAsStream(name));
  ImageView view = new ImageView(image);
  view.setPreserveRatio(true);
  return view;
}

Однако проверки доступа, похоже, не заботятся об объекте Class, который getResourceAsStream вызывается, а о том, какой класс находится над стеком вызовов. Это, конечно, модуль, который содержит вспомогательный класс, который не может читать ресурсы из модуля, который вызвал вспомогательный метод. В этом случае getResourceAsStream просто возвращает ноль. Это привело к некоторым интересным исключениям NullPointerException и запутанному взгляду на моем лице. В конце я просто вызвал запрашивающие модули  getResourceAsStream и передал полученный InputStream помощнику:

public static ImageView getImage(InputStream stream) {
     Image image = new Image(stream);
     ImageView view = new ImageView(image);
     view.setPreserveRatio(true);
     return view;
}

Поговорив с Марком Рейнхольдом в JavaOne, я узнал, что такое поведение разработано. Существует альтернатива, которая больше похожа на решение BundleContext: вы также можете передать java.lang.reflect.Module вспомогательный метод, подобный приведенному выше. Этот экземпляр усовершенствованного модуля позволяет получателю делать с вызывающим модулем все, что он захочет. Включая getResourceAsStream в этом модуле.

Список загруженных модулей

Карпров модули загружены

Исходная панель инструментов имела приложение, в котором перечислены загруженные пакеты OSGi, составляющие все приложение. Естественно, это также должно быть перенесено. Существует новый API для интроспективных модулей JPMS. Использовать его довольно просто:

Layer layer = Layer.boot();
for (Module m: layer.modules()) {
  if(m.getName().startsWith("carprov")) {
     String name = m.getName();
     Optional<Version> version = m.getDescriptor().getVersion();
     // Show it in the ui
  }
}

Модули организованы в слои. Поскольку мы специально не создаем модуль Layer сами, загруженные модули являются частью загрузочного слоя. Мы извлекаем этот слой и запрашиваем все загруженные модули. Затем мы обрабатываем только модули, начинающиеся с «carprov», чтобы не показывать модули JDK в обзоре.

Вывод

Будет интересно посмотреть, как текущий прототип JPMS превратится в готовую к работе модульную систему для Java 9. Одно можно сказать наверняка: это большой шаг вперед для платформы Java.

В целом я был приятно удивлен, насколько далеко я смог продвинуться с прототипом Jigsaw. Да, он менее динамичный, чем оригинал OSGi. С другой стороны, это также значительно менее сложно. Динамика обслуживания OSGi отличная, но она заставляет вас обрабатывать множество (параллельных) пограничных случаев. Вам действительно нужна эта динамика все время? Тем не менее, моей следующей задачей будет возвращение оригинальной динамики с использованием JPMS. Будьте на связи!