История одного проекта.
Вам когда нибудь снились верблюды? Вот и мне тоже нет. Но работая с Camel-ом уже третий год, начинают снится не только верблюды.
В общем буду делиться опытом, писать о верблюдах и учить вас их готовить. Это серия статей в трёх частях: первая часть будет для тех кому интересны истории и муки творчества; вторая — больше техническая о паттернах интеграции, их применении и третья часть об ошибках и отладке.
Если вам нужно объединить ваши сервисы, здесь вы узнаете чем хорош Camel. Если вы хотите научиться использовать что-нибудь новенькое, здесь мы начнём с азов. Если вам нравятся истории и оригинальные фишки, которые есть в каждой команде, то читайте дальше.
Задача интеграции
Начну с того, как появилась необходимость в сервисной шине. Мы разрабатываем крупную систему, которую ласково “за глаза” назвали монстриком. Монстрик получился большой и страшный, а в действительности это была одна из BPM систем (business process managment system). Началось всё несколько лет назад. Однажды на совещании руководитель проекта заговорил о планах на будущее:
— Коллеги, в ближайшее время мы планируем интегрироваться с большим количеством внутренних и внешних систем. Сейчас нам надо проработать системный подход для того, чтоб наши аналитики смогли начать подготовку задач.
Потом ещё какое-то время он рассказывал о тех системах, с которыми нам предстоит интегрироваться. Здесь были как внешние хорошо известные системы, например, elibrary.ru, так и внутренние, которые ещё только предстояло создать. По завершению рассказа обозначились характерные черты интеграции с разными типами систем. Для внешних систем, — это большая неопределённость описания процессов и объектов предметной области (бизнес объектов), которыми предстояло обмениваться; зато было понятно направление передачи данных (загрузка или выгрузка); просматривались дополнительные требования к защите каналов передачи данных, обеспечению гарантированной доставки и предпосылки для создания инструментов исправления ошибочных данных. Для вторых — внутренних систем, ясности было больше. Четко описывалось функциональное назначение каждой из систем. Благодаря чему можно было проработать круг объектов подлежащих передачи. Вот только для этих систем неопределённость проявлялась на уровне будем — не будем, успеем — не успеем реализовать. Не вдаваясь в подробности прикладной области, ограничусь типичными задачами на решении, которых мы сконцентрировались.
- Необходимо загрузить информацию в основную систему по стандартному протоколу. На момент обсуждения для внешних систем упоминались два: HTTP, ftp; для внутренних: JMS, HTTP, NFS, SMB и/или используя интерфейс RMI. С помощью какой архитектуры мы могли бы решить эту задачу?
- Предположим необходимо выгрузить информацию из основной системы, и передать во внешнюю систему по одному из стандартных протоколов. Какие могли быть способы решения такой задачи?
- Или же требуется передать один, два, n бизнес-объектов. Как мы могли подготавливать данные и какой формат использовать?
- Допустим, необходимо отправить электронное сообщение через почтовый сервер, который временно не доступен. Для этого пользователю потребуется либо дождаться доступности последнего, либо получить сообщение о невозможности закончить свою работу сейчас. Как мы могли построить систему, гарантирующую доставку и не блокирующую работу пользователей?
- Примем, что нам необходимо иметь возможность забирать информацию с внешней системы в 1 или 3 часа ночи, но наша система в это время прерывала свою работу на обслуживание. Как сделать так, чтоб во время не доступности основной системы процессы выгрузки и загрузки данных не останавливались?
- И последняя задача: необходимо иметь возможность оперативно изменить логин и пароль для доступа к внешней системе или протокол связи с ftp на sftp. Как мы могли сделать так, чтоб изменение настроек можно было выполнять гибко, не нарушая работы основной системы?
Обсудив задачи, мы пришли к решению создать автономное приложение, которое будет связывать основную систему с другими. Идея использовать промежуточное звено не нова и известна в мире интеграции под термином сервисная шина предприятия или ESB). Давайте очертим круг функциональных возможностей, которыми должно было обладать наше приложение. За основу взяты формулировки задач. Оно должно уметь:
- взаимодействовать с другими системами по стандартным протоколам: HTTP, FTP, а также по протоколу: JMS или с помощью интерфейса RMI.
- читать, записывать и создавать файлы в локальной файловой системе, для работы с NFS
- отправлять почтовые сообщения через SMTP сервера.
- разбирать один из транспортных форматов XML, JSON
- иметь гибкую настройку
- иметь возможность описывать правила согласования форматов на одном из языков программирования Java, Groovy. Мы работали с этими языками, поэтому могли рассчитывать на отсутствие дополнительного overhead-а на изучение.
- возможность работать с XSLT и xPath, была бы дополнительным плюсом.
От рассуждений к практике
Через какое-то время, когда процесс поиска только набирал обороты, аналитики по заказу клиента написали первую постановку:
— Ребят, мы тут постановку написали, надо её сделать быстро. Заказчик очень ждал её ещё вчера, поэтому постарайтесь.
У нас так бывает часто, но всегда мы успеваем сделать новую функциональность во время. Этот раз не исключение, нас ждал большой объём работы и сжатые сроки реализации. Хотя мы и не могли использовать для этой постановки сервисную шину, но мы использовали постановку как первую практическую задачу для создания прототипа.
А пока предстояло выбрать технологии, на которых должен был строиться наш прототип. Изобретать свой велосипед и начинать с чистого листа — здорово, но слишком дорого. Проприетарные решения тоже не стали использовать потому, что результат требовался быстро, а на то, чтоб согласовать и решить финансовые вопросы нужно время. Поэтому обратились к opensource проектам. На тот момент выбор был небольшой, поэтому оценить достоинства “верблюда” смогли с первого взгляда.
Как видно из схемы, Apache Camel — модульный, легко расширяемый каркас для интеграции приложений. Главные структурные элементы: компонентная модель, механизм маршрутизации, механизм обработки сообщений. Компонентная модель представляет собой набор фабрик создающих конечные точки маршрутизации. Например, конечная точка может быть абстракцией отправляющей сообщения в очередь JMS брокера. Механизм маршрутизации связывает конечные точки с абстракциями обработки сообщений. Последний, механизм обработки сообщений, позволяет манипулировать данными сообщений. Например, преобразовывать в другой формат, проводить валидацию, добавлять новое содержимое, журналировать и многое другое. Все три архитектурных компонента модульные и благодаря этому возможности Camel постоянно расширяются. Подробно ознакомиться с архитектурой можно в википедии и на официальном сайте. Только появившись Apache Camel обзавёлся солидной коллекцией компонентов. Нашлись компоненты, чтобы удовлетворить все наши требования: JMS, HTTP, ftp, file, SMTP, xPath, xslt, XStream, Groovy, Java. Как и автор статьи Which Integration Framework Should You Use – Spring Integration, Mule ESB or Apache Camel? мы выбрали Camel. В этой статье проводится сравнение трёх framework-ов: Spring Integration, Mule ESB and Apache Camel. Ключевое преимущество автор описывает так:
… Apache Camel due to its awesome Java, Groovy and Scala DSLs, combined with many supported technologies.
Возможность использовать fluent Java DSL вместо “корявого” XML стала большим преимуществом и для нас. Возникает вопрос в чём корявость? XML отличный язык разметки, но ему уже давно предпочитают JSON или YAML. Предпочтение им отдают из-за простоты, лучшей читаемости, меньшего числа вспомогательной информации и более простых алгоритмов разбора. Языки программирования такие как Java, Groovy, Scala имеют полноценную поддержку современными IDE, а значит в отличии от XML появляется возможность отладки и рефакторинга. Сомнений в Camel-е не осталось, и он лег в основу нашей сервисной шины. Тот факт, что этот проект использовали другие компании добавлял уверенности в правильном выборе.
Оставался главный вопрос — как интегрировать Camel в нашего монстрика.
Муки выбора JMS против RMI.
Интеграцию сервисной шины и нашего монстрика можно было реализовать с помощью одного из множества компонентов поддерживаемых Camel-ом. Основываясь на задачах приведённых выше мы сформировали требования: связь должна быть стабильной и гарантирующей доставку сообщений. Остановились на трёх вариантах: первые два были синхронные RMI и HTTP, и один асинхронный JMS. Из трёх остановились на двух самых простых вариантах подходящих для наших проектов на Java: JMS, RMI. JMS (Java message service) — это стандарт для рассылки сообщений, он регламентирует правила отправки, получения, создания и просмотра сообщений. Второй, RMI (Remote Method Invocation) — это программный интерфейс вызова удаленных процедур, он позволяет вызывать методы одной JVM в контексте другой. Стандартная процедура удалённого вызова включает упаковку Java объектов и передачу. Справедливости ради, стоит отметить, что противопоставлять JMS и RMI не корректно потому, что JMS может быть транспортом и составной частью RMI. Мы противопоставляем стандартную реализацию RMI и реализацию JMS — ActiveMQ. Раньше RMI уже использовался нами для интеграции двух приложений. Почему его выбрали тогда? Когда оба приложения на Java, нет ничего проще RMI. Для того, чтобы его использовать, достаточно описать интерфейс и зарегистрировать объект, реализующий этот интерфейс. Но нам довелось решать проблемы возникающие с RMI при передаче большого объёма данных между приложениями, память забивалась, и приложения “складывались”. Мы искали способы решения этой проблемы и обсуждали её с разработчиками JVM на JavaOne. Выяснилось, что сборщики “мусора” в виртуальной машине и распределённые сборщики — это разные вещи. Всё упиралось в то, что для стандартного сборщика мусора можно было выбрать его тип и настроить оптимальные параметры, а для распределённого такой возможности не было. Если говорить о других отличиях, то RMI ограничивал интеграцию приложениями выполняющимися на JVM, а JMS — нет. Вдобавок к описанным трудностям было желание изучить что-то новое: отказаться от RMI и использовать альтернативное решение.
Первый прототип Camel
Давайте вернёмся к созданию прототипа. Первая практическая задача для сервисной шины была такая: пользователь инициирует процесс выгрузки данных, система их подготавливает и отправляет в сервисную шину. Вся работа по доставке данных ложится на неё. На рисунке — пример постановки задачи в символах паттернов интеграции корпоративных приложений (EIP).
Сервисная шина объединяет приём сообщений из канала JMS, их преобразование и отправку через HTTP. Отмеченные на рисунке канал JMS и канал отправки сообщений в формате HTML предполагалось реализовать используя JMS и Jetty компоненты Camel. Процесс преобразования данных можно было реализовать на Java и/или использовать шаблонизаторы такие как, например, VM (Apache Velocity). Предложенная схема передачи данных реализуется на Java DSL в одну строку. Пример:
from("jms:queue:se.export")
.setHeader(Exchange.HTTP_METHOD,constant(org.apache.camel.component.http.HttpMethods.POST))
.process( new JmsToHttpMessageConvertor() )
.inOnly("jetty:{{externalsystem.uri}}");
На примере выше приведён роут на Java DSL. Роут — это описание маршрута передачи сообщения. В Camel-е описания могут быть двух типов Java DSL и XML DSL. Характеристиками такого маршрута являются начальная и одна или несколько конечных точек, обозначенными токенами from и to, соответственно. Маршрут описывает путь сообщения от начальной до конечной точки. Если конечные точки указаны последовательно, сообщение будет передано на первую, сервисная шина дождется ответа, который затем отправит на следующую точку. Могут встречаться маршруты, выбирающие нужную конечную точку (Dynamic Router), или отправляющие сообщение сразу на несколько точек (Recipient List). Параметр токенов from и to — это строка с URI. URI представляет собой тройку параметров, состоящую из названия компонента Camel, идентификатора ресурса и параметров подключения. Давайте разберём на примере:
from(“jms:queue:se.export?timeToLife=10000”)
Это описание входной точки, которая использует компонент JMS. Компонент JMS предоставляет возможность получать данные из ресурса queue:se.export. Где queue — тип канала сообщений, может быть или очередь(queue), или тема (topic). Далее идёт название канала “se.export”. Очередь с таким именем будет создана брокером сообщений. Последняя часть URI, параметры конечной точки. “timeToLife=10000” говорит о том, что время жизни пакета составляет 10 сек.
Из примера понятно как мы планировали организовать передачу данных, в следующей статье будет больше реального кода и примеров.
Итак мы решили задачу передачи данных, создали прототип интеграционной шины который состоял из Camel, и был практически готов к внедрению. Оставалось решить задачу его правильной и удобной настройки.
Настройка прототипа
Мне очень импонирует эта тема, так как сложно найти практические советы и примеры реализации. У нас структура стендов такая:
У каждого разработчика своя копия ПО и он перед запуском её полностью настраивает. Есть два тестовых стенда для ручного тестирования, и конечно, есть рабочая система. Формулировка задачи получается следующая:
- мелкая настройка на каждом из стендов должна была выполнятся просто редактированием минимального количества параметров в конфигурационных файлах системы
- большая настройка (добавление новых функций) выполнялось незаметно для всех стендов
Вдобавок, если на стендах разработчиков была возможность использовать сборщик проектов (Maven), то на тестовых стендах и рабочем сервере такой возможности не было.
Сложностей в этой задаче масса: Camel связан с брокером JMS, что заставляет использовать разные каналы для разных стендов или разные брокеры сообщений. Мы пошли самым простым путём запуская встроенный в шину брокер ActiveMQ. В таком случае остаётся предусмотреть настройки подключений для разных серверов.
Давайте перейдём к примерам использования параметров в настройках Camel:
- Пример из файла camel-config.xml
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="locations"> <list> <value>classpath:system.properties</value> <value>classpath:smtp.properties</value> </list> </property> </bean>
В этом примере задаются два файла с настройками для Spring-а.
- Пример, в котором используются настройки, заданные двумя файлами из предыдущего примера.
<bean id="someServiceStartPolicy" class="org.apache.camel.routepolicy.quartz.CronScheduledRoutePolicy"> <property name="routeStartTime" value="${config.someService.routeStartTime}"/> <property name="routeStopTime" value="${config.someService.routeStopTime}"/> </bean>
Сами свойства задаются в строке "${config.someService.routeStartTime}"
- Пример, в котором передаются несколько файлов с настройками в контекст Camel-а
<camelContext id="rootCamelRoute" xmlns="http://camel.apache.org/schema/spring"> <propertyPlaceholder id="properties" location="smtp.properties, system.properties"/> … </propertyPlaceholder> </camelContext>
- Пример использования параметров в роутах Camel на Java DSL
from("jms:queue:email.send") .setHeader("to", simple("${headers.email}")) .setHeader("from", simple("${properties:config.smtp.from}")) .to("{{config.smtps.server}}?username={{config.smtps.user}}&password={{config.smtps.password}}&contentType=text/html&consumer.delay=60000")
Здесь используются сразу несколько способов адресации параметров. Вот они:
— в simple диалекте строкой “${properties:config.smtp.to}”
— в URI endpoint-а строкой “{{config.smtps.server}}”
Названия параметров могут быть любые, строки взяты из примера выше.
Давайте представим реальную задачу:
есть сервис, который отправляет письма через сервисную шину на сервер SMTP; такой сервис должен для рабочей системы выполнять отсылку сообщений пользователям, а для тестовой выполнять отсылку всех писем на один почтовый ящик.
Тогда эта задача трансформируется в добавление разной логики для некоторых маршрутов выполняемых на тестовой и рабочей системе.
Вот пример того как такая задача может быть решена, используя параметры в роутах Camel-а.
from("jms:queue:event.recoverypass")
.setHeader("to", isDebug() ? simple("${properties:config.smtp.to}") : simple("${headers.email}"))
.setHeader("from", simple("${properties:config.smtp.from}"))
… // some other headers
.choice()
.when( header("password").isNotNull() )
.setHeader("subject", simple("${properties:config.passwordNotify}"))
.to("velocity:vm/email/newPasswordNotify.vm")
.otherwise()
.setHeader("subject", simple("${properties:config.recoverypass}"))
.to("velocity:vm/email/recoveryPassword.vm")
.end()
.to("{{config.smtps.server}}?username={{config.smtps.user}}&password={{config.smtps.password}}&contentType=text/html&consumer.delay=60000")
Пример кода выше нужен для сервиса восстановления пароля. Реализована следующая логика: пользователь нажимает на кнопку восстановить пароль, ему отправляется письмо с временной ссылкой для генерации нового пароля, пользователь переходит по ссылке и тот же роут отправляет пользователю новый пароль по почте. Вот наверно на этом и стоит закончить первую часть, осталось только подвести итоги.
Итоги
Прототип шины был закончен: появились несколько роутов выполняющих передачу сообщений, появились конфигурационные файлы облегчающие настройку и развёртывание шины. Уже по результатам первых шагов в осваивании Camel-а можно было говорить о большом потенциале такого подхода. Простота и лаконичность написания маршрутов завораживает. Кажется, что одной строкой можно сделать всё. Но следует обратить внимание на то, что работа с роутами требует смены
На этом пока, пока. До встречи в следующей части. Напомню, она будет посвящена юзкейсам использования Camel-а.
Автор: coriollon