Разработка REST API-сервиса на платформе WSO2

Kate

Administrator
Команда форума
Общая схема взаимодействия между архитектурными слоями
Общая схема взаимодействия между архитектурными слоями
Для разработки на 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), указываем название проекта и выбираем необходимые модули.

fbcc6d2a4090d733a4a4330aff510219.png

Чуть подробней о модулях. Модули ESBConfig и Composite Exporter должны быть выбраны всегда, так как это основные компоненты. Registry Resources необходим, когда требуется хранить в проекте файлы WSDL/XSD/Swagger/OpenAPI и др. В принципе эти три модуля обязательны для создания REST API-сервиса.

Connector Exporter нужен, когда требуется интегрироваться с одним из внешних API, которые доступны как библиотеки (список коннекторов можно найти на сайте WSO2). Также опциональны Docker и Kubernetes, их функция понятна из названия.

Теперь указываем параметры проекта и заканчиваем предварительную настройку:

818e4133b422798e4f4c995179ca1a54.png

Добавляем в каждый из отмеченных ниже каталогов пустой файл с именем ".gitkeep". Это необходимо, чтобы Git добавил их все в контроль версий, несмотря на то что они пустые.

32a18426237f7620c19817393de90065.png

Создаем обработчики​

Далее создаем обработчики для входящих запросов и ошибок. Выберите нужную папку и в контекстном меню нажмите New Sequence.

b4cbc8c7f2c990fb200426bf201c0af9.png

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

bbb0b6c99709c43669cd408aa96697bc.png

Вот пример кода 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.

edff3cfb3587bd28ec906ac65d3083bb.png

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 обработчика не потерялись.

ce0d8428130b30e4e739826d85bcf233.png

Пример 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, шаблоны сильно упростят нам жизнь, избавив от лишней копипасты. Для начала — шаблон для вызова внешнего сервиса:

5ac2a8c0ec660f3842bd92097a80ada3.png

Выбираем тип шаблона Sequence Template:

a30fafa9dc3150dbb34e8a8f76b711cb.png

Пример шаблона вызова:

<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 в соответствующей папке:

b90c9a0639e0bc2873d826a61ccce556.png

В списке предлагается много вариантов, нас интересует HTTP Endpoint:

fc9784156927e9ebb7bba4c5fae75f3d.png

Хорошее правило — делать столько 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 или импортировать. Для начала рассмотрим первый вариант:

799794e84ee9983d241e3ca73134aa0e.png

Указываем параметры:

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, и выбираем, какие файлы должны попасть в сборку:

6590910360a66cce76f256dcd6fe5de2.png

Теперь собираем проект с помощью Maven. Идем в каталог с проектом и запускаем сборку:

cd ../ADP/MOEXService/
mvn clean install
Если сборка прошла успешно, должно появиться вот такое сообщение:

2c9131d47a727b86a7c64c1eab059cf3.png

Дальнейшие действия зависят от вашего конкретного проекта. Мы, например, пишем интеграционные тесты с использованием 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.

 
Сверху