Мул против верблюда

Практическое применение интеграционных платформ Mulesoft и Apache Camel

В нашей части света в последние 8-10 лет крупные бизнес компании и гос организации ведут планомерное избавление от дорогих и тяжелых IT-решений, стараясь заменить их на open source. Например, IBM и HP почти полностью свернули деятельность здесь, а Oracle бывает только проездом раз в год с гастролями. На смену им пришли более гибкие и дешевые AWS, Red Hat и Salesforce и, как ни странно, Microsoft. Типичная задача в последние годы — это миграция монолитных java приложений с дорогущих WebSphere и WebLogic на JBoss или Tomcat развернутых внутри OpenShift. Я, как девелопер, часто занимаюсь разработкой интеграционных решений. На поле интеграционных платформ на слуху в основном три имени: Mulesoft, WSO2 и Apache Camel, которые пришли на смену тяжеловесным платформам от IBM и Oracle. Мне доводилось работать с Mulesoft и Camel, и я давно хотел для себя прояснить какая же платформа мне нравится больше.

Забавно, что обе платформы используют метафору вьючного животного в своем названии, что, на мой взгляд, удачно их выделяет среди нейтрально непонятных айтишных словечек типа «fusion», «dataflow» или прямолинейного «integration». На этом сходства не кончаются. Обе платформы написаны на java с активным использованием всей мощи java open source инфраструктуры. Обе платформы предлагают свои варианты «программирования на XML» с использованием DSL (domain specific language). Базовые архитектурные концепты для интеграционных флоу также кажутся довольно схожими на первый взгляд.

Различия, тем не менее, довольно существенные. Прежде всего это различные деплоймент архитектуры. Apache Camel — это просто набор java-библиотек, которые могут быть использованы в любом java-приложении и в любом java-контейнере. Mulesoft же пошел по пути создания собственной инфраструктуры. Разработку предлагается вести с помощью графического IDE Anypoint Studio. Mule-приложение деплоится и работает внутри легковесного контейнера Mule Runtime Engine, который часто еще называют Mule ESB.

Второе важное различие — это подход к программированию. Camel развивается в направлении схожим со Spring. Можно программировать используя fluent java API, а можно конфигурировать процессы в xml. Mule тоже использует xml для кодирования процессов, но он ушел в сторону графического интерфейса, где большинство операций можно сделать кликая мышкой, получив на выходе xml-файл. Использовать java API для mule можно, но это будет сложно и долго. Mulesoft давно ушел от идеи развивать java API.

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

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

Типичная задача интеграции — реализовать интеграционный слой, который принимает запросы от первой системы, преобразует их и пересылает во вторую систему, после чего ответ от второй системы соответственно преобразуется и отправляется в первую. Обе платформы — mule и camel — предоставляют мощный инструментарий для такой задачи, который делает имплементацию быстрой и легкой. Интересно будет сравнить, как подобный груз переносит «мул», а как «верблюд».

Детализируем условия задачи:

  • приложение принимает входящие GET-запросы по адресу http://localhost:8080/test
  • в качестве входной информации сервер принимает query параметр «id», т.е. каждый запрос выглядит как http://localhost:8080/test?id=1234
  • на каждый входящий запрос приложение делает GET-запрос в сторонний REST-сервис по адресу https://restservice:9999/booking при условиях:
    • протокол взаимодействия HTTPS
    • каждый запрос должен содержать кастомный HTTP header "Source-Sytem: mule/camel"
    • значение из query параметра «id» передается как URL параметр, т.е. запрос выглядит как https://restservice:9999/booking/1234
    • используется Basic Authentication
    • доступ к сервису происходит через NTLM proxy
  • ответ от REST-сервиса отправляется без измений как ответ на исходный входящий запрос

Интеграция с помощью Apache Camel

Исходя из постановки задачи, мы понимаем, что нам нужно как минимум два ключевых компонента:

  • HTTP сервер, который будет слушать входящие запросы на порту 8080
  • HTTP клиент, который будет запрашивать информацию у стороннего сервера https://restservice:9999

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

from("jetty:http://0.0.0.0:8080/test")

С клиентом будет посложнее, так как мы хотим использовать прокси и аутентификацию. Внимательно вкуриваем документацию и обнаруживаем, что обе фичи уже поддерживается HTTP компонентом. Выглядеть это должно примерно так:

to("https://restservice:9999/booking/" +
                        "?bridgeEndpoint=true" +
                        "&proxyAuthMethod=NTLM" +
                        "&proxyAuthHost=proxy_host" +
                        "&proxyAuthPort=proxy_port" +
                        "&proxyAuthScheme=http" +
                        "&proxyAuthUsername=proxy_username" +
                        "&proxyAuthPassword=proxy_password" +
                        "&proxyAuthDomain=proxy_domain" +
                        "&authenticationPreemptive=true" +
                        "&authMethod=Basic" +
                        "&authMethodPriority=Basic" +
                        "&authUsername=username" +
                        "&authPassword=password")

Как видно, мы специфицируем прокси парамтеры и параметры базовой аутентификации. При этом с TLS не делаем ничего особенного, кроме указания протокола HTTPS.

И последний трюк — это трансформация входящего query параметра «id» в URL параметр. В camel это можно сделать с помощью манипуляций с exchange’s headers. Camel API предлагает делать это с помощью методов setHeader и removeHeader.

В итоге у меня получился довольно сжатый и лаконичный код:

import org.apache.camel.main.Main;
import org.apache.camel.Exchange;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.http.HttpMethods;


public class TestApp extends RouteBuilder {

    public static void main(String... args) throws Exception {
        Main main = new Main();
        main.configure().addRoutesBuilder(new TestApp());
        main.run(args);
    }

    public void configure() {
        from("jetty:http://0.0.0.0:8080/test")
                .setHeader(Exchange.HTTP_METHOD, constant(HttpMethods.GET))
                .setHeader("Source-System", constant("camel"))
                .setHeader(Exchange.HTTP_PATH, simple("${header.id}"))
                .removeHeader(Exchange.HTTP_QUERY)
                .to("https://restservice:9999/booking/" +
                        "?bridgeEndpoint=true" +
                        "&proxyAuthMethod=NTLM" +
                        "&proxyAuthHost=proxy_host" +
                        "&proxyAuthPort=proxy_port" +
                        "&proxyAuthScheme=http" +
                        "&proxyAuthUsername=proxy_username" +
                        "&proxyAuthPassword=proxy_password" +
                        "&proxyAuthDomain=proxy_domain" +
                        "&authenticationPreemptive=true" +
                        "&authMethod=Basic" +
                        "&authMethodPriority=Basic" +
                        "&authUsername=username" +
                        "&authPassword=password");
    }
}

Из непонятных трюков в этом коде я вижу две вещи. Во-первых, в параметрах HTTP компонента я использовал bridgeEndpoint=true, который как раз предусмотрен для подобных кейсов, когда запрос перенаправляется с одного эндпойнта на другой. В документации bridgeEndpoint описан так, что никогда не догадаешься, зачем он нужен, но если его убрать, то получим ошибку с полезным сообщением вида:

java.lang.IllegalArgumentException: Invalid uri: /test/123. 
If you are forwarding/bridging http endpoints, 
then enable the bridgeEndpoint option on the endpoint:
https://restservice:9999/booking/?

Во-вторых, то как происходит транформация query параметра в URl параметр:

.setHeader(Exchange.HTTP_PATH, simple("${header.id}"))
.removeHeader(Exchange.HTTP_QUERY)

Jetty компонент автоматически копирует query параметры в exchange’s header. С помощью simple("${header.id}") мы получаем значение хэдора с именем «id». С помощью setHeader(Exchange.HTTP_PATH, simple("${header.id}")) мы говорим, что значение хэдора «id» должно использоваться как URL параметр, то есть добавляться в path. И в завершеннии понадобилось удалить все хэдоры, пришедшие как query параметры от Jetty, с помощью removeHeader(Exchange.HTTP_QUERY). Если этого не сделать,то они автоматически добавятся как query параметры в запрос HTTP клиента.

Интеграция с помощью Mule

Работа с mule начинается с установки Anypoint Studio. Без Anypoint Studio разработка mule приложения будет мучительной и малопродуктивной.

Итак, поставили и запустили Anypoint Studio, создали новый Mule Project. В Mule Palette находим два компонента Listener(HTTP) и Request (HTTP). C помощью Listener(HTTP) мы создалим HTTP сервер. С помощью Request(HTTP) мы создадим HTTP клиент. Драг-анд-дропом создаем флоу. Должно получиться такое простое флоу:

Это, конечно же, еще не все. Нужно специфицировать наши сервер и клиент. Начнем с Listener. Находим Connector configuration в параметрах Listener и кликаем плюсик. Появляется диалог, в котором мы можем специфицировать конфигурацию HTTP Listener

После того, как прописали порт и базовый путь, в xml файле можно подглядеть, что получилось. Должно выглядеть примерно так:

<http:listener-config name="HTTP_Listener_config"
	                      doc:name="HTTP Listener config">
   <http:listener-connection host="0.0.0.0 port="8080" 
basePath="/test"/>
</http:listener-config>

<flow name="integrationFlow">
   <http:listener doc:name="Listener"
		               config-ref="HTTP_Listener_config"
		               path="/" />
</flow>

Не одна строка как в Camel, но тоже довольно просто и интуитивно понятно.

Теперь разберемся с Request(HTTP). Находим configuration в параметрах Request(HTTP) и кликаем плюсик. Получаем диалог, в котором можем настроить все необходимые нам параметры, включая TLS, прокси и аутентификацию.

После вдумчивой настройки параметров в xml файле должно получиться примерно так:

<http:request-config	name="HTTP_Request_configuration"	doc:name="HTTP Request configuration">
	<http:request-connection host="restservice" port="9999" protocol="HTTPS">
		<tls:context>
			<tls:trust-store insecure="true" />
		</tls:context>
		<http:proxy-config>
			<http:ntlm-proxy 
ntlmDomain="proxy_domain"	
host="proxy_host"	
port="proxy_port"	
username="proxy_username"	
password="proxy_password" />
		</http:proxy-config>
		<http:authentication>
			<http:basic-authentication	
username="username"	
password="password" />
		</http:authentication>
	</http:request-connection>
</http:request-config>

<flow name="integrationFlow">

	<http:request method="GET"
		              doc:name="Request"
		              config-ref="HTTP_Request_configuration"
		              path="/booking/{id}">
	</http:request>
</flow>

Здесь нужно отметить, что для TLS нужно поставить галку insecure, иначе mule будет пытаться проверить SSL-сертификат сервера restservice на валидность, а нам это не нужно.

И последний штрих — передача входного query параметра «id». Опять смотрим на параметры Request(HTTP) и находим закладку URI Parameters, где мы должны прописать имя URI параметра и его значение id=attributes.queryParams.id

Также заглянем в закладку Headers и добавим хэдер Source-Sytem=mule

Окончательный вид нашего xml будет такой:

<mule xmlns:sockets="http://www.mulesoft.org/schema/mule/sockets"
      xmlns:tls="http://www.mulesoft.org/schema/mule/tls"
      xmlns:http="http://www.mulesoft.org/schema/mule/http"
      xmlns="http://www.mulesoft.org/schema/mule/core"
      xmlns:doc="http://www.mulesoft.org/schema/mule/documentation"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.mulesoft.org/schema/mule/core http://www.mulesoft.org/schema/mule/core/current/mule.xsd
	http://www.mulesoft.org/schema/mule/http http://www.mulesoft.org/schema/mule/http/current/mule-http.xsd
	http://www.mulesoft.org/schema/mule/tls http://www.mulesoft.org/schema/mule/tls/current/mule-tls.xsd
	http://www.mulesoft.org/schema/mule/sockets http://www.mulesoft.org/schema/mule/sockets/current/mule-sockets.xsd">

	<http:listener-config name="HTTP_Listener_config"
	                      doc:name="HTTP Listener config">
		<http:listener-connection host="0.0.0.0"		                         
		                          port="8080" 
					  basePath="/test"/>
	</http:listener-config>

	<http:request-config
			name="HTTP_Request_configuration"
			doc:name="HTTP Request configuration">
		<http:request-connection
				host="restservice"
				port="9999"
				protocol="HTTPS">
			<tls:context>
				<tls:trust-store insecure="true" />
			</tls:context>
			<http:proxy-config>
				<http:ntlm-proxy 
						ntlmDomain="proxy_domain"
						host="proxy_host"
						port="proxy_port"
						username="proxy_username"
						password="proxy_password" />
			</http:proxy-config>
			<http:authentication>
				<http:basic-authentication
						username="username"
						password="password" />
			</http:authentication>
		</http:request-connection>
	</http:request-config>

	<flow name="integrationFlow">
		<http:listener doc:name="Listener"
		               config-ref="HTTP_Listener_config"
		               path="/" />

		<http:request method="GET"
		              doc:name="Request"
		              config-ref="HTTP_Request_configuration"
		              path="/booking/{id}">
			<http:headers><![CDATA[#[output application/java
---
{
	"Source-Sytem" : "mule"
}]]]></http:headers>
			<http:uri-params ><![CDATA[#[output application/java
---
{
	"id" : attributes.queryParams.id
}]]]></http:uri-params>
		</http:request>

	</flow>
</mule>

На первый вгляд выглядит очень многословно по сравнению с Camel. Однако несомненный плюс mule в том, что многие вещи можно делать интуитивно не заглядывая в в документацию.

Выводы

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

В ситуации выбора платформы помимо коммерческой составляющей я бы исходил из характеристик задачи и имеющегося бэкграунда девелоперов. Если речь идет о небольшом java проекте, где нужно быстро запилить не очень сложную интеграцию и не хочется разводить зоопарк из разных технологий и контейнеров, то тут мой выбор за Apache Camel. Его легко можно включить в Spring Boot или java standalone проект, ведь это всего лишь набор зависимостей maven. Девелоперы имеют дело с привычным java кодом. Деплоймент приложения не требует каких-то специфических ухищрений.

Если же речь идет о чем-то более амбициозном, вроде разворачивания API инфраструктуры из кучи микро сервисов взаимодействующих друг с другом, то я бы пристальнее присмотрелся к Mule. Mulesoft вложил много времени и сил в том, чтобы сделать разработку API максимально быстрой, легкой и надежной. И у них это действительно хорошо получилось. Да, придется инвестировать в обучение. Нужно разбираться с mulesoft инфраструктурой, отказаться от java и начать программировать в xml или (о ужас!) накликивать мышкой флоу в Anypoint Studio. Некоторую сложность может представлять деплоймент в новый контейнер mule runtime engine (хотя на самом деле деплоймент mule приложения предельно прост — это всего лишь копирование zip-файла в определенную директорию). В конечном итоге, это принесет хорошие плоды.

Выше были озвучены выводы, где я старался быть объективным. Теперь несколько субъективных впечатлений. В Camel мне не очень нравится насколько педантично они подошли к реализации enterprise integration patterns. В них присутствует определенный академизм, который не очень хорошо стыкуется с реальной практикой. Каждый раз, когда я что-то делаю с помощью «верблюда», я спотыкаюсь о неинтуитивность фреймворка. Меня все время преследует ощущение, что все сделано как-то неудобно и все работает не так, как я ожидал. Из-за этого довольно часто мне не удается сходу накидать работающий интеграционный флоу и приходится внимательно перечитывать документацию, чтобы заставить «верблюда» работать так, как мне надо. С Mule у меня такой проблемы не возникает, он почти всегда соответствует моим интуитивным ожиданиям. Подход к архитектуре интеграционных компонентов «мула» вполне ложится на мой опыт практика-интегратора. Не знаю уж, как там на самом деле было, но мое ощущение таково, что архитекторы mulesoft прежде всего отталкиваются от практических реалий стоящих перед интеграциями, нежели от желания соответствовать неким теоретическим стандартам. Это особенно заметно в последнем релизе Mule 4, где многие фундаментальные вещи (такие как структура Mule Message или HTTP connector) были существенно переработаны так, что их действительно стало удобнее использовать.