В прошлой статье мы рассказывали, как у нас в банке работает платформа WSO2. Мы предоставляем ее как сервис, как интеграционный слой, следим за его стабильностью, а разработкой на платформе занимаются уже команды из подразделений. Они работают на разных языках — Java, C# и т.д. — и обращаются к нам по необходимости за консультациями, потому что это не их основной стек разработки. Проанализировав обращения, мы решили сделать несколько инструкций по разработке на WSO2, которые охватили 80% всех вопросов, что приходят от команд. Теперь хотим поделиться наработками со всеми и начнем с разработки REST API-сервиса на WSO2. Бонус для самых терпеливых — в конце поста.
Для разработки на WSO2 нам потребуется:
WSO2 Integration Studio — среда разработки. На момент написания статьи актуальна версия 8.0.0, рекомендую обновиться до нее, поскольку в ней исправлено много ошибок прошлых версий. Также советую поставить все апдейты версии через Help -> Check Updates;
Oracle Java JDK 1.8;
Apache Maven;
Git + опционально Fork или Source Tree (графическая IDE);
Docker;
WSO2 Microintegrator;
Postman, JMeter, SoapUI (хотя можно обойтись curl и консолью, если удобно).
Итак, запускаем WSO2 Integration Studio. Создаем новый проект (File – New – Integration Project), указываем название проекта и выбираем необходимые модули.
Чуть подробней о модулях. Модули ESBConfig и Composite Exporter должны быть выбраны всегда, так как это основные компоненты. Registry Resources необходим, когда требуется хранить в проекте файлы WSDL/XSD/Swagger/OpenAPI и др. В принципе эти три модуля обязательны для создания REST API-сервиса.
Connector Exporter нужен, когда требуется интегрироваться с одним из внешних API, которые доступны как библиотеки (список коннекторов можно найти на сайте WSO2). Также опциональны Docker и Kubernetes, их функция понятна из названия.
Теперь указываем параметры проекта и заканчиваем предварительную настройку:
Добавляем в каждый из отмеченных ниже каталогов пустой файл с именем ".gitkeep". Это необходимо, чтобы Git добавил их все в контроль версий, несмотря на то что они пустые.
Создаем обработчики
Далее создаем обработчики для входящих запросов и ошибок. Выберите нужную папку и в контекстном меню нажмите New Sequence.
Создадим обработчик FaultSequence, который будет перехватывать исключения, по аналогии с tryCatch в языках программирования. Платформа должна обрабатывать и логировать все ошибки, а также отдавать клиенту соответствующие сообщения. В противном случае он будет отваливаться по таймауту.
Вот пример кода FaultSequence обработчика:
<sequence name="MOEX_FaultSequence" trace="disable" xmlns="http://ws.apache.org/ns/synapse">
<class description="LoggerERROR" name="ru.rosbank.mediator.LoggerMediatorERROR">
<property name="logLevel" value="ERROR"/>
<property name="ReqType" value="ERROR"/>
</class>
<propertyGroup description="setHeaders">
<property name="HTTP_SC" scope="axis2" type="STRING" value="500"/>
<property name="messageType" scope="axis2" type="STRING" value="application/json"/>
<property name="RESPONSE" scope="default" type="STRING" value="true"/>
<property action="remove" name="NO_ENTITY_BODY" scope="axis2"/>
</propertyGroup>
<header action="remove" description="Remove Header=To" name="To" scope="default"/>
<payloadFactory description="setFault" media-type="json">
<format>{"errorCode": "$1", "errorMessage": "$2"}</format>
<args>
<arg evaluator="xml" expression="$ctx:ERROR_CODE"/>
<arg evaluator="xml" expression="$ctx:ERROR_MESSAGE"/>
</args>
</payloadFactory>
<class description="InResLoggerJSON" name="ru.rosbank.mediator.LoggerMediatorJSON">
<property name="logLevel" value="INFO"/>
<property name="ReqType" value="InResponse"/>
<property name="isLogHeader" value="true"/>
<property name="isLogPayload" value="true"/>
</class>
<class description="InResLoggerTXT" name="ru.rosbank.mediator.LoggerMediatorTXT">
<property name="logLevel" value="DEBUG"/>
<property name="ReqType" value="End"/>
<property name="Payload" value="Request processed failure"/>
</class>
<respond/>
</sequence>
Далее создаем inbound-обработчик, который будет обрабатывать входящие запросы — например, выставлять параметры property, environment. В параметре ENV_SERVICE_NAME важно указать название сервиса, оно же будет использоваться в LoggerService для записи логов в отдельный файл. А в параметре onError добавить ссылку на ранее созданный обработчик ошибок, в нашем случае MOEX_FaultSequence.
LoggerService — это плагин нашей собственной разработки, его мы используем вместо стандартного плагина, который не подходит под наши требования. Вместо ClassMediator можно использовать стандартный LogMediator.
Пример InboundSequence обработчика:
<sequence name="MOEX_InboundSequence" onError="MOEX_FaultSequence" trace="disable" xmlns="http://ws.apache.org/ns/synapse">
<propertyGroup description="setProperty">
<property name="ENV_SERVICE_NAME" scope="default" type="STRING" value="MOEXService"/>
<property expression="fn:substring-after(get-property('MessageID'), 'uuid:')" name="ENV_MESSAGE_ID" scope="default" type="STRING"/>
<property expression="$axis2:TransportInURL" name="ENV_REST_URL_POSTFIX" scope="default" type="STRING"/>
<property expression="fn:substring-after($axis2:TransportInURL, '/')" name="ENV_METHOD_NAME" scope="default" type="STRING"/>
<property expression="fn:substring-after($axis2:TransportInURL, '?')" name="ENV_QUERY_PARAMS" scope="default" type="STRING"/>
<property expression="$axis2:REMOTE_ADDR" name="ENV_IP_ADDRESS" scope="default" type="STRING"/>
</propertyGroup>
<class description="InReqLoggerTXT" name="ru.rosbank.mediator.LoggerMediatorTXT">
<property name="logLevel" value="DEBUG"/>
<property name="ReqType" value="Begin"/>
<property name="Payload" value="Request accepted..."/>
</class>
</sequence>
Теперь нам нужен обработчик, который будет заносить в лог факт обработки запроса. Можно добавить здесь и другие возможности, но для примера мы ограничимся только логированием. Здесь также важно в параметре "onError" указать ссылку на ранее созданный обработчик ошибок, onError="MOEX_FaultSequence", чтобы ошибки OutboundSequence обработчика не потерялись.
Пример OutboundSequence обработчика:
<sequence name="MOEX_OutboundSequence" onError="MOEX_FaultSequence" trace="disable" xmlns="http://ws.apache.org/ns/synapse">
<class description="InResLoggerTXT" name="ru.rosbank.mediator.LoggerMediatorTXT">
<property name="logLevel" value="DEBUG"/>
<property name="ReqType" value="End"/>
<property name="Payload" value="Request processed success"/>
</class>
</sequence>
Создаем шаблоны
Поскольку мы будем создавать несколько API, шаблоны сильно упростят нам жизнь, избавив от лишней копипасты. Для начала — шаблон для вызова внешнего сервиса:
Выбираем тип шаблона Sequence Template:
Пример шаблона вызова:
<template name="MOEX_callAPI_Template" xmlns="http://ws.apache.org/ns/synapse">
<parameter defaultValue="" isMandatory="false" name="setCallEndpointName"/>
<parameter defaultValue="" isMandatory="false" name="setEndpointURL"/>
<parameter defaultValue="" isMandatory="false" name="setEndpointURL_Suffix"/>
<parameter defaultValue="" isMandatory="false" name="setQueryParams"/>
<sequence>
<property expression="get-property('file', $func:setEndpointURL)" name="ENV_ENDPOINT_URL" scope="default" type="STRING"/>
<filter regex="true" source="boolean($ctx:ENV_ENDPOINT_URL) and string-length($ctx:ENV_ENDPOINT_URL) > 0">
<then/>
<else>
<property expression="get-property('env', $func:setEndpointURL)" name="ENV_ENDPOINT_URL" scope="default" type="STRING"/>
</else>
</filter>
<switch source="boolean($func:setQueryParams) and string-length($func:setQueryParams) > 0">
<case regex="true">
<property expression="fn:concat($ctx:ENV_ENDPOINT_URL, $func:setEndpointURL_Suffix, '?', $func:setQueryParams)" name="uri.var.endpointURL_FullAddress" scope="default" type="STRING"/>
</case>
<default>
<property expression="fn:concat($ctx:ENV_ENDPOINT_URL, $func:setEndpointURL_Suffix)" name="uri.var.endpointURL_FullAddress" scope="default" type="STRING"/>
</default>
</switch>
<class description="OutReqLoggerTXT" name="ru.rosbank.mediator.LoggerMediatorTXT">
<property name="logLevel" value="DEBUG"/>
<property name="ReqType" value="OutRequest"/>
<property expression="fn:concat('sending request to endpointURL_FullAddress=', $ctx:uri.var.endpointURL_FullAddress)" name="Payload"/>
</class>
<call>
<endpoint key-expression="$func:setCallEndpointName"/>
</call>
</sequence>
</template>
В этом шаблоне предусмотрена проверка наличия конфигурационного файла (property), откуда мы берем значение версии микроинтегратора WSO2. Если файла нет, значение берется из environment. В конце мы логируем запрос и вызываем веб-сервис.
Создаем endpoint для вызова внешних сервисов
Через контекстное меню выбираем New Endpoint в соответствующей папке:
В списке предлагается много вариантов, нас интересует HTTP Endpoint:
Хорошее правило — делать столько endpoint, сколько методов у вас в REST-сервисе. Конечно, будет работать и с одним, но если он начнет тормозить, это повлияет на всё. Разные методы имеют разную частоту запросов, и если при работе через один endpoint свободные соединения забьются из-за медлительности какого-то метода, то остальные методы тоже начнут тормозить. Больше endpoint — больше гибкости в настройке системы.
Пример реализации endpoint:
<endpoint name="MOEX_Authenticate_EP" xmlns="http://ws.apache.org/ns/synapse">
<http method="get" uri-template="{uri.var.endpointURL_FullAddress}">
<timeout>
<duration>30000</duration>
<responseAction>fault</responseAction>
</timeout>
<suspendOnFailure>
<errorCodes>101500,101501,101506,101507,101508</errorCodes>
<initialDuration>1000</initialDuration>
<progressionFactor>2.0</progressionFactor>
<maximumDuration>60000</maximumDuration>
</suspendOnFailure>
<markForSuspension>
<errorCodes>101503,101504,101505</errorCodes>
<retriesBeforeSuspension>3</retriesBeforeSuspension>
<retryDelay>100</retryDelay>
</markForSuspension>
</http>
</endpoint>
Здесь важно указать метод http, шаблон адреса, на который будет отправляться запрос (та же переменная, что сформирована в шаблоне вызова). В зависимости от требований системы вы определяете таймаут и прописываете, при каких ошибках делать повторные запросы и при каких условиях помечать endpoint как недоступный. Так реализуется паттерн circuit breaker, или предохранитель.
После создания endpoint с методом get нужно создать аналогичный, но уже с методом post, put, patch, delete или другим, в зависимости от требований к сервису.
Создаем API
Подготовительная работа завершена, время создавать сам API. Выбираем New REST API и получаем несколько вариантов: создать API с нуля, генерировать через Swagger или импортировать. Для начала рассмотрим первый вариант:
Указываем параметры:
Name: MOEXService_API
Context: /moex/v{version} — сразу указываем версионирование сервиса, при отсутствии обратной совместимости это необходимо. Букву «v» добавляем для единообразия, так советует вендор.
Version: 1.0 или актуальная для вас
Вот пример API:
<api context="/moex/v1.0" name="MOEXService_API" version="1.0" version-type="context" xmlns="http://ws.apache.org/ns/synapse">
<resource methods="GET" url-mapping="/authenticate">
<inSequence>
<sequence key="MOEX_InboundSequence"/>
<class description="OutReqLoggerJSON" name="ru.rosbank.mediator.LoggerMediatorJSON">
<property name="logLevel" value="INFO"/>
<property name="ReqType" value="OutRequest"/>
<property name="isLogHeader" value="true"/>
<property name="isLogPayload" value="true"/>
</class>
<call-template description="callMOEX" onError="MOEX_FaultSequence" target="MOEX_callAPI_Template">
<with-param name="setCallEndpointName" value="MOEX_Authenticate_EP"/>
<with-param name="setEndpointURL" value="webServiceURL_MOEX_Authenticate"/>
<with-param name="setEndpointURL_Suffix" value=""/>
<with-param name="setQueryParams" value="{$ctx:ENV_QUERY_PARAMS}"/>
</call-template>
<filter description="Checking whether the response is success or failure" regex="200" source="$axis2:HTTP_SC">
<then>
<class description="OutResLoggerJSON" name="ru.rosbank.mediator.LoggerMediatorJSON">
<property name="logLevel" value="INFO"/>
<property name="ReqType" value="OutResponse"/>
<property name="isLogHeader" value="true"/>
<property name="isLogPayload" value="true"/>
</class>
</then>
<else>
<class description="OutResLoggerBINARY" name="ru.rosbank.mediator.LoggerMediatorBINARY">
<property name="logLevel" value="INFO"/>
<property name="ReqType" value="OutResponse"/>
<property name="isLogHeader" value="true"/>
<property name="isLogPayload" value="true"/>
</class>
</else>
</filter>
<loopback/>
</inSequence>
<outSequence>
<sequence key="MOEX_OutboundSequence"/>
<respond/>
</outSequence>
<faultSequence>
<sequence key="MOEX_FaultSequence"/>
</faultSequence>
</resource>
</api>
Здесь мы вызываем обработчик входящего запроса, выставляем параметры environment и логирования, логируем запрос, вызываем бэкенд через ранее созданный шаблон, затем указываем endpoint и адрес, на который отправить запрос. Все параметры вы задаете в зависимости от своего проекта.
В нашем случае мы также проверяем успех запроса. Фильтруем статус ответа, если пришел ответ "200", то все хорошо и мы логируем ожидаемый JSON, если нет, логируем все тело запроса в виде бинарных данных.
Несколько важных моментов:
Каждый тег <resource> описывает один REST-метод.
Атрибут url-mapping содержит относительный URI, по которому клиент будет обращаться к WSO2 .
В параметре SetEndpointURL указывается название переменной для вытягивания URI конечного сервиса из конфигурации.
В параметре SetEndpointURL_Suffix указывается путь к методу относительно URI конечного сервиса.
У нас нет значения переменной, куда отправить запрос, и это нормально, потому что конфигурацию мы выносим отдельно от кода. WSO2 поддерживает отдельные файлы конфигурации, где мы пропишем нужный адрес.
Конфигурационные параметры сервиса можно хранить в EnvironmentVariables или в специальном конфигурационном файле. Мы используем конфигурационный файл file.properties. Пример конфигурации для нашего сервиса:
# Адрес сервисов MOEX
webServiceURL_MOEX_Authenticate=https://passport-test.moex.com/authenticate
webServiceURL_MOEX_GetToken=https://play-api.moex.com/auth/oauth/v2/token
webServiceURL_MOEX_API=https://play-api.moex.com/client/v1/applications
Другой способ создания API — через Swagger (“Generate API using Swagger definition”). Здесь мы указываем путь к swagger-файлу и затем к модулю, который будет его хранить. В этом случае все методы подтянутся автоматически, но все равно нужно будет их настроить. Хорошая практика — указывать в API ссылку на swagger-файл, чтобы через get-запрос клиенты могли получать спецификацию напрямую из сервиса.
Деплоим сервис
WSO2 Integration Studio поставляется со встроенным микроинтегратором. Он не особо функциональный, но для отладки его возможностей достаточно. Рекомендую все таки использовать Docker-образы с последними fix-pack, так как встроенный (embedded) wso2mi в IDE не содержит никаких исправлений, что может привести к проблемам/ошибкам при разработке.
Начинаем сборку. Открывает pom-файл в модуле CompositeExporter, и выбираем, какие файлы должны попасть в сборку:
Теперь собираем проект с помощью Maven. Идем в каталог с проектом и запускаем сборку:
cd ../ADP/MOEXService/
mvn clean install
Если сборка прошла успешно, должно появиться вот такое сообщение:
Дальнейшие действия зависят от вашего конкретного проекта. Мы, например, пишем интеграционные тесты с использованием Postman и заглушки для BackendService с Wiremock. Перед коммитом стоит добавить в файл исключений ".gitignore" все target-каталоги, в нашем примере это
projects/ADP/MOEXService/MOEXServiceCompositeExporter/target/*
projects/ADP/MOEXService/MOEXServiceRegistryResources/target/*
projects/ADP/MOEXService/MOEXServiceConfigs/target/*
Требования к разработке в WSO2 Microintegrator
В качестве бонуса расскажем о требованиях к разработке, которые мы сформировали внутри команды. По опыту, их выполнение помогает предотвратить немало проблем в работе с WSO2.
В первую очередь необходимо применять повторно используемые SequenceMediator, так как это упрощает понимание и повышает читабельность кода. В проекте должны быть отдельные Sequence для:
валидации/парсинга входящего сообщения (Validation Handling), если применимо;
для обработки входящего сообщения/запроса (Input Handling), с реализацией общих механизмов для логирования и установки глобальных параметров (environment);
для обработки исходящего сообщения/ответа (Output Handling);
для обработки и формирования сообщения об ошибке (Fault/Error Handling);
для обработки некорректного метода или URL, если применимо.
Вызов внешнего API необходимо также сохранить в Sequence, что важно при использовании нескольких вызовов последовательно. Блок <faultSequence/> не должен быть пустым: обязательно нужно делать обработку ошибок в каждом сервисе.
Необходимо придерживаться единых правил именования объектов (название проекта, API, Endpoint, Sequence и др.) — у WSO2 есть отдельная таблица на эту тему, в том же документе можно найти другие хорошие советы.
Сообщения об ошибке должны иметь единую структуру в зависимости от формата обмена сервиса.
JSON:
"fault": {
"msgId": "uuid"
"code": 999,
"text": "Текст ошибки"
"exception": "Детальное сообщение об ошибке / stacktrace"
}
XML:
<fault>
<msgId>uuid</msgId>
<code>999</code>
<text>Текст ошибки</text>
<exception>Детальное сообщение об ошибке / stacktrace</exception>
</fault>
SOAP 1.1:
<soap:Fault>
<faultcode>SOAP-ENV:Server</faultcode>
<faultstring>Текст ошибки</faultstring>
<detail>
<FaultDetail>
<errorCode>999</errorCode>
<exception>Детальное сообщение об ошибке / stacktrace</exception>
<msgId>uuid</msgId>
</FaultDetail>
</detail>
</soap:Fault>
После получения ответа от Endpoint нужно проверять код ответа HTTP_SC и на основании него формировать ответ клиенту. Если код ответа не 200, 201 и 202, необходимо формировать ошибку.
Для установки параметров нужно использовать именно медиатор PropertyGroup. Медиатор Property допускается только в том случае, когда устанавливается один параметр.
При вызове Endpoint необходимо предусмотреть настройки повторных запросов в случае ошибок. В каждом сервисе обязательно наличие GET-запроса /health, который возвращает HTTP или ошибку 200.
И напоследок — ряд общих требований к процессам и оформлению:
наличие интеграционных тестов и логирования входящих/исходящих запросов ;
хранение параметров URL, Login, Password в конфигурационных файлах;
доступность документации Swagger/OpenAPI версии не ниже 3.0.0 в проекте через Registry (в REST API также нужно добавить через параметр Custom Swagger Definition Path);
указание через Registry всех схем XSD или JSONSchems, которые используются для валидации запросов или трансформации;
возможность получить документацию OpenAPI через запрос вида http://host:port/api_name?swagger.json;
возможность получить WSDL через запрос вида http://host:port/api_name?wsdl, если публикуется сервис в формате SOAP.
Надеемся, в этом списке вы найдете для себя что-нибудь полезное. В следующей статье расскажем о разработке SOAP-сервисов — там есть свои нюансы. Если есть вопросы по этому посту, будем рады ответить в комментариях.
Кстати, 9 декабря мы планируем провести онлайн-митап по IT-архитектуре. Дмитрий Зыков из Росбанка расскажет об автоматизации процессов управления архитектурой. Затем Дмитрий Бардин из Croc Code — об оценка модернизации инфраструктуры. И завершит основную программу Егор Слесаренко из Leroy Merlin с рассказом о том, как в его компании внедряют Composable Architecture. Начало митапа в 19:00, программа рассчитана часа на полтора, зарегистрироваться можно на сайте. Ждем вас!