25

I am looking to make a SOAP call from spring reactive webclient. I couldn't find any documentation for it. Wondering what would the approach. Right now I am thinking

  1. Construct the SOAP message using JAXB on a separate thread pool
  2. Make the call by converting it to string via webclient
  3. Do convert back into java using jaxb on the way back on separate tp.

What are the downsides and any other approaches?

Faisal Masood
  • 271
  • 1
  • 3
  • 7

2 Answers2

8

Here is a working example with Spring Reactor: https://github.com/gungor/spring-webclient-soap

You need to enclose your generated JAXB classes in a soap envelope with a custom encoder as below then add it to WebClient's exchange strategies.

package webclient.soap.encoding;

import org.reactivestreams.Publisher;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.CodecException;
import org.springframework.core.codec.Encoder;
import org.springframework.core.codec.EncodingException;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.PooledDataBuffer;
import org.springframework.util.ClassUtils;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
import org.springframework.ws.WebServiceMessage;
import org.springframework.ws.WebServiceMessageFactory;
import org.springframework.ws.client.core.WebServiceTemplate;
import org.springframework.ws.support.DefaultStrategiesHelper;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.MarshalException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

public class Jaxb2SoapEncoder implements Encoder<Object> {

    private final JaxbContextContainer jaxbContexts = new JaxbContextContainer();

    @Override
    public boolean canEncode(ResolvableType elementType, MimeType mimeType) {
        Class<?> outputClass = elementType.toClass();
        return (outputClass.isAnnotationPresent(XmlRootElement.class) ||
                    outputClass.isAnnotationPresent(XmlType.class));

    }

    @Override
    public Flux<DataBuffer> encode(Publisher<?> inputStream, DataBufferFactory bufferFactory, ResolvableType elementType, MimeType mimeType, Map<String, Object> hints) {
        return Flux.from(inputStream)
                .take(1)
                .concatMap(value -> encode(value, bufferFactory, elementType, mimeType, hints))
                .doOnDiscard(PooledDataBuffer.class, PooledDataBuffer::release);
    }

    @Override
    public List<MimeType> getEncodableMimeTypes() {
        return Arrays.asList( MimeTypeUtils.TEXT_XML );
    }



    private Flux<DataBuffer> encode(Object value ,
                                    DataBufferFactory bufferFactory,
                                    ResolvableType type,
                                    MimeType mimeType,
                                    Map<String, Object> hints){

        return Mono.fromCallable(() -> {
            boolean release = true;
            DataBuffer buffer = bufferFactory.allocateBuffer(1024);
            try {
                OutputStream outputStream = buffer.asOutputStream();
                Class<?> clazz = ClassUtils.getUserClass(value);
                Marshaller marshaller = initMarshaller(clazz);

                // here should be optimized
                DefaultStrategiesHelper helper = new DefaultStrategiesHelper(WebServiceTemplate.class);
                WebServiceMessageFactory messageFactory = helper.getDefaultStrategy(WebServiceMessageFactory.class);
                WebServiceMessage message = messageFactory.createWebServiceMessage();

                marshaller.marshal(value, message.getPayloadResult());
                message.writeTo(outputStream);

                release = false;
                return buffer;
            }
            catch (MarshalException ex) {
                throw new EncodingException(
                        "Could not marshal " + value.getClass() + " to XML", ex);
            }
            catch (JAXBException ex) {
                throw new CodecException("Invalid JAXB configuration", ex);
            }
            finally {
                if (release) {
                    DataBufferUtils.release(buffer);
                }
            }
        }).flux();
    }


    private Marshaller initMarshaller(Class<?> clazz) throws JAXBException {
        Marshaller marshaller = this.jaxbContexts.createMarshaller(clazz);
        marshaller.setProperty(Marshaller.JAXB_ENCODING, StandardCharsets.UTF_8.name());
        return marshaller;
    }
}

WebClient config

@Bean
    public WebClient webClient(){
        TcpClient tcpClient = TcpClient.create();

        tcpClient
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
                .doOnConnected(connection -> {
                    connection.addHandlerLast(new ReadTimeoutHandler(5000, TimeUnit.MILLISECONDS));
                    connection.addHandlerLast(new WriteTimeoutHandler(5000, TimeUnit.MILLISECONDS));
                });

        ExchangeStrategies exchangeStrategies = ExchangeStrategies.builder().codecs( clientCodecConfigurer -> {
            clientCodecConfigurer.customCodecs().encoder(new Jaxb2SoapEncoder());
        }).build();

        WebClient webClient = WebClient.builder()
                .clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient).wiretap(true)))
                .exchangeStrategies( exchangeStrategies )
                .build();

        return webClient;
    }

WebClient

public void call(GetCountryRequest getCountryRequest) throws SOAPException, ParserConfigurationException, IOException {

        webClient.post()
                .uri( soapServiceUrl )
                .contentType(MediaType.TEXT_XML)
                .body( Mono.just(getCountryRequest) , GetCountryRequest.class  )
                .retrieve()
                .onStatus(
                        HttpStatus::isError,
                        clientResponse ->
                                clientResponse
                                        .bodyToMono(String.class)
                                        .flatMap(
                                                errorResponseBody ->
                                                        Mono.error(
                                                                new ResponseStatusException(
                                                                        clientResponse.statusCode(),
                                                                        errorResponseBody))))

                .bodyToMono(GetCountryResponse.class)
                .doOnSuccess( (GetCountryResponse response) -> {
                    //handle success
                })
                .doOnError(ResponseStatusException.class, error -> {
                    //handle error
                })
                .subscribe();

    }
gungor
  • 393
  • 3
  • 13
  • Encoder is a cool thing, thank you for a good idea. But I believe you should also configure the decoder, because the answer also will be a `SOAP` message, and `webClient` has no idea how to map it into your object.s – Frankie Drake Aug 28 '20 at 19:26
  • Actually, org.springframework.http.codec.xml.Jaxb2XmlDecoder handles Jaxb decoding. It exists in default exchange strategies of WebClient. Jaxb2XmlDecoder uses com.sun.xml.bind.v2.runtime.unmarshaller.UnmarshallerImpl to decode SOAP xml to Jaxb object. – gungor Aug 28 '20 at 20:21
  • I tried it but received an error saying that unexpected element `Envelope` occurred – Frankie Drake Aug 30 '20 at 12:27
  • Did you have a look at the example: https://github.com/gungor/spring-webclient-soap ? – gungor Aug 30 '20 at 12:37
  • Yes, and, as I can see, you don't use the decoder at all... – Frankie Drake Aug 30 '20 at 12:52
  • 1
    Now Jaxb2SoapDecoder is added, it seems Jaxb2XmlDecoder does not work with different versions of jaxb-runtime – gungor Oct 30 '20 at 20:21
  • 1
    @gungor When i test with an api that returns a long response i have this error `org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer : 262144` – Aymen Kanzari Jan 10 '23 at 14:38
  • @AymenKanzari Did you try increasing buffer size both in Jaxb2SoapEncoder and Jaxb2SoapDecoder. DataBuffer buffer = bufferFactory.allocateBuffer(1024); – gungor Jan 12 '23 at 06:00
  • @gungor i change it in the `Jaxb2SoapEncoder` class `bufferFactory.allocateBuffer(10000000)` but it's not working, i don't found the `bufferFactory` in the `Jaxb2SoapDecoder` class – Aymen Kanzari Jan 12 '23 at 12:15
  • @gungor https://stackoverflow.com/questions/74969128/webclient-exceeded-limit-on-max-bytes-to-buffer – Aymen Kanzari Jan 12 '23 at 12:19
  • @gungor In `encode` you have in: `Flux.from(inputStream).take(1)` ? Why `take(1)` ? – Mikhail Geyer Apr 28 '23 at 08:09
7

You need to generate SOAP client as the stub classes with methods for asynchronous. JAX-WS API supports asynchronous invocation. Use wsiimport with enableAsyncMapping for generating method operationAsync(Input request, AsyncHandler asyncHandler);

AsyncHandler create using Mono.create()

Service service = new Service();
ServicePortType portType = service.getPortType();

public Mono<Output> operation(Input input) {
            return Mono.create(sink ->
               portType.operation(input, outputFuture -> {
                   try {
                       sink.success(outputFuture.get());
                   } catch (Exception e) {
                       sink.error(e);
                   }
               })
            );
        }

and you get Mono reactivly

I have found suggest in the post https://blog.godatadriven.com/jaxws-reactive-client

  • 3
    A link to a solution is welcome, but please ensure your answer is useful without it: [add context around the link](//meta.stackexchange.com/a/8259) so your fellow users will have some idea what it is and why it’s there, then quote the most relevant part of the page you're linking to in case the target page is unavailable. [Answers that are little more than a link may be deleted.](//stackoverflow.com/help/deleted-answers) – Zoe Nov 05 '18 at 19:46
  • 4
    This solution doesn't use Spring Reactor's WebClient. – Andras Hatvani Apr 18 '19 at 12:26
  • 1
    external link saved at http://web.archive.org/web/20200303110721/https://blog.godatadriven.com/jaxws-reactive-client – Oleg Ushakov Mar 03 '20 at 11:08