SOAP client
SOAP is a protocol for exchanging XML messages, often used for integration with external systems over HTTP and a WSDL contract.
The soap-client module creates client implementations for interfaces annotated with javax.jws.WebService or jakarta.jws.WebService and registers them in the application graph.
Usually, such interfaces and related JAXB classes are generated from WSDL, for example with wsdl2java.
After generation, Kora creates the client implementation and connects it to an HTTP client, XML mapping, and telemetry.
Dependency¶
Dependency build.gradle:
Module:
Dependency build.gradle.kts:
Module:
Requires an HTTP client implementation (for example http-client-jdk or http-client-ok)
and a configuration module (HOCON or YAML) to be present in the application.
When SOAP interfaces and JAXB classes are generated with the wsdl2java plugin in jakarta mode,
the required jakarta.* / JAXB runtime is already provided by the generated sources and the JDK.
In that case the transitive jakarta / Glassfish / activation dependencies of soap-client can be excluded to avoid version clashes:
build.gradle:
Description¶
The application is expected to already have interfaces annotated with javax.jws.WebService or jakarta.jws.WebService
(both annotation families are supported). They can be written manually, but are usually created from WSDL
by a separate tool, for example a Gradle plugin.
Based on such interfaces, the annotation processor (bundled in the annotation-processors artifact) creates in the same package:
- A client implementation named
$<Interface>_SoapClientImpl, registered as a@DefaultComponentin the application graph. - A module named
$<Interface>_SoapClientModuleannotated with@Module, which registers theSoapServiceConfig(tagged with@Tag(<Interface>.class)) and the client itself.
After that, the configuration and the SOAP client become available for dependency injection automatically.
How it works¶
At runtime the generated client uses the connected HttpClient and behaves as follows:
- Sends an
HTTP POSTrequest withContent-Type: text/xmlto the address from theurlconfiguration parameter. - Adds the
SOAPActionHTTPheader only whenactionis set on the method's@WebMethodannotation. - Applies the
timeoutconfiguration value as the request timeout. - Treats
HTTP 200as a successful response and unmarshals the body into the method's return type. - Treats
HTTP 500as aSOAP Faultand converts it either to a typed WSDL fault exception or toSoapFaultException. - Raises
InvalidHttpResponseSoapExceptionfor any otherHTTPstatus code. - Parses
multipart(XOP/MTOMattachment) responses automatically. - For every
@WebMethodit generates a synchronous method and a<method>Asyncmethod returningCompletionStagefor non-blocking calls.
Configuration¶
All configurations for SOAP clients are created with the soapClient prefix.
The main part of the client configuration is placed under the service name from the @WebService annotation.
The section name is selected in this order:
namefrom@WebServiceserviceNamefrom@WebServiceportNamefrom@WebService- interface name
A SOAP client named SimpleService will have the soapClient.SimpleService configuration path.
Basic configuration parameters:
- Service
URLwhere requests will be sent (required, no default). - Maximum request execution time (default not specified, optional).
Full Configuration
Example of the complete configuration described by the SoapServiceConfig class:
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"
}
}
}
}
}
- Service
URLwhere requests will be sent (required, default: not specified). - Maximum request execution time (default:
60s). - Enables module logging (default:
false). - Enables module metrics (default:
true). - Configures SLO for the DistributionSummary metric (default:
TelemetryConfig.MetricsConfig.DEFAULT_SLO). - Additional tags for metrics (default:
{}). - Enables module tracing (default:
true). - Additional attributes for tracing (default:
{}).
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
- Service
URLwhere requests will be sent (required, default: not specified). - Maximum request execution time (default:
60s). - Enables module logging (default:
false). - Enables module metrics (default:
true). - Configures SLO for the DistributionSummary metric (default:
TelemetryConfig.MetricsConfig.DEFAULT_SLO). - Additional tags for metrics (default:
{}). - Enables module tracing (default:
true). - Additional attributes for tracing (default:
{}).
Module metrics are described in the Metrics Reference section.
The configuration is described by the SoapServiceConfig interface. The url parameter is required:
if it is missing from the configuration, the application graph fails to build with a ConfigValueExtractionException
(missing value after parse). The timeout parameter defaults to 60s.
The configuration is registered in the graph under @Tag(<Interface>.class), so when a client is
constructed manually the SoapServiceConfig dependency must be resolved with that same tag.
Usage¶
After all components are created, the SOAP client becomes available for injection.
Below is an example for the SimpleService client:
Invocation¶
A generated method accepts the request type and returns the typed response.
For the SimpleService client with a test operation:
@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();
}
}
Asynchronous¶
For every @WebMethod, the generator also creates a <method>Async method returning CompletionStage<T> for non-blocking calls.
The async method is declared on the generated $<Interface>_SoapClientImpl class rather than on the WSDL interface,
so to use it you cast the injected client to the generated implementation type:
@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);
}
}
Request customization¶
SOAP clients do not use the @InterceptWith mechanism of declarative HTTP clients.
Instead, the generated $<Interface>_SoapClientImpl provides a secondary constructor that accepts a
Function<SoapEnvelope, SoapEnvelope> envelope processor. The processor is applied to the request SOAP envelope
before it is marshalled and sent — this is the extension point for adding SOAP headers (authorization, tracing,
custom elements) or otherwise transforming the outgoing envelope.
The generated implementation has two constructors:
(HttpClient, SoapClientTelemetryFactory, SoapServiceConfig)— used by the generated@DefaultComponent; appliesFunction.identity()(no changes).(HttpClient, SoapClientTelemetryFactory, SoapServiceConfig, Function<SoapEnvelope, SoapEnvelope>)— lets you supply a custom processor.
To use a custom processor, register your own factory that returns the client interface type and constructs the
implementation with the processor. Because it provides the same interface type, your factory overrides the generated
@DefaultComponent. Resolve SoapServiceConfig with @Tag(<Interface>.class) — the tag under which the generated module registers it:
@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);
}
}
}
- Any
Function<SoapEnvelope, SoapEnvelope>can be used here;SoapEnvelopeProcessors.wssAuthis a built-in one.
@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)
}
}
- Any
Function<SoapEnvelope, SoapEnvelope>can be used here;SoapEnvelopeProcessors.wssAuthis a built-in one.
A custom processor can add arbitrary SOAP headers by appending to envelope.getHeader().getAny():
Authorization¶
SoapEnvelopeProcessors.wssAuth(username, password) is a built-in processor that adds a
WS-Security UsernameToken header (Username plus a plaintext Password)
to every request envelope. Wire it exactly as shown above by passing it as the envelope processor to the client constructor.
Logging¶
When telemetry.logging.enabled is true, the client logs the full request and response SOAP envelopes (the XML bodies).
To mask or transform logged payloads (for example, to hide sensitive data), override the SoapClientLogger.SoapClientLoggerBodyMapper
component. SoapClientModule provides it as a @DefaultComponent, so a user @Component implementation replaces it:
@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)
}
}
Exception handling¶
All SOAP client failures are unchecked. Transport and HTTP errors extend the base SoapException
(a RuntimeException), so a single catch (SoapException e) handles those, or a specific subtype can be caught.
The XML marshalling/unmarshalling exceptions extend RuntimeException directly (not SoapException), so they must
be caught separately.
Main exception types:
SoapException— base unchecked exception (extendsRuntimeException) for transport andHTTPSOAPclient failures.SoapFaultException(extendsSoapException) — the server returned aSOAP Faultthat does not match a typedWSDLfault.getFault()returns aSoapFaultexposinggetFaultcode()(QName),getFaultstring(),getFaultactor(), andgetDetail().InvalidHttpResponseSoapException(extendsSoapException) — the server returned an unexpectedHTTPstatus code (anything other than200or500).SoapRequestMarshallingException(extendsRuntimeException, notSoapException) — the request envelope could not be marshalled toXML.SoapResponseUnmarshallingException(extendsRuntimeException, notSoapException) — the responseXMLcould not be unmarshalled.
When a WSDL operation declares faults (<wsdl:fault>), the generator emits typed checked exceptions annotated with @WebFault,
and the method throws them directly when the returned fault detail matches one of them. If the fault does not match any
declared type, SoapFaultException is thrown instead.
try {
var response = service.test(request);
// ... use the response
} catch (MyServiceFault e) { //(1)!
// handle a specific declared WSDL fault
} catch (SoapFaultException e) { //(2)!
SoapFault fault = e.getFault();
var code = fault.getFaultcode();
var message = fault.getFaultstring();
} catch (InvalidHttpResponseSoapException e) {
// unexpected HTTP status code
} catch (SoapException e) {
// any other transport/HTTP SOAP failure
} catch (SoapRequestMarshallingException | SoapResponseUnmarshallingException e) {
// XML (un)marshalling failure — extends RuntimeException, not SoapException
}
- Typed
@WebFaultexception generated from a<wsdl:fault>; the concrete class name comes from theWSDL. - Any
SOAP Faultthat does not match a declared typed fault.
try {
val response = service.test(request)
// ... use the response
} catch (e: MyServiceFault) { //(1)!
// handle a specific declared WSDL fault
} catch (e: SoapFaultException) { //(2)!
val fault = e.fault
val code = fault.faultcode
val message = fault.faultstring
} catch (e: InvalidHttpResponseSoapException) {
// unexpected HTTP status code
} catch (e: SoapException) {
// any other transport/HTTP SOAP failure
} catch (e: SoapRequestMarshallingException) {
// request XML marshalling failure — extends RuntimeException, not SoapException
} catch (e: SoapResponseUnmarshallingException) {
// response XML unmarshalling failure — extends RuntimeException, not SoapException
}
- Typed
@WebFaultexception generated from a<wsdl:fault>; the concrete class name comes from theWSDL. - Any
SOAP Faultthat does not match a declared typed fault.
Low-level result model¶
Internally the request engine SoapRequestExecutor returns a SoapResult, a sealed interface with two records:
SoapResult.Success(Object body) and SoapResult.Failure(SoapFault fault, String faultMessage).
The generated client maps Success to the typed response and Failure to a typed fault exception or SoapFaultException,
so you normally do not work with SoapResult directly.
Testing¶
The client can be tested with @KoraAppTest by injecting it as a @TestComponent and pointing url at a mock server.
The example below overrides SOAP_CLIENT_URL to the mock server address and invokes service.test(request), checking the typed response:
@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 {
// the mock server responds with a TestResponse envelope for the request below
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() {
// the mock server responds with a TestResponse envelope for the request below
val request = TestRequest().apply {
val1 = "1"
val2 = "2"
}
val response = service.test(request)
assertEquals("1", response.val1)
}
}
The request envelope sent to the server and the response envelope it returns look like this on the wire:
<!-- Request -->
<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>
<!-- Response -->
<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 Plugin¶
A Gradle plugin can be used as one option for creating interfaces annotated with javax.jws.WebService or jakarta.jws.WebService,
as well as JAXB classes based on WSDL.
Dependency¶
Usage¶
Suppose there is a WSDL where the SimpleService service is declared.
Then the plugin configuration for generation with jakarta annotations will look like this:
Plugin setup 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"],
]
}
Plugin setup 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"
)
)
)
}
The useJakarta = true option makes the plugin generate interfaces with jakarta.jws annotations.
Set it to false (or omit it) to generate javax.jws annotations instead — the annotation processor supports both.