ChrisDekker
ChrisDekker

Reputation: 1773

Make asynchronous SOAP call in Spring WebFlux

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:

  1. Construct and send the SOAP message using WebFlux' WebClient.
  2. 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.

Upvotes: 6

Views: 10724

Answers (3)

philipstanton310
philipstanton310

Reputation: 41

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.

Upvotes: 1

Serhii Pov&#237;senko
Serhii Pov&#237;senko

Reputation: 3926

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

Upvotes: 5

Nikolay Hristov
Nikolay Hristov

Reputation: 1702

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.

Upvotes: 0

Related Questions