Статьи

Создание сухого модульного монолита с OSGi

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

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

Эта статья не требует предварительных знаний OSGi. По мере продвижения я объясню соответствующие аспекты, и если вы уйдете из этой статьи с пониманием того, что OSGi можно использовать для построения развязанного монолита при подготовке к возможному движению к микросервисам, она достигла своей цели. Вы можете найти источники для примера приложения на GitHub.

Наш домен: модульное приложение для обмена сообщениями

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

  • поддержка оболочки
  • поддержка IRC
  • Поддержка IoT с использованием дисплея на основе Tinkerforge и детектора движения

Каждый из этих каналов использует одинаковые интерфейсы для отправки и получения сообщений. Должна быть возможность подключать и отключать каналы и автоматически соединять их друг с другом. В терминах OSGi каждый канал будет представлять собой пакет и использовать сервисы OSGi для связи с другими каналами.

Не беспокойтесь, если у вас нет оборудования Tinkerforge. Очевидно, что модуль Tinkerforge не будет работать, но он не повлияет на другие каналы.

Общие настройки проекта и OSGi Bundles

Пример проекта будет построен с использованием Maven, и большая часть общих настроек выполняется в родительском модуле .

Пакеты OSGi — это просто файлы JAR с расширенным манифестом, который содержит записи, специфичные для OSGi. Пакет должен указать, какие пакеты он импортирует из других пакетов и какие пакеты он экспортирует. К счастью, большая часть этого происходит автоматически с помощью плагина bnd-maven-plugin . Он анализирует источники Java и автоматически создает подходящий импорт. Экспорт и другие специальные настройки определяются в специальном файле bnd.bnd . В большинстве случаев этот файл может быть пустым или даже пропущен.

Два плагина ниже гарантируют, что каждый модуль Maven создает действительный пакет OSGi. Отдельные модули не нуждаются в специальных настройках OSGi в pom — для них достаточно сослаться на родительский pom, который здесь создается. Maven-jar-plugin определяет, что мы хотим использовать файл MANIFEST из bnd вместо файла, сгенерированного Maven по умолчанию.

 <build> <plugins> <plugin> <groupId>biz.aQute.bnd</groupId> <artifactId>bnd-maven-plugin</artifactId> <version>3.3.0</version> <executions> <execution> <goals> <goal>bnd-process</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>2.5</version> <configuration> <archive> <manifestFile> ${project.build.outputDirectory}/META-INF/MANIFEST.MF </manifestFile> </archive> </configuration> </plugin> <!-- ... more plugins ... --> </plugins> </build> 

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

Декларативные услуги

В примере используются декларативные сервисы (DS) в качестве внедрения зависимостей и каркаса сервисов. Это очень легкая система, определяемая спецификациями OSGi, которая позволяет публиковать и использовать сервисы, а также использовать конфигурацию. DS очень хорошо подходит для OSGi, поскольку поддерживает полную динамику OSGi, когда пакеты и сервисы могут приходить и уходить в любое время. Компонент в DS может предлагать службу OSGi и зависеть от других служб и конфигурации OSGi. Каждый компонент имеет свой собственный динамический жизненный цикл и будет активирован только при наличии всех обязательных зависимостей. Он также будет динамически адаптироваться к изменениям в службах и конфигурации, поэтому изменения применяются практически мгновенно.

Поскольку DS заботится о зависимостях, разработчик может сосредоточиться на бизнес-сфере и не должен кодировать динамику OSGi. В качестве первого примера для компонента DS см. Сервис ChatBroker ниже. Во время выполнения DS использует XML-файлы для описания компонентов. Bnd-maven-plugin автоматически обрабатывает аннотации DS и прозрачно создает файлы XML во время сборки.

API чата

В нашем простом домене чата нам нужен только один интерфейс службы ChatListener , чтобы получать или отправлять сообщения чата. ChatListener прослушивает сообщения и модули, которые хотят получать сообщения, публикует реализацию ChatListener как сервис OSGi, чтобы сигнализировать, что они хотят слушать. Это называется шаблоном доски и широко используется.

 public interface ChatListener { void onMessage(ChatMessage message); } 

ChatMessage — это объект-значение, в котором хранится вся информация о сообщении чата.

 public class ChatMessage implements Serializable { private static final long serialVersionUID = 4385853956172948160L; private Date time; private String sender; private String message; private String senderId; public ChatMessage(String senderId, String sender, String message) { this.senderId = senderId; this.time = new Date(); this.sender = sender; this.message = message; } // .. getters .. } 

Кроме того, мы используем компонент ChatBroker , который позволяет отправлять сообщения всем доступным в данный момент слушателям. Это скорее удобная услуга, поскольку каждый канал может просто реализовать эту функцию самостоятельно.

 @Component(service = ChatBroker.class, immediate = true) public class ChatBroker { private static Logger LOG = LoggerFactory.getLogger(ChatBroker.class); @Reference volatile List<ChatListener> listeners; public void onMessage(ChatMessage message) { listeners.parallelStream().forEach((listener)->send(message, listener)); } private static void send(ChatMessage message, ChatListener listener) { try { listener.onMessage(message); } catch (Exception e) { LOG.warn(e.getMessage(), e); } } } 

ChatBroker определяется как декларативный компонент сервиса с использованием аннотаций DS. Он будет предлагать ChatBroker OSGi и будет активирован немедленно, когда будут присутствовать все зависимости (по умолчанию компоненты DS активируются, только если их служба запрашивается другим компонентом).

Аннотация @Reference определяет зависимость от одного или нескольких сервисов OSGi. В этом случае volatile List отмечает, что зависимость (0..n) . Список автоматически заполняется потокобезопасным представлением текущих доступных служб ChatListener . Метод send использует потоки Java 8 для параллельной отправки всем слушателям.

В этом модуле нам нужен файл bnd.bnd чтобы объявить, что мы хотим экспортировать пакет API. Фактически это единственная настройка создания пакета, которую мы делаем во всем этом примере проекта.

 Export-Package: net.lr.demo.chat.service 

Модуль Shell

Канал оболочки позволяет отправлять и получать сообщения чата с помощью Felix Gogo Shell , интерфейса командной строки (очень похожего на bash), который облегчает связь с OSGi. Смотрите также приложение на enroute для оболочки Gogo .

Класс SendCommand реализует команду Gogo, которая отправляет сообщение всем слушателям, когда команда shell send <msg> набирается в оболочке. Он объявляет о себе как сервис OSGi со специальными сервисными свойствами. Область действия и функция определяют, что служба реализует команду и как она адресована. Полный синтаксис нашей команды — chat:send <msg> но его можно сокращать до send <msg> если send уникален.

Когда Феликс Гого распознает команду в оболочке, он вызовет метод с именем команды и отправит параметр (ы) в качестве аргументов метода. В случае SendCommand сообщение с параметром используется для создания сообщения ChatMessage , которое затем отправляется в службу ChatBroker .

 @Component(service = SendCommand.class, property = {"osgi.command.scope=chat", "osgi.command.function=send"} ) public class SendCommand { @Reference ChatBroker broker; private String id; @Activate public void activate(BundleContext context) { this.id = "shell" + context.getProperty(Constants.FRAMEWORK_UUID); } public void send(String message) { broker.onMessage(new ChatMessage(id, "shell", message)); } } 

Класс ShellListener получает ChatMessage и печатает его в оболочку. Он реализует интерфейс ChatListener и публикует себя как сервис, поэтому он станет видимым для ChatBroker и будет добавлен в свой список слушателей чата. Когда приходит сообщение, onMessage метод onMessage который просто печатает в System.out , который в Gogo представляет оболочку.

 @Component public class ShellListener implements ChatListener { public void onMessage(ChatMessage message) { System.out.println(String.format( "%tT %s: %s", message.getTime(), message.getSender(), message.getMessage())); } } 

Модуль IRC

Этот модуль использует Apache Camel для подключения к каналу IRC, на который отправляются и принимаются сообщения. IRCConnector использует безопасную конфигурацию типа, как определено в спецификации 1.3 типа OSGi . Это позволяет определять имена, типы и значения по умолчанию для значений конфигурации. Во время выполнения они передаются администратором конфигурации Felix и настраиваются с помощью файлов .cfg в синтаксисе свойств. Конфигурация предоставляется компоненту в методе activate .

Вы можете заметить, что есть две зависимости @Reference к компонентам Camel irc и bean которые непосредственно не используются в приведенном ниже коде. Это своего рода обходной путь, чтобы убедиться, что мы подождем, пока компоненты станут активными, поскольку Apache Camel не полностью интегрирован с DS.

 @Component(name = "connector.irc", immediate = true) public class IRCConnector implements ChatListener { @Reference(target="(component=irc)") org.apache.camel.spi.ComponentResolver irc; @Reference(target="(component=bean)") org.apache.camel.spi.ComponentResolver bean; private OsgiDefaultCamelContext context; @Reference private ChatBroker broker; private ProducerTemplate producer; private String ircURI; @ObjectClassDefinition(name = "IRC config") @interface TfConfig { String nick() default "osgichat"; String server() default "193.10.255.100"; int port() default 6667; String channel() default "#osgichat"; } @Activate public void activate(BundleContext bc, TfConfig config) throws Exception { context = new OsgiDefaultCamelContext(bc, new OsgiServiceRegistry(bc)); ircURI = String.format( "irc:%s@%s:%d/%s", config.nick(), config.server(), config.port(), config.channel()); context.addRoutes(new RouteBuilder() { public void configure() throws Exception { from(ircURI).bean(new ChatConverter()).bean(broker); } }); context.start(); producer = context.createProducerTemplate(); } @Deactivate public void deactivate() throws Exception { context.shutdown(); } public void onMessage(ChatMessage message) { if (!"irc".equals(message.getSenderId())) { try { producer.sendBody(ircURI, message.getMessage()); } catch (CamelExecutionException e) { System.out.println(e.getMessage()); } } } } 

Когда зависимости и конфигурация присутствуют, OSGi activate метод activate . В нем создается контекст Camel с одним маршрутом для получения сообщений IRC с использованием компонента Camel IRC . Этот маршрут определяется from(ircURI) , где ircURI умолчанию ircURI значение "irc://[email protected]:6667/#osgichat" и открывает соединение с указанным сервером и каналом IRC и будет вызываться для каждого сообщения, полученного от канал. Сообщения передаются в ChatConverter , который преобразует их в наш тип ChatMessage и затем отправляет ChatBroker для доставки в нашу систему обмена сообщениями.

В другом направлении мы слушаем сообщения ChatMessages , предлагая обычный сервис ChatListener . Когда сообщение поступает в onMessage оно отправляется на другой маршрут Camel с использованием onMessage . На этом маршруте сообщение просто отправляется на тот же IRC URI, который сообщает Camel, что нужно отправить IRC сообщение на канал.

Модуль Тинкерфордж

Давайте сделаем наше приложение немного интереснее, добавив канал, который взаимодействует с устройствами IoT. Для этого мы используем систему Tinkerforge . Он позволяет экспериментировать с IoT без пайки, а также предлагает библиотеку Java, которая взаимодействует с демоном-кирпичом, поэтому нет необходимости писать собственный код.

Компонент TinkerConnect создает и настраивает Tinkerforge IPConnection, который взаимодействует с демоном Brick.

 @Component(name = "tf", configurationPolicy = ConfigurationPolicy.REQUIRE, service = TinkerConnect.class) @Designate(ocd = TinkerConnect.TfConfig.class) public class TinkerConnect { private static Logger LOG = LoggerFactory.getLogger(TinkerConnect.class); private IPConnection ipcon; @ObjectClassDefinition(name = "Tinkerforge config") @interface TfConfig { String host() default "localhost"; int port() default 4223; } @Activate public void activate(TfConfig config) throws Exception { ipcon = new IPConnection(); ipcon.connect(config.host(), config.port()); } @Deactivate public void deactivate() throws NotConnectedException { ipcon.disconnect(); } IPConnection getConnection() { return ipcon; } } 

LCDWriter — это еще один ChatListener , который использует сервис TinkerConnect для подключения к ЖК-дисплею и записывает все сообщения ChatMessage на ЖК-дисплей. Полный код также поддерживает буфер сообщений и прокрутку сообщений. Здесь в статье вы можете найти минимальный код для записи на дисплей.

 @Component public class LCDWriter implements ChatListener { private BrickletLCD20x4 lcd; private DateFormat df; @Reference TinkerConnect tinkerConnect; @Activate public void activate() throws TimeoutException, NotConnectedException { this.df = DateFormat.getTimeInstance(DateFormat.MEDIUM, Locale.ENGLISH); IPConnection ipcon = tinkerConnect.getConnection(); lcd = new BrickletLCD20x4("rV1", ipcon); lcd.backlightOn(); lcd.clearDisplay(); lcd.addButtonPressedListener((button) -> buttonPressed(button)); } public void onMessage(ChatMessage message) { try { initlcd(); lcd.clearDisplay(); lcd.writeLine((short)0, (short)0, df.format(message.getTime())); lcd.writeLine((short)1, (short)0, message.getSender()); lcd.writeLine((short)2, (short)0, message.getMessage()); if (message.getMessage().length() > 20) { lcd.writeLine((short)3, (short)0, message.getMessage().substring(20)); } } catch (Exception e) { // Ignore } } } 

Поскольку у нас нет полноценного устройства ввода ASCII, мы используем детектор движения для отправки сообщения при обнаружении движения.

 @Component(immediate = true, service=MotionDetector.class) public class MotionDetector { @Reference TinkerConnect tinkerConnect; @Reference ChatBroker broker; private BrickletMotionDetector motion; private MotionDetectedListener listener; @Activate public void activate() throws Exception { IPConnection ipcon = tinkerConnect.getConnection(); motion = new BrickletMotionDetector("sHt", ipcon); listener = () -> { broker.onMessage(new ChatMessage("sensor", "sensor", "Motion detected")); }; motion.addMotionDetectedListener(listener); } @Deactivate public void deActivate() throws Exception { motion.removeMotionDetectedListener(listener); } } 

Модульный монолит с OSGi может стать воротами к микросервисам

Упаковка с помощью bndtools

OSGi нуждается в отдельной декларации упаковки для развертывания. Это более сложный процесс, чем в обычном Java-проекте, поскольку OSGi — это слабая связь. POM отдельных модулей часто зависят только от API, поэтому они обычно не содержат достаточно информации для объявления полной настройки OSGi.

Для упаковки во время выполнения в OSGi нам нужен список пакетов, а также дополнительная конфигурация для контейнера и пакетов. Список пакетов может быть указан «вручную», но это очень утомительно и подвержено ошибкам. Лучшим способом является предоставление списка подходящих пакетов в виде индекса OSGi Bundle Repository (OBR) и использование распознавателя OSGi для определения фактических пакетов, которые будут использоваться.

В нашем случае мы определяем индекс с помощью файла POM . В этом файле мы определяем зависимости Maven от комплектов и других индексных POM. Некоторые проекты OSGi, такие как Aries RSA, уже предоставляют такие индексные POM, что позволяет очень легко их добавлять. Во время сборки индекс OBR создается с использованием bnd-indexer-plugin, который содержит метаданные всех пакетов. Наиболее важной частью этих данных является список требований и возможностей каждого комплекта.

 <plugin> <groupId>biz.aQute.bnd</groupId> <artifactId>bnd-indexer-maven-plugin</artifactId> <version>3.2.0</version> <configuration> <localURLs>REQUIRED</localURLs> </configuration> <executions> <execution> <id>index</id> <goals> <goal>index</goal> </goals> </execution> </executions> </plugin> 

Для разрешения пакетов мы используем bndtools . Это плагин Eclipse для разработки OSGi, а также поддерживает плагины Maven для таких задач, как разрешение пакетов. В bndtools развертывание определяется файлом bndrun . Этот файл содержит некоторую общую конфигурацию, например, используемую платформу OSGi, а также указатель на индекс и начальный список требований. Файл может быть создан вручную или с помощью редактора bndrun Eclipse. Первоначальный список требований — это просто список пакетов верхнего уровня из индекса.

 -runrequires: \ osgi.identity;filter:='(osgi.identity=org.apache.felix.metatype)',\ osgi.identity;filter:='(osgi.identity=org.apache.felix.fileinstall)',\ osgi.identity;filter:='(osgi.identity=org.ops4j.pax.logging.pax-logging-service)',\ osgi.identity;filter:='(osgi.identity=org.apache.felix.gogo.command)',\ osgi.identity;filter:='(osgi.identity=org.apache.felix.gogo.shell)',\ osgi.identity;filter:='(osgi.identity=net.lr.demo.chat.command)',\ osgi.identity;filter:='(osgi.identity=net.lr.demo.chat.lcd)',\ osgi.identity;filter:='(osgi.identity=net.lr.demo.chat.irc)' 

Первые 5 требований описывают базовую инфраструктуру для управления и ведения конфигурации, а также оболочку Gogo. Для нашего реального приложения нам нужно только добавить пакеты для каналов. Все их зависимости разрешены из требований комплектов и вспомогательного индекса.

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

Универсальная упаковка aka Модульный монолит

В проекте bndrun -all мы определяем файл bndrun со связками всех каналов. Процесс разрешения автоматически добавит необходимые библиотеки для поддержки таких каналов, как camel-core и camel-irc. Поскольку все каналы работают в одном процессе, нам не нужно никакого удаленного взаимодействия — каналы могут взаимодействовать с использованием простых сервисов OSGi.

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

Запуск приложения

Сначала мы создаем проект — это также создаст работающие JAR-файлы. Тогда мы можем запустить chat-all.jar .

 mvn clean install cd packaging/all java -jar target/chat-all.jar 

Это работает все в одной упаковке. Chat-all.jar содержит каркас OSGi, а также все пакеты времени выполнения. Конфигурация находится в каталоге etc

JAR должен запускаться без ошибок и вызывать оболочку Felix Gogo, которая показывает подсказку. Параллельно с запущенным приложением откройте канал #osgichat на freenode irc . Через некоторое время пользователь должен осгичать .

Теперь мы сначала отправляем сообщение из приложения OSGi в IRC.

 g! send Hi there 

Сообщение должно отображаться на оболочке и на канале IRC.

Теперь отправьте сообщение на IRC-канал. Это сообщение также должно отображаться на оболочке Gogo. Для завершения процесса и оболочки введите Ctrl-D .

Что мы узнали?

В этой статье мы узнали, как создавать приложения модульным способом, не представляя сложности микросервисов с самого начала. Мы определили отдельные пакеты на основе ограниченного контекста (в данном случае разные технологии чата) и свободно связали их со службами OSGi. Затем мы создали единый модуль развертывания (монолит развертывания), который легко развертывать и отлаживать.

Во второй статье мы покажем, как разделить это приложение на два микросервиса и развернуть их.