I have a Reactive Spring Application using WebFlux with a REST API. Whenever a user calls my API, I need to make a call to a SOAP service which exposes a WSDL, perform some operation and return the result.
How do I combine this call to a SOAP service with the Reactive WebFlux framework?
The way I see it, I can do it 2 different ways:
Construct and send the SOAP message using WebFlux' WebClient.
Wrapping a synchronous call using WebServiceGatewaySupport in a Mono / Flux.
The first approach has my preference, but I don't know how to do that.
Similar questions have been asked here:
Reactive Spring WebClient - Making a SOAP call, which refers to this blog post (https://blog.godatadriven.com/jaxws-reactive-client). But I could not get that example to work.
Using wsdl2java in a Gradle plugin I can create a client interface with asynchronous methods, but I don't understand how to use this. When using the WebServiceGatewaySupport I don't use that generated interface or its methods at all. Instead, I call the generic marshalSendAndReceive method
public class MySoapClient extends WebServiceGatewaySupport {
public QueryResponse execute() {
Query query = new ObjectFactory().createQuery();
// Further create and set the domain object here from the wsdl2java generated classes
return (QueryResponse) getWebServiceTemplate().marshalSendAndReceive(query);
}
}
Can anyone share a complete example going from a WebFlux controller to making a SOAP call and returning asynchronously? I feel like I am missing something crucial.
I had the same aim but without having WSDL file. As an input I had endpoint and XSD file that defines request's scheme that I should to send. Here is my piece of code.
First let's define our SOPA WebClient bean (to avoid creating it each time when we want to make a call)
#Bean(name = "soapWebClient")
public WebClient soapWebClient(WebClient.Builder webClientBuilder) {
String endpoint = environment.getRequiredProperty(ENDPOINT);
log.info("Initializing SOAP Web Client ({}) bean...", endpoint);
return webClientBuilder.baseUrl(endpoint)
.defaultHeader(CONTENT_TYPE, "application/soap+xml")
//if you have any time limitation put them here
.clientConnector(getWebClientConnector(SOAP_WEBCLIENT_CONNECT_TIMEOUT_SECONDS, SOAP_WEBCLIENT_IO_TIMEOUT_SECONDS))
//if you have any request/response size limitation put them here as well
.exchangeStrategies(ExchangeStrategies.builder()
.codecs(configurer -> configurer.defaultCodecs()
.maxInMemorySize(MAX_DATA_BUFFER_SIZE))
.build())
.build();
}
public static ReactorClientHttpConnector getWebClientConnector(int connectTimeoutSeconds, int ioTimeoutSeconds) {
TcpClient tcpClient = TcpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeoutSeconds * 1000)
.doOnConnected(conn -> conn.addHandlerLast(new ReadTimeoutHandler(ioTimeoutSeconds))
.addHandlerLast(new WriteTimeoutHandler(ioTimeoutSeconds)));
return new ReactorClientHttpConnector(HttpClient.from(tcpClient));
}
And now you can use the client to make SOAP calls like this:
#Slf4j
#Component
public class SOAPClient {
private final WebClient soapWebClient;
public SOAPClient(#Qualifier("soapWebClient") WebClient soapWebClient) {
this.soapWebClient = soapWebClient;
}
public Mono<Tuple2<HttpStatus, String>> send(String soapXML) {
return Mono.just("Request:\n" + soapXML)
.doOnNext(log::info)
.flatMap(xml -> soapWebClient.post()
.bodyValue(soapXML)
.exchange()
.doOnNext(res -> log.info("response status code: [{}]", res.statusCode()))
.flatMap(res -> res.bodyToMono(String.class)
.doOnNext(body -> log.info("Response body:\n{}", body))
.map(b -> Tuples.of(res.statusCode(), b))
.defaultIfEmpty(Tuples.of(res.statusCode(), "There is no data in the response"))))
.onErrorResume(ConnectException.class, e -> Mono.just(Tuples.of(SERVICE_UNAVAILABLE, "Failed to connect to server"))
.doOnEach(logNext(t2 -> log.warn(t2.toString()))))
.onErrorResume(TimeoutException.class, e -> Mono.just(Tuples.of(GATEWAY_TIMEOUT, "There is no response from the server"))
.doOnEach(logNext(t2 -> log.warn(t2.toString()))));
}
}
An important thing to mention here is that your soapXML should be in the format that defined by SOAP protocol obviously. To be more specific the message at least should starts and ends with soap:Envelope tag and consist all other data inside. Also, pay attention what version of SOAP protocol you are about to use as it defines what tags are allowed to use within the envelop and what not. Mine was 1.1 and here is specification for it
https://www.w3.org/TR/2000/NOTE-SOAP-20000508/#_Toc478383494
cheers
After lots of pain and trouble I found a decent solution to this problem. Since a wsdl file is provided, you should visit this site: : https://www.wsdl-analyzer.com
you can input a wsdl file and view all operations of the soap service. once you find the desired operation you want to call, click on it, and it will show an example request in xml. Some how, you have to generate this xml to make the request. There are many methods to do so, and some are more complicated than others. I found that manual serialization works well, and is honestly easier than using libraries.
say you have an operation request like this:
<s11:Envelope>
<s11:body>
<s11:operation>
<ns:username>username</ns:username>
<ns:password>password</ns:password>
</sll:operation>
</s11:body>
<s11:Envelope>
then you would generate by
public String gePayload(String username, String password) {
StringBuilder payload = new Stringbuilder();
payload.append("<s11:Envelope><s11:body><s11:operation>");
payload.append("<ns:username>");
payload.append(username);
payload.append("</ns:username>");
payload.append("<ns:password>");
payload.append(password);
payload.append("</ns:password>");
payload.append("</s11:operation></s11:body></s11:Envelope>");
return payload.toString()
}
then the web calls
public String callSoap(string payload) {
Webclient webclient = Webclient.builder()
// make sure the path is absolute
.baseUrl(yourEndPoint)
.build()
return WebClient.post()
.contentType(MediaType.TEXT_XML)
.bodyValue(payload)
.retrieve()
.bodyToMono(String.class)
.block();
}
it is important that you specify the content type is xml, and that the class returns a string. web flux cannot easily convert xml to user defined classes. so you do have to preform manual parsing. You can specify jaxb2xmlEncoders and jaxb2xmlDecoders to endcode/decode a specific class, but I found this to be to complicated. the payload has to match the request format generated by wsdl analyzer, and getting the encoders/decoders to match that format can be a task of its own. you can further research these encoders if you want, but this method will work.
I'm facing the same problem for a week and still can't find the best solution.
If you want to test the WebClient you just need to post a string with the SOAP Envelope request. Something like that:
String _request = "<soap:Envelope xmlns:soap=\"http://www.w3.org/2003/05/soap-envelope\">\n" +
"<soap:Body>\n" +
"<request>\n" +
"<Example>blabla</Example>\n" +
"</request>\n" +
"</soap:Body>\n" +
"</soap:Envelope>";
WebClient webClient = WebClient.builder().baseUrl("http://example-service").build();
Mono<String> stringMono = webClient.post()
.uri("/example-port")
.body(BodyInserters.fromObject(_request))
.retrieve()
.bodyToMono(String.class);
stringMono.subscribe(System.out::println);
The problem is that you need to figure out how to serialize the whole SOAP Envelope (request and response) to a string.
This is only an example - not a solution.
Related
I have a microservice architecture, where one service acts as a proxy, and must only forward the uploaded form data payload to the downstream service using restTemplate, preferably without loading anything from the request on disk or into memory.
I managed to resolve the issue taking the following steps.
Here I will describe the approaches, and the limitations used:
I have the following rest template configuration:
#Bean
public RestTemplate myRestTemplate() {
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
requestFactory.setBufferRequestBody(false);
RestTemplate restTemplate = new RestTemplate(requestFactory);
restTemplate.setInterceptors(new ArrayList<>()); // to avoid interceptors loading data into memory
return restTemplate;
}
in my controller I am processing the HttpServletRequest directly using Apache Commons FileUpload Streaming Api with one asterix:
Special care on the multipart form data, so first the form fields are processed in the while loop, and then only one file was I able to process, since:
FileItemStream fileItemStream = uploadItemIterator.next();
return fileItemStream.openStream();
must be returned without invoking itemIterator.hasNext(), because that will result in FileItemStream.ItemSkippedException
which works wonderfully, no data is saved on disk
c:\Users\myuser\AppData\Local\Temp\tomcat.11416588345568217859.8077\
note: I have set the following property as stated in the documentation.
spring.application.servlet.multipart.enabled: false
From here, Using the streaming api I have an inputStream, which I will pass further down to create my HttpEntity as follows (simplified in example, full inspiration to include filename in request: here):
MultiValueMap<String, Object> multiPartBody = new LinkedMultiValueMap<>();
multiPartBody.add(FILE, inputStream);
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(multiPartBody, myHeaders);
after this, I do make the call to my rest Template:
myRestTemplate.postForEntity(url, requestEntity, MyResponse.class);
this goes all the way via the following sequence:
RestTemplate.doExecute()
HttpAccessor.createRequest()
HttpComponentsClientHttpRequestFactory.createRequest() -> which will return a **HttpComponentsStreamingClientHttpRequest** <- this one is important
RestTemplate.doWithRequest(ClientHttpRequest httpRequest) -> calls: ((HttpMessageConverter<Object>) messageConverter).write(
requestBody, requestContentType, httpRequest);
FormHttpMessageConverter.write()
FormHttpMessageConverter.writeMultipart() -> where outputMessage instanceof StreamingHttpOutputMessage is true
HttpComponentsStreamingClientHttpRequest.executeInternal -> creates a new StreamingHttpEntity(...)
after which this goes down on InternalCLientExecution, and in execChain
sooner or later it will enter in the chain:
HttpComponentsStreamingClientHttpRequest.StreamingHttpEntity.writeTo(OutputStream outputStream) throws IOException {
this.body.writeTo(outputStream);
}
where body is a FormHttpMessageConverter.lambda from above:
if (outputMessage instanceof StreamingHttpOutputMessage streamingOutputMessage) {
streamingOutputMessage.setBody(outputStream -> {
writeParts(outputStream, parts, boundary);
writeEnd(outputStream, boundary);
});
}
so we get further down, and end up in:
FormHttpMessageConverter.writeParts()
FormHttpMessageConverter.writePart()
here a multipartMessage is composed and passed further down (or invoked the superclass AbstractHttpMessageConverter method)
multipartMessage = new MultipartHttpOutputMessage(os, charset);
...
((HttpMessageConverter<Object>) messageConverter).write(partBody, partContentType, multipartMessage);
from here we get into AbstractHttpMessageConverter.write where condition
if (outputMessage instanceof StreamingHttpOutputMessage)
evaluates to false because MultipartHttpOutputMessage is not an instance of StreamingHttpOutputMessage
But this seems not to affect anything, since the whole thing is invoked in the above mentioned lambda, sooner or later, we need to write the bytes from the inputStream into the outputStream.
one impediment:
if I configure the restTemplate as follows:
#Bean
#org.springframework.cloud.client.loadbalancer.LoadBalanced
public RestTemplate myRestTemplate() {
...
}
there is an interceptor/aspect overriding the RestTemplate HttpComponentsClientHttpRequestFactory with RibbonClientHttpRequestFactory (using spring netflix stack), which does not support setBufferRequestBody(false).
That is how I managed to solve the file streaming issue, hope it helps others too:
Limitations/Constraints:
You cannot use MultipartFile in your controllers since spring by default saves data into temp files on fileSystem (can't use resolve-lazily either: because), I was able to overcome this issue only with Apache Commons FileUpload
Using Apache Commons FileUpload I managed to process only one file, and the form data need to be processed before the file data
spring.application.servlet.multipart.enabled: false -> affects other endpoints too
composing downstream form data with correct Content-Disposition: form-data; name="file"; filename="my.txt" needs some strange embedded HttpEntity constructions
#LoadBalanced overrides the whole restTemplate requestFactory
Good luck everyone, and any feedback is welcome.
Let's say that my wsdl file says that this endpoint returns String but I want them to return Future or Mono.
When I just changed the returned type this is what I've got:
<faultstring xml:lang="en">No adapter for endpoint [public reactor.core.publisher.Mono<java.lang.String> example.QuestionEndpoint.questionEndpoint(java.lang.String)]: Is your endpoint annotated with #Endpoint, or does it implement a supported interface like MessageHandler or PayloadEndpoint?</faultstring>
I've searched a lot of articles and forums and found only solutions for SOAP client.
#Endpoint
public class QuestionEndpoint {
#PayloadRoot(namespace = http://stack.org/, localPart = "Question")
#ResponsePayload
public String questionEndpoint(#RequestPayload String request) {
return facade.getQuestion(request)
.flatMap(result -> Mono.just(mapper.toDto(result)))
.block();;
}
}
In my code below endpoint I'm using Mono from 'reactor.core' and doing block() in endpoint just before return the result. Is it enough to work this endpoint in a reactive way?
If it's not enough, Is Spring Boot WS SOAP endpoint able to return some reactive type, even if my WSDL file require specific type?
In REST API it's kinda easy to implement it in a reactive way.
I'm trying to create a ClientResponse in test and use it for testing a service, which also does deserialization with standard way response.bodyToMono(..class..). But it appears that there is something wrong in the way I build a fake client response. Because I receive UnsupportedMediaTypeException in tests.
Nevertheless the same code work fine in runtime SpringBoot app, when WebClient returns ClientResponse (which is built internally).
Let's see at the simplest case hich fails with
org.springframework.web.reactive.function.UnsupportedMediaTypeException:
Content type 'application/json' not supported for bodyType=java.lang.String[]
void test()
{
String body = "[\"a\", \"b\"]";
ClientResponse response = ClientResponse.create(HttpStatus.OK)
.header(HttpHeaders.CONTENT_TYPE,
MediaType.APPLICATION_JSON_VALUE)
.body(body)
.build();
String[] array = response.bodyToMono(String[].class).block();
assertEquals(2, array.length);
}
Please, help me to undeerstand, how the client response should be build to allow a standard (json -> object) deserialization in test environment.
A ClientResponse created manually does not have access to Jackson2Json exchange strategies in default list. Probably it could be configured with Spring auto-configuration, which is turned off in tests without Spring context.
Here is the straightforward way to force (de)serialization String <-> json:
static ExchangeStrategies jacksonStrategies()
{
return ExchangeStrategies
.builder()
.codecs(clientDefaultCodecsConfigurer ->
{
clientDefaultCodecsConfigurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(new ObjectMapper(), MediaType.APPLICATION_JSON));
clientDefaultCodecsConfigurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(new ObjectMapper(), MediaType.APPLICATION_JSON));
}).build();
}
Then use it in the create function
ClientResponse.create(HttpStatus.OK, jacksonStrategies())...
I want to make an HTTP request with Quarkus. However, the target URL is not known at compilation time, it will be composed from different parts at runtime.
Quarkus provides a way to build static REST clients like this:
#Path("/v2")
#RegisterRestClient
public interface CountriesService {
#GET
#Path("/name/{name}")
#Produces("application/json")
Set<Country> getByName(#PathParam String name);
}
However, I am loking for something like the Python requests package:
url = 'https://stackoverflow.com/questions/ask'
response = requests.get(url)
I am building the application in Kotlin, so all Java and Kotlin libraries should work.
What should I use?
With the MP REST Client defined in an interface, you can use the programmatic client creation API:
CountriesService remoteApi = RestClientBuilder.newBuilder()
.baseUri("created at runtime url")
.build(CountriesService.class);
remoteApi.getByName("");
In Spring Boot 1.5.x, I could use interceptors with AsyncRestTemplate to grab headers from an incoming request to a RestController endpoint and put them in any exchange requests made via the AsyncRestTemplate.
I don't see how this can work with the WebClient. It looks like if you build a WebClient that all its headers, etc are set and unchangeable:
WebClient client = WebClient.builder()
.baseUrl( "http://blah.com" )
.defaultHeader( "Authorization", "Bearer ey..." )
.build();
While I can change these using client.mutate(), that instantiates a completely new WebClient object. I'd prefer not to have to create a new one on every request. Is there no way to keep a WebClient and have per-request headers and other parameters?
It seems like a big waste and poor performance to force creating a new object every time.
What you're using here are the default headers that should be sent for all requests sent by this WebClient instance. So this is useful for general purpose headers.
You can of course change the request headers on a per-request basis like this:
Mono<String> result = this.webClient.get()
.uri("/greeting")
.header("Something", "value")
.retrieve().bodyToMono(String.class);
If you wish to have an interceptor-like mechanism to mutate the request before sending it, you can configure the WebClient instance with a filter:
WebClient
.builder()
.filter((request, next) -> {
// you can mutate the request before sending it
ClientRequest newRequest = ClientRequest.from(request)
.header("Something", "value").build();
return next.exchange(newRequest);
})
Please check out the Spring Framework documentation about WebClient.