SOAP клиент
SOAP — это протокол обмена сообщениями в формате XML, который часто используется для интеграции с внешними системами по HTTP и контракту WSDL.
Модуль soap-client создаёт реализации клиентов для интерфейсов, помеченных аннотацией javax.jws.WebService или jakarta.jws.WebService, и регистрирует их в графе приложения.
Обычно такие интерфейсы и связанные с ними классы JAXB генерируются из WSDL, например с помощью wsdl2java.
После генерации Kora создаёт реализацию клиента и подключает её к HTTP-клиенту, преобразованию XML и телеметрии.
Подключение¶
Зависимость build.gradle:
Модуль:
Зависимость build.gradle.kts:
Модуль:
Требует наличия в приложении реализации HTTP-клиента (например http-client-jdk или http-client-ok)
и модуля конфигурации (HOCON или YAML).
Когда интерфейсы SOAP и классы JAXB генерируются плагином wsdl2java в режиме jakarta,
необходимая среда выполнения jakarta.* / JAXB уже предоставляется сгенерированными исходниками и JDK.
В этом случае транзитивные зависимости jakarta / Glassfish / activation модуля soap-client можно исключить, чтобы избежать конфликтов версий:
build.gradle:
Описание¶
Предполагается, что в приложении уже есть интерфейсы, помеченные аннотацией javax.jws.WebService или jakarta.jws.WebService
(поддерживаются оба семейства аннотаций). Их можно написать вручную, но обычно они создаются из WSDL
отдельным инструментом, например Gradle-плагином.
На основе таких интерфейсов процессор аннотаций (входящий в артефакт annotation-processors) создаёт в том же пакете:
- Реализацию клиента с именем
$<Interface>_SoapClientImpl, зарегистрированную как@DefaultComponentв графе приложения. - Модуль с именем
$<Interface>_SoapClientModule, помеченный аннотацией@Module, который регистрируетSoapServiceConfig(с тегом@Tag(<Interface>.class)) и сам клиент.
После этого конфигурация и SOAP-клиент автоматически становятся доступны для внедрения зависимостей.
Как это работает¶
Во время работы сгенерированный клиент использует подключённый HttpClient и ведёт себя следующим образом:
- Отправляет запрос
HTTP POSTсContent-Type: text/xmlна адрес из параметра конфигурацииurl. - Добавляет
HTTP-заголовокSOAPActionтолько когда в аннотации@WebMethodметода заданaction. - Применяет значение конфигурации
timeoutкак предельное время выполнения запроса. - Трактует
HTTP 200как успешный ответ и десериализует тело в тип возвращаемого значения метода. - Трактует
HTTP 500какSOAP Faultи преобразует его либо в типизированное исключение ошибки WSDL, либо вSoapFaultException. - Выбрасывает
InvalidHttpResponseSoapExceptionдля любого другого кода состоянияHTTP. - Автоматически разбирает ответы
multipart(вложенияXOP/MTOM). - Для каждого
@WebMethodгенерирует синхронный метод и метод<method>Async, возвращающийCompletionStageдля неблокирующих вызовов.
Конфигурация¶
Все конфигурации SOAP-клиентов создаются с префиксом soapClient.
Основная часть конфигурации клиента размещается под именем службы из аннотации @WebService.
Имя секции выбирается в следующем порядке:
nameиз@WebServiceserviceNameиз@WebServiceportNameиз@WebService- имя интерфейса
У SOAP-клиента с именем SimpleService путь конфигурации будет soapClient.SimpleService.
Основные параметры конфигурации:
URLслужбы, куда будут отправляться запросы (обязательный, по умолчанию не указано).- Максимальное время выполнения запроса (по умолчанию не указано, опционально).
Полная конфигурация
Пример полной конфигурации, описанной классом SoapServiceConfig:
soapClient {
SimpleService {
url = "https://localhost:8090" //(1)!
timeout = "60s" //(2)!
telemetry {
logging {
enabled = false //(3)!
}
metrics {
enabled = true //(4)!
slo = [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] //(5)!
tags = { // (6)!
"key1" = "value1"
"key2" = "value2"
}
}
tracing {
enabled = true //(7)!
attributes = { // (8)!
"key1" = "value1"
"key2" = "value2"
}
}
}
}
}
URLслужбы, куда будут отправляться запросы (обязательная, по умолчанию не указано).- Максимальное время выполнения запроса (по умолчанию:
60s). - Включает логирование модуля (по умолчанию:
false). - Включает метрики модуля (по умолчанию:
true). - Настройка SLO для метрики DistributionSummary (по умолчанию:
TelemetryConfig.MetricsConfig.DEFAULT_SLO). - Дополнительные теги для метрик (по умолчанию:
{}). - Включает трассировку модуля (по умолчанию:
true). - Дополнительные атрибуты для трассировки (по умолчанию:
{}).
soapClient:
SimpleService:
url: "https://localhost:8090" #(1)!
timeout: "60s" #(2)!
telemetry:
logging:
enabled: false #(3)!
metrics:
enabled: true #(4)!
slo: [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] #(5)!
tags: #(6)!
key1: value1
key2: value2
tracing:
enabled: true #(7)!
attributes: #(8)!
key1: value1
key2: value2
URLслужбы, куда будут отправляться запросы (обязательная, по умолчанию не указано).- Максимальное время выполнения запроса (по умолчанию:
60s). - Включает логирование модуля (по умолчанию:
false). - Включает метрики модуля (по умолчанию:
true). - Настройка SLO для метрики DistributionSummary (по умолчанию:
TelemetryConfig.MetricsConfig.DEFAULT_SLO). - Дополнительные теги для метрик (по умолчанию:
{}). - Включает трассировку модуля (по умолчанию:
true). - Дополнительные атрибуты для трассировки (по умолчанию:
{}).
Метрики модуля описаны в разделе Справочник метрик.
Конфигурация описывается интерфейсом SoapServiceConfig. Параметр url обязателен:
если он отсутствует в конфигурации, граф приложения не собирается и выбрасывается ConfigValueExtractionException
(отсутствие значения после разбора). Параметр timeout по умолчанию равен 60s.
Конфигурация регистрируется в графе под @Tag(<Interface>.class), поэтому при
ручном создании клиента зависимость SoapServiceConfig должна разрешаться с тем же тегом.
Использование¶
После создания всех компонентов SOAP-клиент становится доступен для внедрения.
Ниже приведён пример для клиента SimpleService:
Вызов¶
Сгенерированный метод принимает тип запроса и возвращает типизированный ответ.
Для клиента SimpleService с операцией test:
@Component
public final class SomeService {
private final SimpleService service;
public SomeService(SimpleService service) {
this.service = service;
}
public String call() throws Exception {
var request = new TestRequest();
request.setVal1("1");
request.setVal2("2");
TestResponse response = service.test(request);
return response.getVal1();
}
}
Асинхронность¶
Для каждого @WebMethod генератор также создаёт метод <method>Async, возвращающий CompletionStage<T> для неблокирующих вызовов.
Асинхронный метод объявляется в сгенерированном классе $<Interface>_SoapClientImpl, а не в интерфейсе WSDL,
поэтому для его использования нужно привести внедрённый клиент к типу сгенерированной реализации:
@Component
public final class SomeService {
private final SimpleService service;
public SomeService(SimpleService service) {
this.service = service;
}
public CompletionStage<String> callAsync() {
var request = new TestRequest();
request.setVal1("1");
request.setVal2("2");
return (($SimpleService_SoapClientImpl) service).testAsync(request)
.thenApply(TestResponse::getVal1);
}
}
Настройка запроса¶
SOAP-клиенты не используют механизм @InterceptWith из декларативных HTTP-клиентов.
Вместо этого сгенерированный $<Interface>_SoapClientImpl предоставляет дополнительный конструктор, принимающий обработчик
конверта Function<SoapEnvelope, SoapEnvelope>. Обработчик применяется к конверту SOAP запроса
перед его сериализацией и отправкой — это точка расширения для добавления заголовков SOAP (авторизация, трассировка,
произвольные элементы) или иного преобразования исходящего конверта.
У сгенерированной реализации два конструктора:
(HttpClient, SoapClientTelemetryFactory, SoapServiceConfig)— используется сгенерированным@DefaultComponent; применяетFunction.identity()(без изменений).(HttpClient, SoapClientTelemetryFactory, SoapServiceConfig, Function<SoapEnvelope, SoapEnvelope>)— позволяет задать собственный обработчик.
Чтобы использовать собственный обработчик, зарегистрируйте свою фабрику, которая возвращает тип интерфейса клиента и создаёт
реализацию с обработчиком. Поскольку она предоставляет тот же тип интерфейса, ваша фабрика переопределяет сгенерированный
@DefaultComponent. Разрешайте SoapServiceConfig через @Tag(<Interface>.class) — тег, под которым его регистрирует сгенерированный модуль:
@Module
public interface SoapModule {
default SimpleService simpleService(HttpClient httpClient,
SoapClientTelemetryFactory telemetryFactory,
@Tag(SimpleService.class) SoapServiceConfig config) {
var processor = SoapEnvelopeProcessors.wssAuth("username", "password"); //(1)!
try {
return new $SimpleService_SoapClientImpl(httpClient, telemetryFactory, config, processor);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
}
- Здесь можно использовать любую
Function<SoapEnvelope, SoapEnvelope>;SoapEnvelopeProcessors.wssAuth— встроенная.
@Module
interface SoapModule {
fun simpleService(httpClient: HttpClient,
telemetryFactory: SoapClientTelemetryFactory,
@Tag(SimpleService::class) config: SoapServiceConfig): SimpleService {
val processor = SoapEnvelopeProcessors.wssAuth("username", "password") //(1)!
return `$SimpleService_SoapClientImpl`(httpClient, telemetryFactory, config, processor)
}
}
- Здесь можно использовать любую
Function<SoapEnvelope, SoapEnvelope>;SoapEnvelopeProcessors.wssAuth— встроенная.
Собственный обработчик может добавлять произвольные заголовки SOAP, дописывая их в envelope.getHeader().getAny():
Авторизация¶
SoapEnvelopeProcessors.wssAuth(username, password) — встроенный обработчик, который добавляет в каждый конверт запроса заголовок
UsernameToken стандарта WS-Security (Username и Password в открытом виде).
Подключите его ровно так, как показано выше, передав его в качестве обработчика конверта в конструктор клиента.
Логирование¶
Когда telemetry.logging.enabled равно true, клиент логирует полные конверты SOAP запроса и ответа (тела XML).
Чтобы замаскировать или преобразовать логируемые данные (например, скрыть чувствительные данные), переопределите компонент SoapClientLogger.SoapClientLoggerBodyMapper.
SoapClientModule предоставляет его как @DefaultComponent, поэтому пользовательская реализация @Component заменяет его:
@Component
public final class MaskingBodyMapper implements SoapClientLogger.SoapClientLoggerBodyMapper {
@Override
public String mapRequest(String serviceName, String soapMethod, byte[] requestAsBytes) {
return "<masked/>";
}
@Override
public String mapResponseSuccess(String serviceName, String soapMethod, byte[] responseAsBytes) {
return new String(responseAsBytes, StandardCharsets.UTF_8);
}
@Override
public String mapResponseFailure(String serviceName, String soapMethod, byte[] responseAsBytes) {
return new String(responseAsBytes, StandardCharsets.UTF_8);
}
}
@Component
class MaskingBodyMapper : SoapClientLogger.SoapClientLoggerBodyMapper {
override fun mapRequest(serviceName: String, soapMethod: String, requestAsBytes: ByteArray): String {
return "<masked/>"
}
override fun mapResponseSuccess(serviceName: String, soapMethod: String, responseAsBytes: ByteArray): String {
return String(responseAsBytes, StandardCharsets.UTF_8)
}
override fun mapResponseFailure(serviceName: String, soapMethod: String, responseAsBytes: ByteArray): String {
return String(responseAsBytes, StandardCharsets.UTF_8)
}
}
Обработка исключений¶
Все ошибки SOAP-клиента являются непроверяемыми (unchecked). Транспортные и HTTP-ошибки наследуются от базового SoapException
(RuntimeException), поэтому их можно обработать одним catch (SoapException e) или перехватить конкретный подтип.
Исключения сериализации/десериализации XML наследуются напрямую от RuntimeException (а не от SoapException), поэтому их нужно
перехватывать отдельно.
Основные типы исключений:
SoapException— базовое непроверяемое исключение (наследуется отRuntimeException) для транспортных иHTTP-ошибокSOAP-клиента.SoapFaultException(наследуется отSoapException) — сервер вернулSOAP Fault, который не соответствует типизированной ошибкеWSDL.getFault()возвращаетSoapFault, предоставляющийgetFaultcode()(QName),getFaultstring(),getFaultactor()иgetDetail().InvalidHttpResponseSoapException(наследуется отSoapException) — сервер вернул неожиданный код состоянияHTTP(любой, кроме200или500).SoapRequestMarshallingException(наследуется отRuntimeException, а не отSoapException) — конверт запроса не удалось сериализовать вXML.SoapResponseUnmarshallingException(наследуется отRuntimeException, а не отSoapException) —XMLответа не удалось десериализовать.
Когда операция WSDL объявляет ошибки (<wsdl:fault>), генератор создаёт типизированные проверяемые исключения, помеченные аннотацией @WebFault,
и метод выбрасывает их напрямую, когда detail возвращённой ошибки соответствует одному из них. Если ошибка не соответствует ни одному
объявленному типу, вместо этого выбрасывается SoapFaultException.
try {
var response = service.test(request);
// ... использование ответа
} catch (MyServiceFault e) { //(1)!
// обработка конкретной объявленной ошибки WSDL
} catch (SoapFaultException e) { //(2)!
SoapFault fault = e.getFault();
var code = fault.getFaultcode();
var message = fault.getFaultstring();
} catch (InvalidHttpResponseSoapException e) {
// неожиданный код состояния HTTP
} catch (SoapException e) {
// любая другая транспортная/HTTP-ошибка SOAP
} catch (SoapRequestMarshallingException | SoapResponseUnmarshallingException e) {
// ошибка (де)сериализации XML — наследуется от RuntimeException, а не от SoapException
}
- Типизированное исключение
@WebFault, сгенерированное из<wsdl:fault>; конкретное имя класса берётся изWSDL. - Любой
SOAP Fault, не соответствующий объявленной типизированной ошибке.
try {
val response = service.test(request)
// ... использование ответа
} catch (e: MyServiceFault) { //(1)!
// обработка конкретной объявленной ошибки WSDL
} catch (e: SoapFaultException) { //(2)!
val fault = e.fault
val code = fault.faultcode
val message = fault.faultstring
} catch (e: InvalidHttpResponseSoapException) {
// неожиданный код состояния HTTP
} catch (e: SoapException) {
// любая другая транспортная/HTTP-ошибка SOAP
} catch (e: SoapRequestMarshallingException) {
// ошибка сериализации XML запроса — наследуется от RuntimeException, а не от SoapException
} catch (e: SoapResponseUnmarshallingException) {
// ошибка десериализации XML ответа — наследуется от RuntimeException, а не от SoapException
}
- Типизированное исключение
@WebFault, сгенерированное из<wsdl:fault>; конкретное имя класса берётся изWSDL. - Любой
SOAP Fault, не соответствующий объявленной типизированной ошибке.
Низкоуровневая модель результата¶
Внутри движок запросов SoapRequestExecutor возвращает SoapResult — запечатанный (sealed) интерфейс с двумя record:
SoapResult.Success(Object body) и SoapResult.Failure(SoapFault fault, String faultMessage).
Сгенерированный клиент отображает Success в типизированный ответ, а Failure — в типизированное исключение ошибки или SoapFaultException,
поэтому обычно с SoapResult напрямую работать не приходится.
Тестирование¶
Клиент можно протестировать с помощью @KoraAppTest, внедрив его как @TestComponent и указав в url адрес мок-сервера.
В примере ниже SOAP_CLIENT_URL переопределяется на адрес мок-сервера, вызывается service.test(request) и проверяется типизированный ответ:
@KoraAppTest(Application.class)
class SimpleServiceTests implements KoraAppTestConfigModifier {
@TestComponent
private SimpleService service;
@Override
public KoraConfigModification config() {
return KoraConfigModification.ofSystemProperty("SOAP_CLIENT_URL", "http://localhost:8080");
}
@Test
void testCall() throws Exception {
// мок-сервер отвечает конвертом TestResponse на запрос ниже
var request = new TestRequest();
request.setVal1("1");
request.setVal2("2");
var response = service.test(request);
assertEquals("1", response.getVal1());
}
}
@KoraAppTest(Application::class)
class SimpleServiceTests : KoraAppTestConfigModifier {
@TestComponent
lateinit var service: SimpleService
override fun config(): KoraConfigModification =
KoraConfigModification.ofSystemProperty("SOAP_CLIENT_URL", "http://localhost:8080")
@Test
fun testCall() {
// мок-сервер отвечает конвертом TestResponse на запрос ниже
val request = TestRequest().apply {
val1 = "1"
val2 = "2"
}
val response = service.test(request)
assertEquals("1", response.val1)
}
}
Конверт запроса, отправляемый на сервер, и конверт ответа, который он возвращает, в передаче по сети выглядят так:
<!-- Запрос -->
<ns2:Envelope xmlns:ns2="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns3="http://kora.tinkoff.ru/simple/service">
<ns2:Header/>
<ns2:Body>
<ns3:TestRequest>
<val1>1</val1>
<val2>2</val2>
</ns3:TestRequest>
</ns2:Body>
</ns2:Envelope>
<!-- Ответ -->
<ns2:Envelope xmlns:ns2="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns3="http://kora.tinkoff.ru/simple/service">
<ns2:Header/>
<ns2:Body>
<ns3:TestResponse>
<val1>1</val1>
</ns3:TestResponse>
</ns2:Body>
</ns2:Envelope>
Плагин wsdl2java¶
Gradle-плагин можно использовать как один из вариантов для создания интерфейсов, помеченных аннотацией javax.jws.WebService или jakarta.jws.WebService,
а также классов JAXB на основе WSDL.
Подключение¶
Использование¶
Предположим, есть WSDL, в котором объявлена служба SimpleService.
Тогда конфигурация плагина для генерации с аннотациями jakarta будет выглядеть так:
Настройка плагина build.gradle:
wsdl2java {
cxfVersion = "4.0.2"
wsdlDir = layout.projectDirectory.dir("src/main/resources/wsdl")
useJakarta = true
markGenerated = true
verbose = false
packageName = "ru.tinkoff.kora.generated.soap"
generatedSourceDir.set(layout.buildDirectory.dir("generated/sources/wsdl2java/java"))
includesWithOptions = [
"**/simple-service.wsdl": ["-wsdlLocation", "https://kora.tinkoff.ru/simple/service?wsdl"],
]
}
Настройка плагина build.gradle.kts:
wsdl2java {
cxfVersion = "4.0.2"
wsdlDir = layout.projectDirectory.dir("src/main/resources/wsdl")
useJakarta = true
markGenerated = true
verbose = false
packageName = "ru.tinkoff.kora.generated.soap"
generatedSourceDir.set(layout.buildDirectory.dir("generated/sources/wsdl2java/java"))
includesWithOptions.putAll(
mapOf(
"**/simple-service.wsdl" to listOf(
"-wsdlLocation",
"https://kora.tinkoff.ru/simple/service?wsdl"
)
)
)
}
Опция useJakarta = true заставляет плагин генерировать интерфейсы с аннотациями jakarta.jws.
Установите её в false (или опустите), чтобы вместо этого генерировать аннотации javax.jws — процессор аннотаций поддерживает оба варианта.