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.
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 carpetatarget
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.