OSGI это не сложно
Я много раз встречал мнение, что OSGI это сложно. И более того, у самого когда-то такое мнение было. Году в 2009, если быть точным. На тот момент мы собирали проекты при помощи Maven Tycho, и деплоили их в Equinox. И это действительно было сложнее, чем разрабатывать и собирать проекты под JavaEE (в тот момент как раз появилась версия EJB 3, на которую мы и переходили). Equinox был намного менее удобен по сравнению с Weblogic, например, а преимущества OSGI тогда мне были не очевидны.
Зато потом, через много лет, мне пришлось на новой работе взяться за проект, который был задуман на основе Apache Camel и Apache Karaf. Это была не моя идея, я давно знал к тому моменту про Camel, и решил почитать про Karaf, даже еще не имея оффера. Почитал один вечер, и понял — вот же оно, простое и готовое, практически то же самое решение некоторых проблем типового JavaEE, аналогичное которому я когда-то делал на коленке при помощи Weblogic WLST, Jython, и Maven Aether.
Итак, допустим вы решили попробовать OSGI на платформе Karaf. С чего начнем?
Если хочется более глубокого понимания
Можно конечно начать с чтения документации. А можно и с хабра — тут были весьма неплохие статьи, скажем вот совсем уже давно такая. Но в целом karaf получил пока незаслуженно мало внимания. Была еще пара обзоров этот или этот. Вот это упоминание karaf лучше пропустить. Как говорится, не читайте на ночь советских газет… ибо вам там скажут, что kafar это OSGI фреймворк — так вы не верьте. OSGI фреймворки — это Apache Felix или Eclipse Equinox, на базе которых karaf как раз и работает. Можно при этом выбрать любой из них.
Надо заметить, что когда упоминается Jboss Fuse, или Apache ServiceMix, то следует это читать как «Karaf, с предустановленными компонентами», т.е. по сути — тоже самое, только собранное вендором. Я бы не советовал начинать с этого на практике, но почитать вполне можно и обзорные статьи про ServiceMix, например.
Для начала, я попробую тут определить совсем кратко, что из себя представляет OSGI, и для чего можно это применять.
По большому счету OSGI это средство для создания Java-приложений из модулей. Близким аналогом можно считать, например JavaEE, и в какой-то степени OSGI контейнеры могут выполнять JavaEE модули (скажем, web-приложения в виде War), а с другой стороны, многие JavaEE контейнеры содержат OSGI внутри, как средство реализации модульности «для себя». То есть, JavaEE и OSGI — это вещи похожие до совместимости, и удачно взаимодополняющие.
Важная часть любой модульной системы — это определение самого модуля. В случае OSGI модуль называется бандлом (bundle), и является хорошо известным всем разработчикам jar архивом с некоторыми дополнениями (то есть, и тут очень похож например на war или ear). По аналогии с JavaEE бандлы могут экспортировать и импортировать сервисы, являющиеся, по сути, методами классов (то есть, сервис — это интерфейс, или все публичные методы класса).
Метаданные бандла — это знакомый всем META-INF/MANIFEST.MF. Заголовки манифеста OSGI не пересекаются с заголовками для JRE, соответственно, вне OSGI контейнера бандл — это обычный jar. Существенно, что среди метаданных обязательно есть:
Bundle-SymbolicName: com.example.myosgi
Bundle-Version: 1.0.0
Это «координаты» бандла, и тут важен тот факт, что мы можете иметь в одном контейнере две и более одновременно установленных и работающих версии одного бандла.
По аналогии с JavaEE бандлы имеют жизненный цикл, который выглядит так: .
Кроме сервисов бандлы могут импортировать и экспортировать также пакеты (packages, в обычном для java смысле этого термина). Экспортируемые пакеты определены внутри бандла, и делаются доступными другим компонентам, когда бандл устанавливается в систему. Импортируемые определены где-то извне, должны быть кем-то экспортированы, и предоставлены бандлу контейнером, прежде чем он сможет начать работать.
Импорты пакетов могут быть объявлены необязательными, также как и импорты сервисов. И еще довольно существенно, что импорт и экспорт содержат указание на версию (или диапазон версий).
Отличия от JavaEE
Ну хорошо, что они похожи — мы поняли. А чем они отличаются?
На мой взгляд, основное отличие состоит в том, что OSGI дает нам намного большую гибкость. Как только бандл перешел в состояние STARTED, возможности ограничены только вашей фантазией. Скажем, вы можете спокойно создавать потоки (да, да, я знаю про ManagedExecutorService), пулы коннектов к базам, и т.п. Контейнер не берет на себя управление всеми ресурсами в той же мере, что JavaEE.
Вы можете в процессе работы экспортировать новые сервисы. Попробуйте скажем в JavaEE динамически создать новый сервлет? А тут это вполне возможно, более того, сервлетный контейнер karaf, созданный на базе jetty, ваш созданный сервлет тут же обнаружит, и он будет доступен клиентам по определенному URL.
Хотя это и является небольшим упрощением, но если JavaEE приложение в его классическом виде состоит в основном из компонентов
- пассивных, ожидающих вызова со стороны клиента
- определенных статически, то есть на момент деплоя приложения
С другой стороны, приложение на базе OSGI может содержать
- активные и пассивные компоненты, работающие по расписанию, выполняющие опрос, ну и слушающие сокет, и т.п.
- сервисы можно определять и публиковать динамически
- можно подписаться на события фреймворка, например слушать регистрацию сервисов, бандлов и т.п., получать ссылки на другие бандлы и сервисы, и делать много чего еще
Да, на JavaEE многое из этого тоже частично возможно (например, через JNDI), но в случае OSGI на практике делается проще. Хотя и рисков тут, наверное, несколько больше.
Отличия karaf от чистого OSGI
Помимо фреймворка karaf включает много чего полезного. В сущности, karaf есть средство для удобного управления OSGI фреймворком — установки туда бандлов (в том числе группами), их конфигурирования, мониторинга, описания ролевой модели и обеспечения безопасности, и тому подобного.
А давайте уже практиковаться?
Ну чтож, начнем сразу с установки. Тут писать особо нечего — идем на сайт karaf.apache.org, скачиваем дистрибутив, распаковываем. Версии karaf отличаются между собой поддержкой разных спецификаций OSGI (4, 5 или 6), и версий Java. Семейство 2.x я не советую, а вот и 3 (если у вас Java 8, как у меня), и 4 вполне можно пользоваться, хотя развивается на сегодня только семейство 4.x (текущая версия 4.2.2, она поддерживает OSGI 6 и Java вплоть до 10).
Karaf вполне нормально работает под Windows и Linux, все что нужно для создания сервиса и автозапуска, имеется. Поддержка MacOS и многих других видов Unix тоже декларируется.
Обычно можно запустить karaf сразу, если вы в интернете. Если нет, то как правило стоит подправить файл конфигурации, указав, где у вас репозиторий(-ии) maven. Обычно это будет корпоративный Nexus, или скажем Artifactory, кому что нравится. Конфигурация karaf располагается в папке etc дистрибутива. Названия файлов конфигурации не слишком очевидны, но в этом случае вам нужен файл org.ops4j.pax.url.mvn.cfg. Формат этого файла — java properties.
Задать репозиторий(и) можно как в самом файле конфигурации, перечислив список URL в настройках, так и просто показав, где лежит ваш settings.xml. Там же караф возьмет расположение вашего proxy, что в интранете как правило знать обязательно.
Kafar нужны несколько портов, это порты HTTP, HTTPS (если web настроен, по умолчанию нет), SSH, RMI, JMX. Если они у вас заняты, или вы хотите запустить на одном хосте несколько копий, то придется изменить и их. Всего этих портов примерно пять.
Порты типа jmx и rmi — вот тут: org.apache.karaf.management.cfg, ssh — org.apache.karaf.shell.cfg, чтобы поменять порты http/https, нужно будет создать (его скорее всего нет) файл etc/org.ops4j.pax.web.cfg, и записать в него значение org.osgi.service.http.port=нужный-вам-порт.
Дальше точно можно запускать, и как правило все заведется. Для промышленного использования очевидно придется внести изменения в файл bin/setenv, или bin/setenv.bat, чтобы например выделить нужный объем памяти, но для начала, чтобы посмотреть, это не нужно.
Можно запустить Karaf сразу с консолью, командой karaf, а можно в фоновом режиме, командой start server, и потом подключиться к нему по SSH. Это вполне стандартный SSH, с поддержкой SCP, и SFTP. Вы можете выполнять команды, и копировать туда-сюда файлы. Вполне можно подключиться любым клиентом, например моим любимым инструментом является Far NetBox. Доступен вход по логину и паролю, а также и по ключам. В потрохах jsch, со всеми вытекающими последствиями.
Рекомендую иметь сразу дополнительное окно консоли, для просмотра логов, которые размещаются в data/log/karaf.log (и другие файлы обычно там же, хотя это настраивается). Логи вам пригодятся, из кратких сообщений в консоли не все бывает понятно.
Я бы посоветовал сразу установить web, и hawtio web-консоль. Эти две вещи позволят вам намного проще ориентироваться в том, что происходит в контейнере, и в значительной степени рулить процессом оттуда (как бонус, вы получите jolokia и возможность мониторинга по http). Установка hawtio выполняется двумя командами из консоли karaf (как описано тут), и увы, на сегодня версия karaf 3.x уже не поддерживается (вам придется поискать более старые версии hawtio).
Из коробки https сразу не будет, для этого нужно предпринять некоторые усилия типа генерации сертификатов и пр. Реализация основана на jetty, поэтому все эти усилия по большей части делаются так же.
Хорошо, оно запустилось, что дальше?
Собственно, а вы чего ожидали? Я же говорил, будет ssh. Tab работает, если что.
Самое время установить какое-нибудь приложение. Приложение для OSGI либо является бандлом (bundle), или состоит из нескольких бандлов. Караф умеет деплоить приложения в нескольких форматах:
- Бандл в виде jar, как с манифестом OSGI, так и без него
- xml, содержащий Spring DM или Blueprint
- xml, содержащий так называемую feature, которая представляет собой набор из бандлов, других features, и ресурсов (файлов конфигурации)
- архив .kar, содержащий несколько features и репозиторий maven с зависимостями
- JavaEE приложения (при некоторых дополнительный условиях), например .war
Это можно сделать несколькими способами:
- положить приложение в папку deploy
- установить из консоли командой install
- установить feature командой из консоли feature:install
- kar:install
Ну в общем, это вполне похоже на то, что умеет типовой JavaEE контейнер, но несколько удобнее (я бы сказал — сильно удобнее).
Простой jar
Самый простой вариант — это установить обычный jar. Если он у вас лежит в maven репозитории, то для установки достаточно команды:
install mvn:groupId/artifactId/version
При этом Karaf понимает, что перед ним обычный jar, и обрабатывает его, создавая на лету бандл-обертку, т.н. wrapper, генерируя манифест по умолчанию, с импортами и экспортами пакетов.
Смысла от установки просто jar как правило немного, так как этот бандл будет пассивным — из него только экспортируются классы, которые будут доступны другим бандлам.
Этот способ применяется для установки компонентов типа Apache Commons Lang, для примера:
install mvn:org.apache.commons.lang3/commons-lang/3.8.1
А вот и не получилось :) Вот верные координаты:
install mvn:org.apache.commons/commons-lang3/3.8.1
Посмотрим, что вышло: list -u покажет нам бандлы и их источники:
karaf@root()> list -u
START LEVEL 100 , List Threshold: 50
ID | State | Lvl | Version | Name | Update location
-------------------------------------------------------------------------------------------------
87 | Installed | 80 | 3.8.1 | Apache Commons Lang | mvn:org.apache.commons/commons-lang3/3.8.1
88 | Installed | 80 | 3.6.0 | Apache Commons Lang | mvn:org.apache.commons/commons-lang3/3.6
Как видите, вполне можно установить две версии одного компонента. Update location — это то место, где мы взяли бандл, и откуда его можно при необходимости обновить.
Jar и Spring context
Если внутри вашего jar имеется Spring Context, все становится интереснее. Karaf Deployer автоматически ищет xml-контексты в папке META-INF/spring, и создает их, если успешно нашлись все нужные бандлу внешние пакеты.
Таким образом, все сервисы, которые были внутри контекстов, уже запустятся. Если у вас там были например Camel Spring, то Camel routes запустятся тоже. Это означает, что скажем REST сервис, или сервис, слушающий TCP-порт, вы уже можете запустить. Разумеется, запустить несколько сервисов, слушающих один порт, так просто не выйдет.
Просто Spring XML context
Если у вас внутри Spring Context были например определения JDBC DataSources, то вы вполне можете установить их в Karaf отдельно. Т.е. взять xml файл, содержащий только DataSource в виде <bean>, или любой другой набор компонентов, вы можете положить его в папку deploy. Context будет запущен стандартным способом. Единственная проблема в том, что созданные таким образом DataSources не будут видны другим бандлам. Их нужно экспортировать в OSGI в виде сервисов. Об этом — чуть позже.
Spring DM
Чем в сущности отличается Spring DM (версия с поддержкой OSGI) от классического Spring? Тем что в классическом случае у вас все бины в контексте создаются на этапе инициализации контекста. Новых появиться не может, старые никуда не денутся. В случае OSGI новые бандлы могут быть установлены, а старые удалены. Среда становится более динамичной, на это нужно как-то реагировать.
Способ реагирования называется сервисами. Сервис — это как правило некий интерфейс, со своими методами, который опубликован каким-либо бандлом. Сервис имеет метаданные, позволяющие его искать и отличать от другого сервиса, реализующего аналогичный интерфейс (от другого DataSource, очевидно). Метаданные — это простой набор свойств key-value.
Так как сервисы могут появляться и пропадать, те, кому они нужны, могут либо подписаться на сервисы при старте, либо слушать события, чтобы узнать об их появлении или пропадании. На уровне Spring DM, в XML, это реализовано как два элемента, service и reference, базовое назначение которых достаточно простое: опубликовать имеющийся бин из контекста как сервис, и подписаться на внешний сервис, опубликовав его в текущий spring-контекст.
Соответственно, при инициализации такого бандла, контейнер найдет для него нужные ему внешние сервисы, и опубликует реализуемые внутри бандла, сделав их доступными извне. Бандл стартует только после того, как ссылки на сервисы будут разрешены.
На самом деле, все немножко сложнее, потому что бандл может пользоваться списком похожих сервисов, и подписаться сразу на список. Т.е. у сервиса, в общем случае, есть такое свойство, как cardinality, принимающее значение 0..N. При этом подписка, где указано 0..1, описывает необязательный сервис, и в этом случае бандл успешно стартует, даже если такого сервиса в системе нет (а вместо ссылки на него получит заглушку).
Замечу, что сервис — это именно любой интерфейс (а можно публиковать и просто классы), поэтому вы вполне можете в качестве сервиса опубликовать java.util.Map с данными.
Кроме всего прочего, service позволяет указать метаданные, а reference — искать сервис по этим метаданным.
Blueprint
Blueprint — это более новая инкарнация Spring DM, которая немного попроще. А именно, если в Spring у вас есть custom XML элементы, то тут их нет, за ненадобностью. Иногда это все же доставляет неудобства, но прямо скажем — нечасто. Если вы не мигрируете проект из Spring, то можно начать сразу с Blueprint.
Суть тут таже самая — это XML, где описаны компоненты, из которых собирается контекст бандла. Для знающих Spring тут нет вообще ничего незнакомого.
Вот пример, как описать DataSource, и экспортировать в виде сервиса:
<?xml version="1.0" encoding="UTF-8"?>
<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0">
<bean id="dataSource" class="oracle.jdbc.pool.OracleDataSource">
<property name="URL" value="URL"/>
<property name="user" value="USER"/>
<property name="password" value="PASSWORD"/>
</bean>
<service interface="javax.sql.DataSource" ref="dataSource" id="ds">
<service-properties>
<entry key="osgi.jndi.service.name" value="jdbc/ds"/>
</service-properties>
</service>
</blueprint>
Ну вот, мы задеплоили этот файл в папку deployment, и посмотрели результаты команды list. Увидели, что бандл не запустился — в статусе Indtalled. Пробуем start , и получаем сообщение об ошибке.
Теперь в списке бандл в статусе Failed. В чем дело? Очевидно, в том, что ему тоже нужны зависимости, в данном случае — Jar с классами Oracle JDBC, а еще точнее — пакет oracle.jdbc.pool.
Находим нужный jar в репозитории, или скачиваем с сайта Oracle, и устанавливаем, как было описано раньше. Наш DataSource запустился.
Как этим всем воспользоваться? Ссылка на сервис называется в Blueprint reference (где-то, в контексте другого бандла):
<reference id="dataSource" interface="javax.sql.DataSource"/>
Затем данный бин становится, как обычно, зависимостью для других бинов (в примере camel-sql):
<bean id="sql" class="org.apache.camel.component.sql.SqlComponent">
<property name="dataSource" ref="dataSource"/>
</bean>
Jar и Activator
Канонический способ инициализации бандлов — это класс, реализующий интерфейс Activator. Это типичный интерфейс жизненого цикла, содержащий методы start и stop, которым передается контекст. Внутри них бандл как правило запускает свои потоки, если нужно, начинает слушать порты, подписывается на внешние сервисы при помощи OSGI API, и так далее. Это самый пожалуй сложный, самый базовый, и самый гибкий способ. Мне он за три года не понадобился ни разу.
Настройки и конфигурирование
Понятно, что такая конфигурация DataSource, как приведена в примере, мало кому нужна. Логин, пароль, и прочее, все хардкодится внутри XML. Нужно вынести эти параметры наружу.
<property name="url" value="${oracle.ds.url}"/>
<property name="user" value="${oracle.ds.user}"/>
<property name="password" value="${oracle.ds.password}"/>
Решение достаточно простое, и похожее на то, что применяется в классическом Spring: в определенный момент жизненного цикла контекста происходит подстановка значений свойств из разного рода источников.
На этом мы закончим первую часть. Если будет интерес к этой теме, то продолжение последует. Мы рассмотрим как собирать приложения из бандлов, конфигурировать, мониторить, автоматически развертывать системы на этой платформе.
Автор: sshikov