Developer Discoveries

Soap call with Spring WebFlux

As reactive becoming more used by developers in order to get performance advantages, you will still need to deal with legacy systems. There are still many soap web services being used today. If you want to consume soap services with Spring Reactor WebFlux, here is the example. https://github.com/gungor/spring-webclient-soap.git

If you can consume Rest services using Json, it is not a big deal consuming soap. Generated JAXB classes needs to be enclosed by Soap envelope. Therefore we need an encoder class. We also need a decoder class in order to unmarshal received xml into desired JAXB object. Below is the encode method of Jaxb2SoapEncoder class which you can find in the Git repository. I have modifed Jaxb2XmlEncoder minimally to make a soap envelope from the JAXB object. JAXB classes are generated by jaxb2-maven-plugin of org.codehaus.mojo.

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();
    }

With Jaxb2SoapEncoder, we can handle how request is sent through webflux.

Below is decode method of Jaxb2SoapDecoder.

@Override
    @SuppressWarnings({"rawtypes", "unchecked", "cast"})  // XMLEventReader is Iterator<Object> on JDK 9
    public Object decode(DataBuffer dataBuffer, ResolvableType targetType,
                         @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) throws DecodingException {

        try {
            DefaultStrategiesHelper helper = new DefaultStrategiesHelper(WebServiceTemplate.class);
            WebServiceMessageFactory messageFactory = helper.getDefaultStrategy(WebServiceMessageFactory.class);
            WebServiceMessage message = messageFactory.createWebServiceMessage( dataBuffer.asInputStream() );
            return unmarshal(message, targetType.toClass());
        }
        catch (Throwable ex) {
            ex = (ex.getCause() instanceof XMLStreamException ? ex.getCause() : ex);
            throw Exceptions.propagate(ex);
        }
        finally {
            DataBufferUtils.release(dataBuffer);
        }
    }

    private Object unmarshal(WebServiceMessage message, Class<?> outputClass) {
        try {
            Unmarshaller unmarshaller = initUnmarshaller(outputClass);
            JAXBElement<?> jaxbElement = unmarshaller.unmarshal(message.getPayloadSource(),outputClass);
            return jaxbElement.getValue();
        }
        catch (UnmarshalException ex) {
            throw new DecodingException("Could not unmarshal XML to " + outputClass, ex);
        }
        catch (JAXBException ex) {
            throw new CodecException("Invalid JAXB configuration", ex);
        }
    }

Jaxb2SoapDecoder helps in casting to expected object from before it returns to webflux.

Jaxb2SoapEncoder and Jaxb2SoapDecoder classes must be added to WebClient config as below.

    @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().register(new Jaxb2SoapEncoder());
            clientCodecConfigurer.customCodecs().register(new Jaxb2SoapDecoder());
        }).build();


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

        return webClient;
    }

When calling WebClient retrieve used instead of exchange since error handling is better using retrieve. If no HTTP status error occurred, response is mapped to JAXB Response.

 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();

    }

Working example here: https://github.com/gungor/spring-webclient-soap.git

This project is maintained by gungor