Invocar servicios SOAP desde Spring Boot

Con las interfaces REST y más recientemente GraphQL, los servicios SOAP han caído rápidamente en desuso y hoy solo los sistemas legacy son quienes continúan haciendo uso de esta tecnología.

Si alguna vez has trabajado en este tipo de entornos donde hay mucha tecnología legacy y “enterprisey”, probablemente has tenido que integrar piezas de software más modernas contra estos remanentes tecnológicos.

En esta entrada veremos como invocar un servicio SOAP desde un componente Spring Boot. También abordaremos 2 particularidades de la invocación que es el seteo de un método de autenticación, y el paso de cabeceras HTTP en el llamado.

Preparando el proyecto

He preparado un repositorio con un proyecto de ejemplo listo para ejecutar (por si te interesa ir directo al código) (Java 8 requerido).

https://github.com/felipeleivav/spring-boot-soap-client-example

Si prefieres hacer el paso a paso en un proyecto nuevo, puedes generar tu proyecto con Spring Initializr. O bien utilizar el mismo servicio pero desde el IDE Spring Tools Suite.

Básicamente el proyecto consta de un controller que invocará el servicio SOAP. De este modo, lo vamos a exponer a través de un servicio REST, como una suerte de proxy.

Considerar que el proyecto tiene varias dependencias, por lo que si partes de cero deberás mirar el pom.xml de mi proyecto para homologarlas.

Generando el cliente SOAP

Para poder hacer la invocación de un servicio SOAP necesitamos generar su cliente. Esto implica que en nuestro servicio nuevo debemos conocer la implementación del SOAP que queremos invocar. Si ya has trabajado con servicios SOAP anteriormente probablemente ya has trabajado también con programas como Metro para la generación de clientes SOAP a partir de un WSDL.

En nuestro caso no generaremos un cliente JAR del servicio, si no que solo generaremos las clases del esquema de entrada/salida usando la herramienta XJC que viene incluida en cualquier distribución del JDK.

Tomaremos un WebService público a modo de ejemplo. Lo único que hace es transformar un número a palabras (como 33 a treinta y tres, pero inglés):

https://www.dataaccess.com/webservicesserver/NumberConversion.wso?op=NumberToWords

First things first. Transformaremos el esquema de este servicio a clases Java, esto es, tomar los XSD (XML Schema Definition) y con XJC, transformarlos a *.java.

Como el servicio es pequeño y solo recibe un parámetro de entrada, no tenemos estructuras de entrada/salida lo suficientemente grandes como para estar en un XSD fuera del WSDL principal (WebService Definition Language). Dado esto, aplicaremos XJC directo sobre el WSDL.

Generar clases java a partir de la especificación WSDL

Entonces abrimos una terminal, nos paramos en el directorio de fuentes del proyecto y ejecutamos XJC sobre el WSDL del servicio SOAP:

cd src/main/java
xjc -wsdl https://www.dataaccess.com/webservicesserver/NumberConversion.wso?wsdl

Esto nos genera las clases de entrada y salida del servicio en nuestro proyecto, con lo que ya tenemos lo requerido para conectarnos. Deberíamos ver algo así:

Analizando un esquema...
Compilando un esquema...
com/dataaccess/webservicesserver/NumberToDollars.java
com/dataaccess/webservicesserver/NumberToDollarsResponse.java
com/dataaccess/webservicesserver/NumberToWords.java
com/dataaccess/webservicesserver/NumberToWordsResponse.java
com/dataaccess/webservicesserver/ObjectFactory.java
com/dataaccess/webservicesserver/package-info.java

Generar clase cliente

Ahora que tenemos el esquema en nuestro proyecto. Vamos a crear el cliente. Esto no es más que una clase en la que hacemos la invocación del servicio:

@Service
@Component("NumberConversion")
public class SoapClient extends WebServiceGatewaySupport {

    private String endpoint = "https://www.dataaccess.com/webservicesserver/NumberConversion.wso?wsdl";

    public String convertirNumeroPalabras(Long numero) {
        // Creamos el request
        NumberToWords request = new NumberToWords();
        request.setUbiNum(BigInteger.valueOf(numero));
        
        // Invocamos el servicio
        NumberToWordsResponse response = (NumberToWordsResponse) getWebServiceTemplate().marshalSendAndReceive(endpoint, request);
        
        // Retornamos el dato
        return response.getNumberToWordsResult();
    }

}

Acá notar que nuestra clase cliente extiende de “WebServiceGatewaySupport” la cual es una clase de Spring para acceder a servicios web.

El resto del código se explica solo: Creación del objeto request, invocación, y retorno del dato, todo esto usando las clases de esquema que generamos en el paso anterior.

Configurando el marshalling

El marshalling se refiere a la conversión entre XML y objetos Java en tiempo de ejecución, esto es, los requests que enviamos (Java a XML) y los responses que recibimos (XML a Java).

En nuestro proyecto creamos una nueva clase la cual dice que utilizaremos la librería JAXB2 para realizar el marshall:

@Configuration
public class SoapConfiguration {

    @Autowired
    private SoapClient client;

    @Bean
    public Jaxb2Marshaller marshaller() {
        Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
        marshaller.setContextPath("com.dataaccess.webservicesserver");
        return marshaller;
    }

    @Bean
    public SoapClient soapClient(Jaxb2Marshaller marshaller) {
        client.setMarshaller(marshaller);
        client.setUnmarshaller(marshaller);
        return client;
    }

}

Limpio, cierto? Todo se resume a:

  • Crear el marshaller JAXB2
  • Asociarlo al package del esquema generado
  • Asociarlo a nuestro cliente creado en el paso anterior

Invocando el cliente SOAP

Ahora que ya tenemos todo lo requerido:

  • Clases de esquema
  • Clase cliente
  • Clase configuración

Ya podemos invocar el cliente SOAP desde cualquier parte de nuestro proyecto.

En este caso, como dijimos que sería una suerte de proxy, agregaré el llamado directo sobre un controller REST.

@RestController
@RequestMapping("/num2words")
public class SoapClientController {
    
    @Autowired
    private SoapClient soapClient;

    @GetMapping("/{numero}")
    public String convertirNumeroPalabras(@PathVariable("numero") Long numero) {
        return soapClient.convertirNumeroPalabras(numero);
    }

}

De este modo, el retorno de nuestro servicio REST, será lo que recuperamos desde el SOAP invocado.

Para probar solo ingresa en tu navegador: http://localhost:8080/num2words/42.

Puedes hacer la prueba con números más complejos:

Setear autenticación

Es muy probable que el servicio que necesites invocar cuente con algún método de autenticación. Para este caso, como tenemos un servicio público, solo setearemos un método de autenticación básico el cual no tendrá efectos sobre el request pero sí nos servirá como ejemplo.

Lo único que modificaremos será la clase “SoapConfiguration”:

@Bean
public HttpComponentsMessageSender getMessageSender() {
    HttpComponentsMessageSender messageSender = new HttpComponentsMessageSender();
    messageSender.setCredentials(new UsernamePasswordCredentials("username", "password"));
    return messageSender;
}

Hemos agregado el método “getMessageSender()” el cual se encargará de alterar los request que salen al servicio, añadiéndoles una cabecera de autenticación.

Al igual que con el “marshaller”, lo dejamos asociado a nuestro cliente en el método ya existente:

    @Bean
    public SoapClient soapClient(Jaxb2Marshaller marshaller) {
        client.setMessageSender(getMessageSender());
        client.setMarshaller(marshaller);
        client.setUnmarshaller(marshaller);
        return client;
    }

Setear cabeceras HTTP adicionales

Es probable también que tu servicio SOAP requiera algunas cabeceras específicas. Para esto, creamos una nueva clase en la cual setearemos las cabeceras HTTP como clave-valor:

public class SoapHeaders implements WebServiceMessageCallback {

    @Override
    public void doWithMessage(WebServiceMessage message) throws IOException, TransformerException {
        TransportContext context = TransportContextHolder.getTransportContext();
        HeadersAwareSenderWebServiceConnection connection = (HeadersAwareSenderWebServiceConnection) context
                .getConnection();
        connection.addRequestHeader("Cabecera-HTTP", "dato");
    }

}

Acá notar que la clase debe extender de “WebServiceMessageCallback”. Este método básicamente actuará como un interceptor de requests, agregándole las cabeceras HTTP que nosotros definimos vía “addRequestHeader()” y antes que el payload sea finalmente enviado.

Por último, agregamos este interceptor al método que hace la invocación del SOAP en la clase “SoapClient” para que pueda ser invocado:

.marshallSendAndReceive(endpoint, request, new SoapHeaders());

Conclusión

Si bien hoy en día ya prácticamente no se están generando nuevos servicios SOAP, existen muchas empresas y organizaciones que aun no renuevan esta parte de su infraestructura tecnológica, por lo que el camino para muchos será escribir integraciones contra estos servicios.

Afortunadamente, con Spring Boot hemos visto que no es complicado desarrollar estas integraciones. Es más, podemos desarrollarlas de forma bastante limpia y modular.

12 Comments

Add yours →

  1. Muchas gracias, Felipe. Está explicado muy clarito y sencillo.

  2. Hola Felipe, excelente ejemplo, muy didactico y practo

    Tendrias un ejemplo con un WS que use un esquema mas complejo en el request y response ??

    • Hola Kevin, muchas gracias!

      No tengo a mano un ejemplo con esquema más complejo, pero esto no debería ser más que añadir los atributos que quieras a la clase request que en el código de ej. es: com.dataaccess.webservicesserver.NumberToDollars.

  3. Buenas Tardes Felipe,

    Para el caso de manejo de SSL tienes alguna guia?

  4. Hola he estado buscando por internet para poder hacer la conexión con un servicio soap grande y tu tutorial parece muy limpio pero no puedo correr la instrucción xjc me dice que no es un comando valido, sabes a que se deba ? quise seguir tu tutorial primero con tu SOAP de prueba pero no me sale agradezco cualquier ayuda, la respuesta no me importa recibirla como XML o String solo necesito una parte de ella que otro sistema la va a interpretar es un pdf base 64

  5. Hola Felipe, el ejemplo me valió, estoy intentando en hacer lo mismo pero esta vez no deseo mostrar los datos a través de un servicio rest sino a través de cadenas y así poder manipularlos, quizás tengas algo parecido o puedas guiarme en ello, gracias por tu colaboración.

  6. hola estoy siguiendo tu ejemplo pero al ejecutar el comando xjc -wsd en src/main/java en linux me indica xjc command not found estoy usando java 11 debo hacer alguna instalacion aparte de xjc ? muchas gracias

  7. new UsernamePasswordCredentials(“username”, “password”));
    al parecer no funciona o esta deprecada, porque no encontre ninguna dependencia o jar que funcione

  8. Hola Felipe, muy buen ejemplo.

    Una consulta, al ejecutar el servicio me da este error ¿sabes a qué podría deberse?

    java.lang.IllegalStateException: No marshaller registered. Check configuration of WebServiceTemplate.

  9. Muchas gracias por el tutorial.

    Me ha sido de gran ayuda, buen Trabajo!.

Agregar un comentario

Su dirección de correo no se hará público.