Realizando peticiones SOAP 🧼 en Spring Boot con Apache CXF

Consumiendo servicios SOAP con Spring Boot + Apache CXF

A pesar de estar ya en desuso, los servicios SOAP, aun siguen entre nosotros, existen servicios antiguos aun creados usando este estándar para crear APIs.

Esto es una continuación de este articulo, que recomiendo darle un vistazo para obtener un poco de contexto.

En esta continuación voy a cubrir el como consumir / hacer llamadas a servicios SOAP desde Spring Boot, usando Apache CXF.

Para ello, como ejemplo, vamos a usar como servicio a consumir el creado en el articulo anterior, crearemos un API REST desde 0 que al llamarla realizara una petición usando el protocolo SOAP, para llamar al servicio y devolvernos la información todo esto con la librería de Apache CXF.

Preparando nuestro proyecto.

Como usualmente hago cuando comienzo un proyecto con Spring, utilizo Spring Initialzr para escoger las dependencias que necesito, en este caso Spring Web, aunque a mi siempre me gusta añadir las DevTools de Spring para mejorar la experiencia de desarrollo.

Spring Initialzr setup

Adicionalmente también necesitaremos agregar la dependencia de Apache CXF y el plugin que se encargara a partir del fichero de definición de el API SOAP (WSDL) que le proporcionemos generar las interfaces correspondientes para poder realizar las llamadas al servicio.

Lo primero que he hecho es definir que version de Apache CXF voy a usar en la parte de <properties> del pom.xml

<properties>  
    <java.version>17</java.version>  
    <cxf.version>4.0.2</cxf.version>  
</properties>

Despues en las <dependencies> he agregado las siguientes dependencias

<dependency>
	<groupId>org.apache.cxf</groupId>
	<artifactId>cxf-spring-boot-starter-jaxws</artifactId>
	<version>${cxf.version}</version>
</dependency>

<dependency>  
    <groupId>org.apache.cxf</groupId>  
    <artifactId>cxf-rt-features-logging</artifactId>  
    <version>${cxf.version}</version>  
</dependency>

Por ultimo, agregaremos en <build> > <plugins> agregamos el plugin de cxf-codegen-plugin

<plugin>
	<groupId>org.apache.cxf</groupId>
	<artifactId>cxf-codegen-plugin</artifactId>
	<version>${cxf.version}</version>
	<executions>
		<execution>
			<id>generate-sources-soap</id>
			<phase>generate-sources</phase>
			<configuration>
				<sourceRoot>${project.build.directory}/generated-sources/cxf</sourceRoot>
				<wsdlRoot>${project.basedir}/src/main/resources/wsdl/</wsdlRoot>
				<includes>
					<include>**/*.wsdl</include>
				</includes>
			</configuration>
			<goals>
				<goal>wsdl2java</goal>
			</goals>
		</execution>
	</executions>
</plugin>

En este ultimo bloque podemos observar varias etiquetas:

  • <sourceRoot> sirve para indicar la ruta donde se almacenaran las clases generadas a partir del WSDL, ${project.build.directory} hace referencia a la carpeta target que genera Maven automáticamente a hacer un build.

  • <wsdlRoot> Indica donde estarán el / los ficheros WSDL, dentro de nuestro proyecto, esto es porque realmente para usar este WSDL tenemos dos opciones:

    • Descargar el fichero WSDL y agregarlo a nuestro proyecto.
    • Usar directamente la URL de el WSDL

Para la primera opción, guardamos en src/main/resources/wsdl el fichero, en este caso es importante que el formato del fichero este en minúsculas .wsdl ya que si nos fijamos la etiqueta XML <includes> le hemos indicado que dentro de esa carpeta recoja todos los ficheros terminados en .wsdl (y diferencia entre mayúsculas y minúsculas).

Para la segunda, en lugar de usar la etiqueta <wsdlRoot> y <includes>, utilizaremos las etiquetas <wsdlOptions> y dentro agregaremos una opción indicando la URL quedaría así:

<plugin>  
    <groupId>org.apache.cxf</groupId>  
    <artifactId>cxf-codegen-plugin</artifactId>  
    <version>${cxf.version}</version>  
    <executions>  
       <execution>  
          <id>generate-sources-soap</id>  
          <phase>generate-sources</phase>  
          <configuration>  
             <sourceRoot>${project.build.directory}/generated-sources/cxf</sourceRoot>  
             <wsdlOptions>  
                <wsdlOption>  
                   <wsdl>http://localhost:8080/ws/Hello?wsdl</wsdl>  
                </wsdlOption>  
             </wsdlOptions>  
          </configuration>  
          <goals>  
             <goal>wsdl2java</goal>  
          </goals>  
       </execution>  
    </executions>  
</plugin>

Al lanzar nuestra aplicación, observaremos que en la carpeta target/cxf tendremos contenido que antes no teníamos, mas concretamente podremos observar que se nos han creado todas las clases y una interfaz que coinciden con lo que tenemos en nuestro servidor SOAP.

Consumiendo el servicio.

Creando la instancia

Vamos a proceder entonces a empezar a realizar peticiones al servicio, para respetar como se suele trabajar en Spring, vamos a crear la Bean correspondiente a partir de la interfaz que nos ha autogenerado el plugin, para ello la librería que hemos introducido anteriormente cxf-spring-boot-starter-jaxws nos proporciona la clase JaxWsProxyFactoryBean para crear la instancia correspondiente.

Para todo este proceso de crear la instancia de las beans, voy a crear una clase SOAPConfig y en ella creare una función, la clase completa quedaria asi:

package dev.mpesteban.soapconsumer;  
  
import org.apache.cxf.ext.logging.LoggingInInterceptor;  
import org.apache.cxf.ext.logging.LoggingOutInterceptor;  
import org.apache.cxf.jaxws.JaxWsProxyFactoryBean;  
import org.slf4j.Logger;  
import org.slf4j.LoggerFactory;  
import org.springframework.context.annotation.Configuration;  
  
@Configuration  
public class SOAPConfig {  
  
    private static final Logger log = LoggerFactory.getLogger(SOAPConfig.class);  
  
    private <T> T generateWithEndpointUrl(final String endpointUrl, final Class<T> clazz) {  
        JaxWsProxyFactoryBean factory = new JaxWsProxyFactoryBean();  
        factory.setServiceClass(clazz);  
        factory.setAddress(endpointUrl);  
  
        LoggingInInterceptor loggingInInterceptor = new LoggingInInterceptor();  
        loggingInInterceptor.setPrettyLogging(true);  
        factory.getInInterceptors().add(loggingInInterceptor);  
  
        LoggingOutInterceptor loggingOutInterceptor = new LoggingOutInterceptor();  
        loggingOutInterceptor.setPrettyLogging(true);  
        factory.getOutInterceptors().add(loggingOutInterceptor);  
  
        T portserviceType = factory.create(clazz);  
  
        log.info("Create proxy factory for service class: " + clazz.getName());  
  
        return (T) portserviceType;  
    }  
  
  
}

Este método me va a permitir fácilmente crear la instancia, para consumir mi servicio SOAP, y especificarle la URL, esto es porque puede ser que a pesar de que la definición utilicemos un entorno especifico, luego al promocionar a entornos superiores vaya variando, también en el caso de que consumiésemos múltiples servicios SOAP podríamos re-utilizar este método.

Por ultimo en la misma clase, SOAPConfig voy a agregar el metodo con la anotacion @Bean para hacer disponible esta instancia al contenedor de spring.

@Bean  
public MySoapService mySoapServiceBean() {  
    return generateWithEndpointUrl("http://localhost:8080/ws/Hello", MySoapService.class);  
}

Para esta prueba he puesto la URL directamente, pero lo ideal seria que este valor viniera del fichero .properties o variables de entorno por ejemplo.

Creando nuestro endpoint de prueba.

Para hacer una prueba rapida y comprobar que podemos consumir nuestro servicio SOAP Correctamente voy a crear una clase MyTestingController con la anotacion @RestController, a la que inyectare el MySoapService y llamare al metodo que cree en el servicio SOAP getCardsByBrand

package dev.mpesteban.soapconsumer;  
  
import dev.mpesteban.soapservice.Car;  
import dev.mpesteban.soapservice.MySoapService;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.web.bind.annotation.GetMapping;  
import org.springframework.web.bind.annotation.RequestParam;  
import org.springframework.web.bind.annotation.RestController;  
  
import java.util.List;  
  
@RestController  
public class MyTestingController {  
  
    @Autowired  
    private MySoapService mySoapService;  
  
    @GetMapping("/getCarByBrand")  
    public List<Car> getCarByBrand(@RequestParam String brand) {  
        return mySoapService.getCarsByBrand(brand);  
    }  
  
}

Con esto hecho, arrancare tanto el servidor SOAP que cree en el anterior articulo y la aplicacion que hemos ido creando en este articulo, en mi caso concreto como estoy ejecutandolos en la misma maquina he cambiado el puerto de la aplicacion al 8085 para que sea diferente.

En el navegador si nos dirigimos a: http://localhost:8085/getCarsByBrand?brand=honda podremos observar que nos devuelve el siguiente JSON.

[
    {
        "brand": "Honda",
        "horsepower": 300,
        "model": "Civic"
    },
    {
        "brand": "Honda",
        "horsepower": 90,
        "model": "Jazz"
    }
]

Con lo cual podemos concluir que nuestra integracion esta funcionando correctamente.