Como crear servicios SOAP 馃Ъ馃 con Spring Boot 馃崈 + Apache CXF

Como crear servicios SOAP con Spring Boot + Apache CXF

Uno de los est谩ndares mas extendidos para la creaci贸n de servicios antes de REST, fue SOAP, que la diferencia a simple vista que vemos es que en lugar de usar JSON como elemento de intercambio de la informaci贸n usa XML.

La principal ventaja de los servicios SOAP en el pasado fue que defin铆a no solo una forma de transmitir la informaci贸n, sino tambi茅n reglas en lo que tiene que ver a la estructura de los mensajes, y estableciendo cosas como WSDL (Web Services Description Language) como est谩ndar, lo que permite un "contrato formal" para todos aquellos consumidores que usen tu servicio.

A d铆a de hoy REST tiene OpenAPI / Swagger como est谩ndar de definici贸n que seria el equivalente en SOAP a WSDL, as铆 que personalmente dir铆a que en la actualidad no tiene ning煤n sentido crear servicios SOAP respecto a uno REST principalmente porque JSON es mucho las ligero que XML, pero si que considero que es importante conocerlo ya que durante mi carrera para implementar requisitos me he visto en la necesidad de realizar integraciones con servicios SOAP, es por ello que el articulo el objetivo principal es abrir camino, para un segundo articulo en el que hablo de como consumir servicios SOAP.

Comencemos.

Para empezar, voy a usar la maravillosa pagina de Spring Initialzr que nos permite seleccionar de manera r谩pida y sencilla los diferentes "starters" que vamos a necesitar, en este caso vamos a necesitar 煤nicamente:

  • Spring Web

Yo en mi caso tambi茅n voy a a帽adir las Spring DevTools, pero no es necesaria.

Spring Initializr

Adicionalmente tambi茅n a帽adir茅 la siguiente dependencia en el pom.xml

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

os recomiendo poner la versi贸n en <properties> dentro del pom.xml y usar ${cxf.version}

Creando nuestra primera operaci贸n.

A diferencia de los servicios REST en los servicios SOAP tenemos un 煤nico endpoint que puede tener varias "operaciones", cada una de estas operaciones representa un endpoint si lo comparamos con los servicios REST (esto a algunos os recordara a GraphQL)

Para crear mi primera operaci贸n he creado una clase y le he agregado un simple m茅todo:

package dev.mpesteban.soapservice;  

import jakarta.jws.WebMethod;  
import jakarta.jws.WebService;  


@WebService  
public class MySoapService {  

    @WebMethod  
    public String sayHello(String name) {  
        return "Hello, " + name + "!";  
    }  
}

Lo siguiente es crear las Beans necesarias para que funcione correctamente con Spring Boot, para ello he creado una clase con la anotaci贸n @Configuration con lo siguiente:

package dev.mpesteban.soapservice;  

import jakarta.xml.ws.Endpoint;  
import org.apache.cxf.Bus;  
import org.apache.cxf.bus.spring.SpringBus;  
import org.apache.cxf.jaxws.EndpointImpl;  
import org.apache.cxf.transport.servlet.CXFServlet;  
import org.springframework.boot.web.servlet.ServletRegistrationBean;  
import org.springframework.context.annotation.Bean;  
import org.springframework.context.annotation.Configuration;  

@Configuration  
public class WebServiceConfig {  
    @Bean  
    public ServletRegistrationBean<CXFServlet> cxfServlet() {  
        return new ServletRegistrationBean<>(new CXFServlet(), "/ws/*");  
    }  

    @Bean(name = Bus.DEFAULT_BUS_ID)  
    public SpringBus springBus() {  
        return new SpringBus();  
    }  

    @Bean  
    public Endpoint endpoint() {  
        EndpointImpl endpoint = new EndpointImpl(springBus(), new MySoapService());  
        endpoint.publish("/Hello");  
        return endpoint;  
    }  

}

Y listo con todo esto tendr铆amos ya listo un servicio muy b谩sico para ser usado.

隆Vamos a probarlo!

Para probar que nuestra operaci贸n ha sido creada y funciona correctamente, arrancamos como har铆amos normalmente con cualquier proyecto de Spring Boot.

Lo primero que podremos detectar como buena se帽al de que esta funcionando correctamente son algunos mensajes en el log, indicando que el servicio ha sido registrado:

2023-08-08T11:33:35.763+02:00  INFO 34656 --- [  restartedMain] o.a.c.w.s.f.ReflectionServiceFactoryBean : Creating Service {http://soapservice.mpesteban.dev/}MySoapServiceService from class dev.mpesteban.soapservice.MySoapService
2023-08-08T11:33:36.124+02:00  INFO 34656 --- [  restartedMain] org.apache.cxf.endpoint.ServerImpl       : Setting the server's publish address to be /Hello

Esto es buena se帽al pero si nos dirigimos a http://localhost:8080/ws/Hello?wsdl deberiamos poder visualizar correctamente la definicion WSDL.

<wsdl:definitions xmlns:xsd="http://www.w3.org/2001/XMLSchema"
聽 聽 xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:tns="http://soapservice.mpesteban.dev/"
聽 聽 xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
聽 聽 xmlns:ns1="http://schemas.xmlsoap.org/soap/http" name="MySoapServiceService"
聽 聽 targetNamespace="http://soapservice.mpesteban.dev/">
聽 聽 <wsdl:types>
聽 聽 聽 聽 <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
聽 聽 聽 聽 聽 聽 xmlns:tns="http://soapservice.mpesteban.dev/" elementFormDefault="unqualified"
聽 聽 聽 聽 聽 聽 targetNamespace="http://soapservice.mpesteban.dev/" version="1.0">
聽 聽 聽 聽 聽 聽 <xs:element name="sayHello" type="tns:sayHello" />
聽 聽 聽 聽 聽 聽 <xs:element name="sayHelloResponse" type="tns:sayHelloResponse" />
聽 聽 聽 聽 聽 聽 <xs:complexType name="sayHello">
聽 聽 聽 聽 聽 聽 聽 聽 <xs:sequence>
聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 <xs:element minOccurs="0" name="arg0" type="xs:string" />
聽 聽 聽 聽 聽 聽 聽 聽 </xs:sequence>
聽 聽 聽 聽 聽 聽 </xs:complexType>
聽 聽 聽 聽 聽 聽 <xs:complexType name="sayHelloResponse">
聽 聽 聽 聽 聽 聽 聽 聽 <xs:sequence>
聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 <xs:element minOccurs="0" name="return" type="xs:string" />
聽 聽 聽 聽 聽 聽 聽 聽 </xs:sequence>
聽 聽 聽 聽 聽 聽 </xs:complexType>
聽 聽 聽 聽 </xs:schema>
聽 聽 </wsdl:types>
聽 聽 <wsdl:message name="sayHelloResponse">
聽 聽 聽 聽 <wsdl:part element="tns:sayHelloResponse" name="parameters"> </wsdl:part>
聽 聽 </wsdl:message>
聽 聽 <wsdl:message name="sayHello">
聽 聽 聽 聽 <wsdl:part element="tns:sayHello" name="parameters"> </wsdl:part>
聽 聽 </wsdl:message>
聽 聽 <wsdl:portType name="MySoapService">
聽 聽 聽 聽 <wsdl:operation name="sayHello">
聽 聽 聽 聽 聽 聽 <wsdl:input message="tns:sayHello" name="sayHello"> </wsdl:input>
聽 聽 聽 聽 聽 聽 <wsdl:output message="tns:sayHelloResponse" name="sayHelloResponse"> </wsdl:output>
聽 聽 聽 聽 </wsdl:operation>
聽 聽 </wsdl:portType>
聽 聽 <wsdl:binding name="MySoapServiceServiceSoapBinding" type="tns:MySoapService">
聽 聽 聽 聽 <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http" />
聽 聽 聽 聽 <wsdl:operation name="sayHello">
聽 聽 聽 聽 聽 聽 <soap:operation soapAction="" style="document" />
聽 聽 聽 聽 聽 聽 <wsdl:input name="sayHello">
聽 聽 聽 聽 聽 聽 聽 聽 <soap:body use="literal" />
聽 聽 聽 聽 聽 聽 </wsdl:input>
聽 聽 聽 聽 聽 聽 <wsdl:output name="sayHelloResponse">
聽 聽 聽 聽 聽 聽 聽 聽 <soap:body use="literal" />
聽 聽 聽 聽 聽 聽 </wsdl:output>
聽 聽 聽 聽 </wsdl:operation>
聽 聽 </wsdl:binding>
聽 聽 <wsdl:service name="MySoapServiceService">
聽 聽 聽 聽 <wsdl:port binding="tns:MySoapServiceServiceSoapBinding" name="MySoapServicePort">
聽 聽 聽 聽 聽 聽 <soap:address location="http://localhost:8080/ws/Hello" />
聽 聽 聽 聽 </wsdl:port>
聽 聽 </wsdl:service>
</wsdl:definitions>

Si este es el caso es que entonces todo va como la seda, para terminar de comprobar que todo funciona correctamente he descargado SOAP UI, una herramienta que para mi es imprescindible cuando trabajo con servicios SOAP, describirlo r谩pidamente es "El Postman de servicios SOAP", aunque Postman a la fecha de escribir este articulo, ya te permite importar WSDL, Postman concreto me ha dado alg煤n que otro problema en alguna ocasi贸n no gener谩ndome el contenido de la request de ejemplo de forma correcta, es por eso que sigo prefiriendo SOAP UI.

Para lanzar llamadas contra nuestro servicio con SOAP UI:

Desde File > New SOAP Project introducimos

  • Project Name: El que tu quieras.
  • Initial WSDL: La URL de nuestro WSDL http://localhost:8080/ws/Hello?wsdl

Esto creara un nuevo proyecto SOAP que nos permitir谩 hacer llamadas a nuestras operaciones,

SOAP UI Operation 1

Lanzamos la petici贸n:

SOAP UI Request

Y podemos ver que nos responde correctamente.

Creando estructuras mas complejas

El ejemplo anterior era sencillo pero 驴Y si queremos recibir un objeto con varios atributos etc. como podemos hacerlo?

Bien aqu铆 entran las etiquetas de JAXB veamos un ejemplo:

Para este ejemplo voy a crear una clase Car y un nuevo m茅todo en mi servicio que devuelva una lista de los Car que coincidan con la marca que le he pasado por par谩metros en la petici贸n.

package dev.mpesteban.soapservice;  


import jakarta.xml.bind.annotation.XmlRootElement;  

@XmlRootElement  
public class Car {  

    private String brand;  

    private String model;  
    private Integer horsepower;  


    public Car(String brand, String model, Integer horsepower) {  
        this.brand = brand;  
        this.model = model;  
        this.horsepower = horsepower;  
    }

    public Car() {}

    public String getBrand() {  
        return brand;  
    }  

    public void setBrand(String brand) {  
        this.brand = brand;  
    }  

    public String getModel() {  
        return model;  
    }  

    public void setModel(String model) {  
        this.model = model;  
    }  

    public Integer getHorsepower() {  
        return horsepower;  
    }  

    public void setHorsepower(Integer horsepower) {  
        this.horsepower = horsepower;  
    }  

}

La clase Car tenga un constructor por defecto, es algo requerido por JAXB, y que si no lo tenemos la aplicaci贸n no arrancara.

Nuestra nueva operaci贸n dentro de la clase MySoapService quedar铆a as铆:

@WebMethod  
public List<Car> getCarsByBrand(String brandName) {  

    final List<Car> cars = List.of(  
            new Car("Ford", "Mustang", 300),  
            new Car("Kia", "Ceed", 140),  
            new Car("Honda", "Civic", 300),  
            new Car("Honda", "Jazz", 90)  
    );  

    return cars.stream().filter((e) -> e.getBrand().equalsIgnoreCase(brandName)).collect(Collectors.toList());  

}

Probamos la peticion y todo como la seda.

SOAP UI Operation 2

Simplificando la creacion de endpoints

Como habr茅is observado al inicio, cada vez que queremos crear un nuevo "Endpoint", implica crear una nueva Bean, indicando las clases que contienen operaciones asociados a ese endpoint, esto hace que pueda haber alg煤n punto en el que la clase de configuraci贸nion crezca mucho, en una tarea repetitiva como lo es la creaci贸n de endpoints.

Para solucionar esto, vamos a crear nuestra propia anotaci贸n, que nos permita al arrancar la aplicaci贸n, obtener todas las clases que contienen esta anotaci贸n, instanciarlas y asociarlas a un Endpoint.

package dev.mpesteban.soapservice;  


import java.lang.annotation.Retention;  
import java.lang.annotation.RetentionPolicy;  


@Retention(RetentionPolicy.RUNTIME)  
public @interface WebServiceEndpoint {  
    String value();  
}

Con nuestra anotaci贸n personalizada creada, vamos a hacer toda la logica relacionada con obtener las clases y crear las beans, para ello voy a modificar la clase WebServiceConfig y hacer que implemente la interfaz BeanFactoryPostProcessor proporcionandonos el metodo postProcessBeanFactory.

El m茅todo postProcessBeanFactory se llama durante la fase de inicializaci贸n del contexto de Spring, despu茅s de que se hayan cargado todas las definiciones de beans pero antes de que se creen las instancias reales de los beans. Esto te brinda la oportunidad de influir en c贸mo se configurar谩n y se instanciar谩n los beans antes de que se utilicen en la aplicaci贸n, en nuestro caso concreto nos va a permitir definir las beans que necesitamos de forma din谩mica.

@Override  
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {  
    try {  
        generateServiceBeans(beanFactory);  
    } catch (ClassNotFoundException e) {  
        throw new RuntimeException(e);  
    }  
}

El metodo generateServiceBeans tiene lo principal que vamos a usar.

public void generateServiceBeans(  
        final ConfigurableListableBeanFactory  beanFactory  
) throws ClassNotFoundException {  

    List<Class<?>> clazzList = findAllWebServiceEndpointClasses(this.getClass().getPackageName());  

    log.info("Detected " + clazzList.size() + " SOAP WebServices interfaces");  

    Bus bus = beanFactory.getBean(Bus.class);  

    for (Class<?> clazz : clazzList) {  
        WebServiceEndpoint wsEndpoint = clazz.getAnnotation(WebServiceEndpoint.class);  
        beanFactory.registerSingleton("endpoint" + clazz.getSimpleName(), generateEndpointBean(bus, clazz, wsEndpoint));  
    }  

}

Como podemos observar tenemos un m茅todo findAllWebServiceEndpointClasses que es el que va a tener la l贸gica para buscar las clases con nuestra anotaci贸n personalizada, recibe el nombre del paquete que para este ejemplo he utilizado la clase actual para obtenerlo:

private List<Class<?>> findAllWebServiceEndpointClasses(String packageName) throws ClassNotFoundException {  
    final List<Class<?>> result = new LinkedList<>();  
    final ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(  
            false);  
    provider.addIncludeFilter(new AnnotationTypeFilter(WebServiceEndpoint.class));  
    for (BeanDefinition beanDefinition : provider  
            .findCandidateComponents(packageName)) {  
            result.add(Class.forName(beanDefinition.getBeanClassName()));  
    }  

    return result;  
}

Esto nos devuelve una lista con todas las clases que tienen esta anotacion, para que funcione correctamente deberemos aplicar la anotacion @WebServiceEndpoint a la clase MySoapService quedando de la siguiente manera:

package dev.mpesteban.soapservice;  

import jakarta.jws.WebMethod;  
import jakarta.jws.WebService;  

import java.util.List;  
import java.util.stream.Collectors;  



@WebServiceEndpoint("/Hello")  
@WebService  
public class MySoapService {  

    @WebMethod  
    public String sayHello(String name) {  
        return "Hello, " + name + "!";  
    }  

    @WebMethod  
    public List<Car> getCarsByBrand(String brandName) {  

        final List<Car> cars = List.of(  
                new Car("Ford", "Mustang", 300),  
                new Car("Kia", "Ceed", 140),  
                new Car("Honda", "Civic", 300),  
                new Car("Honda", "Jazz", 90)  
        );  

        return cars.stream().filter((e) -> e.getBrand().equalsIgnoreCase(brandName)).collect(Collectors.toList());  

    }  
}

Volviendo al metodo generateServiceBeans podemos ver que lo siguiente que hace es recorrer la lista obtenida y por cada elemento llama a el metodo generateEndpointBean. y registra una bean usando beanFactory que le hemos ido pasando por parametros.

    for (Class<?> clazz : clazzList) {  
        WebServiceEndpoint wsEndpoint = clazz.getAnnotation(WebServiceEndpoint.class);  
        beanFactory.registerSingleton("endpoint" + clazz.getSimpleName(), generateEndpointBean(bus, clazz, wsEndpoint));  
    }  
private <T> T generateEndpointBean(  
        final Bus bus,  
        Class<T> clazz,  
        final WebServiceEndpoint endpointConfig  
) {  
    JaxWsServerFactoryBean endpointBean = new JaxWsServerFactoryBean();  
    endpointBean.setAddress(endpointConfig.value());  
    endpointBean.setBus(bus);  
    endpointBean.setServiceClass(clazz);  
    return (T) endpointBean.create();  
}

Usando la clase JaxWsServerFactoryBean creamos nos permite crear una instancia de esa clase lista para ser usada como Bean.

Esto nos permite de manera r谩pida que simplemente cada vez que una clase tenga nuestra anotaci贸n autom谩ticamente se considere que es un nuevo endpoint y nos evita repetir c贸digo constantemente.

Con todo esto tendr铆amos una solida base para un proyecto.

驴Por qu茅 Apache CXF y no usar Spring WS?

Esto es una pregunta que los mas avanzados y que ya entiendan del tema se pueden hacer la respuesta es sencilla:

El principal motivo es que el objetivo de esta guia era que cuanto antes empezar a programar, el problema de Spring WS es que te obliga a que comiences definiendo tu esquema XSD (hacer el contrato de tu API primero), y en cambio Apache CXF te permite generar a partir de tu c贸digo toda la definici贸n necesaria.