Java API for XML Web Services (JAX-WS), es una especificación para la creación de servicios web basados en XML-SOAP que vino a suponer la evolución de la especificación JAX-RPC y que forma parte de JEE 5 y Java SE 6. Está basada en anotaciones y permite la creación tanto de servicios como de clientes de servicios web SOAP directamente con clases Java POJO sin tener que lidiar con XML lo que simplifica enormemente trabajar con esta tecnología.
JAX-WS cuenta con una implementación de referencia llamada Metro, aunque también existen otros productos que implementan la especificación tales como CXF (el antiguo XFire) o Axis 2. En este tutorial vamos a partir de la base de que ya tenemos una aplicación web que utiliza Spring (al menos su contenedor IoC) y queremos publicar los métodos de un bean como un servicio web SOAP estándar para su consumo por parte de terceras aplicaciones. El módulo Spring WS permite la creación de servicios web Contract-first y un soporte (limitado) del estándar JAX-WS pero lo que nosotros queremos es generar el servicio web automáticamente a partir de nuestro código Java sin definir el WSDL.
Este es el primero de una serie de tutoriales dedicados a JAX-WS con CXF y Spring.
- Servidor
- Clientes
- Securización TLS + BASIC
- Handlers
Requisitos: Conocimientos básicos de Spring IoC, aplicaciones web Java, Maven y servicios SOAP
Resumen
Los pasos a seguir y que detalla el artículo son los siguientes
- Incluir las dependencias de Spring y CXF
- Configurar en el web.xml los servlets de Spring y CXF
- Si utilizamos log4j, configurar la redirección de los logs de CXF
- Anotar con @WebService la interfaz o clase cuyos métodos públicos se publicarán con SOAP
- En el applicationContext definir el endpoint para la clase/interfaz
- El WS queda disponible dentro de la aplicación en la ruta / [url-pattern de CXF en el web.xml] / [endpoint del WS en el applicationContext]
Proyecto para pruebas
Vamos a crear un proyecto web Java estándar con Maven que, tal y como veremos más adelante, consistirá únicamente en un bean de Spring que expondremos como un servicio web. Se utilizará Spring 4.3.22 y CXF 3.2.8, y para este último hay que añadir un par de dependencias: cxf-rt-frontend-jaxws y cxf-rt-transports-http
<?xml version="1.0" encoding="UTF-8" ?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.danielme.demo</groupId> <artifactId>spring-cxf-ws</artifactId> <version>1.0</version> <packaging>war</packaging> <description>Maven + JAX-WS + Spring + CXF web project just for playing around with SOAP web services </description> <inceptionYear>2013</inceptionYear> <licenses> <license> <name>GNU GENERAL PUBLIC LICENSE, Version 3.0</name> <url>http://www.gnu.org/licenses/gpl.html</url> </license> </licenses> <developers> <developer> <id>danielme.com</id> <name>Daniel Medina</name> </developer> </developers> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <java.version>1.7</java.version> <log4j.version>1.2.17</log4j.version> <cxf.version>3.2.8</cxf.version> <spring.version>4.3.22.RELEASE</spring.version> <maven.compiler.plugin.version>3.8.0</maven.compiler.plugin.version> <maven.war.plugin.version>3.2.2</maven.war.plugin.version> </properties> <build> <pluginManagement> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>${maven.compiler.plugin.version}</version> <configuration> <source>${java.version}</source> <target>${java.version}</target> <encoding>${project.build.sourceEncoding}</encoding> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>${maven.war.plugin.version}</version> <configuration> <archive> <manifestEntries> <Implementation-Version>${project.version} </Implementation-Version> <Implementation-Title>${artifactId}</Implementation-Title> <Extension-Name>${artifactId}</Extension-Name> <Built-By>Daniel Medina - danielme.com</Built-By> </manifestEntries> </archive> </configuration> </plugin> </plugins> </pluginManagement> </build> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.apache.cxf</groupId> <artifactId>cxf-rt-frontend-jaxws</artifactId> <version>${cxf.version}</version> </dependency> <dependency> <groupId>org.apache.cxf</groupId> <artifactId>cxf-rt-transports-http</artifactId> <version>${cxf.version}</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>${log4j.version}</version> </dependency> </dependencies> </project>
En el web.xml y utilizando la especificación Servlet 3.1 (requiere Tomcat 8), hacemos la siguiente configuración:
- Spring: Se inicia el contexto de Spring como en cualquier proyecto web con Spring IoC indicando los xml de configuración.
- CXF: Iniciamos su servlet, indicando la ruta relativa desde la que “colgaremos” nuestros servicios CXF. He utilizado /ws
<?xml version="1.0" encoding="UTF-8"?> <web-app id="spring-cxf-ws" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1"> <display-name>spring-cxf-ws</display-name> <!-- ==============Spring============= --> <context-param> <param-name>contextConfigLocation</param-name> <param-value> /WEB-INF/applicationContext.xml </param-value> </context-param> <listener> <listener-class> org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!-- ==============CXF============= --> <servlet> <servlet-name>cxf</servlet-name> <servlet-class>org.apache.cxf.transport.servlet.CXFServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>cxf</servlet-name> <url-pattern>/ws/*</url-pattern> </servlet-mapping> </web-app>
El fichero log4j.xml tendrá una configuración estándar en modo DEBUG para salida por consola y fichero.
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"> <!-- http://wiki.apache.org/logging-log4j/Log4jXmlFormat --> <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/"> <appender name="console" class="org.apache.log4j.ConsoleAppender"> <param name="Target" value="System.out" /> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="%d{MM-dd-yyyy hh:mm:ss,SSS a} %5p %C:%L - %m%n" /> </layout> </appender> <appender name="file" class="org.apache.log4j.RollingFileAppender"> <param name="file" value="logs/logger.log" /> <param name="MaxFileSize" value="5MB" /> <param name="MaxBackupIndex" value="10" /> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="%d{MM-dd-yyyy hh:mm:ss,SSS a} %5p %C:%L - %m%n" /> </layout> </appender> <root> <priority value="debug" /> <appender-ref ref="console" /> <!--<appender-ref ref="file" />--> </root> </log4j:configuration>
Para redireccionar los logs de CXF a log4j se creará el fichero de texto /src/main/resources/META-INF/org.apache.cxf.Logger que contendrá la siguiente línea: org.apache.cxf.common.logging.Log4jLogger
El applicationContext.xml de momento lo dejamos «vacío» pero con lo que necesitaremos.
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:jaxws="http://cxf.apache.org/jaxws" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd"> <import resource="classpath:META-INF/cxf/cxf.xml"/> <import resource="classpath:META-INF/cxf/cxf-extension-xml.xml"/> <import resource="classpath:META-INF/cxf/cxf-servlet.xml"/> </beans>
Desplegando en Tomcat 8, podemos comprobar si nuestra aplicación está operativa llamando a la url en la que se encuentran los servicios CXF, que en nuestro caso devolverá lo siguiente:
Creación del servicio web
El servicio web SOAP que se va a crear permitirá realizar ciertas operaciones con los jugadores de un equipo de fútbol. Este servicio web será en realidad un bean definido en Spring que implementará una interfaz y si bien ninguno de estos requisitos son obligatorios (podríamos publicar cualquier clase y esta no tiene que implementar una interfaz) es la práctica habitual. Además normalmente vamos a querer inyectar en el bean clases de servicio, DAOs, etc para implementar las funcionalidades. La clase para modelar los jugadores es la siguiente:
package com.danielme.demo.jaxws.cxf.model; /** * * @author danielme.com * */ public class Player { private String name; private int age; private int number; public Player() { super(); } public Player(int number, String name, int age) { super(); this.name = name; this.age = age; this.number = number; } @Override public String toString() { return number + " - " + name + " (" + age + ")"; } getter y setters... }
Y ahora vamos con la interfaz que define las operaciones sobre los jugadores
package com.danielme.demo.jaxws.cxf.ws; import java.util.List; import javax.jws.WebService; import com.danielme.demo.jaxws.cxf.model.Player; /** * * @author danielme.com * */ @WebService public interface ITeamService { List<Player> getTeam(); List<Player> getPlayers(int... numbers); boolean updatePlayerByNumber(int number, Player player); boolean deletePlayer(int number); void foo(); }
Obsérvese que para convertirla en la definición de un servicio web se ha anotado con @WebService. La configuración por defecto es suficiente, pero se pueden modificar ciertos parámetros a la hora de generar el WSDL y los mensajes SOAP correspondientes tal y como podemos ver en la documentación. El método foo() no hará nada, y luego veremos por qué se ha definido.
Por omisión, todos los métodos de la interfaz serán publicados como servicio web. Usaremos la configuración por defecto, pero se pueden personalizar un poco los métodos y sus parámetros, veamos un ejemplo:
@WebMethod(operationName="actualizar") @WebResult(name="booleano", targetNamespace="http://com.danielme.demo.resultado") boolean updatePlayerByNumber(int number, @WebParam(header = true, name = "jugador", targetNamespace = "http://com.danielme.demo.jugador")Player player);
- @WebMethod: personaliza la representación del método en el SOAP. En nuestro caso, establecemos el nombre de wsdl:operation
- @WebResult: permite personalizar el valor wsdl:part correspondiente con el valor devuelto por el método. En nuestro ejemplo, le hemos puesto el nombre booleano y un namespace propio
- @WebParam: permite personalizar el valor wsdl:part correspondiente a los parámetros que debe recibir el método. En nuestro ejemplo, le hemos definido el nombre, el namespace e indicamos que el valor «viajará» en el header del documento SOAP
Ahora vamos con la implementación de esta interfaz. Es un bean de Spring definido mediante anotación y que implementa la interfaz ITeamService. Con la anotación @BindingType establecemos que queremos utilizar el protocolo SOAP 1.2 (por defecto se utiliza la versión 1.1).
package com.danielme.demo.jaxws.cxf.ws; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import org.apache.log4j.Logger; import org.springframework.stereotype.Service; import javax.xml.ws.BindingType; import com.danielme.demo.jaxws.cxf.model.Player; @Service(value="teamService") @BindingType(javax.xml.ws.soap.SOAPBinding.SOAP12HTTP_BINDING) public class TeamServiceImpl implements ITeamService { private static final Logger logger = Logger.getLogger(TeamServiceImpl.class); private final List<Player> team; public TeamServiceImpl() { team = new LinkedList<Player>(); team.add(new Player(1, "David De Gea", 22)); team.add(new Player(2, "Rafael", 22)); team.add(new Player(3, "Patrice Evra", 31)); team.add(new Player(4, "Phil Jones", 21)); team.add(new Player(5, "Rio Ferdinand", 34)); team.add(new Player(7, "Antonio Valencia", 27)); team.add(new Player(8, "Anderson", 24)); team.add(new Player(10, "Wayne Rooney", 27)); } @Override public List<Player> getTeam() { logger.info("team requested"); return team; } @Override public List<Player> getPlayers(int... numbers) { List<Player> result = new LinkedList<Player>(); if (numbers != null) { Player player = null; for (int i = 0; i < numbers.length; i++) { player = findById(numbers[i]); if (player != null) { result.add(player); } } } logger.info("returning " + result.size() + " players"); return result; } @Override public boolean updatePlayerByNumber(int number, Player player) { logger.info("updating player " + number); Player playerOld = findById(number); boolean result = false; if (playerOld != null) { playerOld.setAge(player.getAge()); playerOld.setNumber(player.getNumber()); playerOld.setName(player.getName()); result = true; } else { logger.warn("player " + number + " not found"); } return result; } @Override public boolean deletePlayer(int number) { logger.info("deleting player " + number); Player player = findById(number); boolean result = false; if (player != null) { team.remove(player); result = true; } else { logger.warn("player " + number + " not found"); } return result; } @Override @WebMethod(exclude = true) public void foo() { logger.info("Hello world!!!"); } private Player findById(int number) { for (Player player : team) { if (player.getNumber() == number) { return player; } } return null; }
Ya sólo resta configurar el servicio en el applicationContext.xml de Spring. He añadido comentada cómo habría que hacer la configuración si queremos exponer como web service una clase cualquiera que no fuera un bean de Spring (si esta clase no implementa interfaz alguna las anotaciones de JAX-WS irían directamente en la misma). El resultado es el siguiente (no olvidar tampoco «activar» las anotaciones para la inyección de dependencias), obsérvese que al haber definido al menos un servicio podemos prescindir de los import de configuración de CXF que vimos anteriormente.
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:jaxws="http://cxf.apache.org/jaxws" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd"> <!-- activates annotations (@Service, @Component, @Autowired) --> <context:component-scan base-package="com.danielme.demo" /> <!-- publish the web service --> <!-- <jaxws:endpoint id="teamService" implementor="com.danielme.demo.jaxws.cxf.ws.TeamServiceImpl" address="/v/1/teamService" />--> <!-- references a spring bean --> <!-- <bean id="teamService" class="com.danielme.demo.jaxws.cxf.ws.TeamService"/>--> <jaxws:endpoint id="teamServiceWS" implementor="#teamService" address="/v/1/teamService"/> </beans>
Se ha indicado el bean de Spring que hay que exponer mediante SOAP y su ruta en el servidor que será relativa a la ruta en la que se ha configurado CXF en el web.xml. Con esto ya lo tenemos todo listo y podemos desplegar la aplicación para comprobar que nuestro servicio está operativo. Si accedemos a la url en la que «escucha» el servlet de CXF veremos un listado de todos los servicios desplegados.
Nota: se pueden ocultar los servicios mostrados en el listado.
<jaxws:endpoint id="teamServiceWS" implementor="#teamService" address="/v/1/teamService"> <jaxws:properties><entry key="org.apache.cxf.endpoint.private" value="true"/></jaxws:properties> </jaxws:endpoint>
En ocasiones puede ser muy útil activar el logging de CXF para cierto endpoint.
<jaxws:endpoint id="teamServiceWS" implementor="#teamService" address="/v/1/teamService"> <jaxws:features> <bean class="org.apache.cxf.feature.LoggingFeature" /> </jaxws:features> </jaxws:endpoint>
Los detalles de las peticiones SOAP se imprimen tanto al recibirse como al enviarse.
--------------------------- ID: 1 Response-Code: 200 Encoding: UTF-8 Content-Type: text/xml Headers: {} Payload: 22David De Gea122Rafael231Patrice Evra321Phil Jones434Rio Ferdinand527Antonio Valencia724Anderson827Wayne Rooney10 --------------------------------------
El descriptor de nuestro servicio que CXF ha generado «mágicamente» lo encontramos en la url
http://localhost:8080/spring-cxf-ws/ws/v/1/teamService?wsdl
<wsdl:definitions name="TeamServiceImplService" targetNamespace="http://ws.cxf.jaxws.demo.danielme.com/"> <wsdl:types> <xs:schema elementFormDefault="unqualified" targetNamespace="http://ws.cxf.jaxws.demo.danielme.com/" version="1.0"> <xs:element name="deletePlayer" type="tns:deletePlayer" /> <xs:element name="deletePlayerResponse" type="tns:deletePlayerResponse" /> <xs:element name="foo" type="tns:foo" /> <xs:element name="fooResponse" type="tns:fooResponse" /> <xs:element name="getPlayers" type="tns:getPlayers" /> <xs:element name="getPlayersResponse" type="tns:getPlayersResponse" /> <xs:element name="getTeam" type="tns:getTeam" /> <xs:element name="getTeamResponse" type="tns:getTeamResponse" /> <xs:element name="updatePlayerByNumber" type="tns:updatePlayerByNumber" /> <xs:element name="updatePlayerByNumberResponse" type="tns:updatePlayerByNumberResponse" /> <xs:complexType name="deletePlayer"> <xs:sequence> <xs:element name="arg0" type="xs:int" /> </xs:sequence> </xs:complexType> <xs:complexType name="deletePlayerResponse"> <xs:sequence> <xs:element name="return" type="xs:boolean" /> </xs:sequence> </xs:complexType> <xs:complexType name="getPlayers"> <xs:sequence> <xs:element maxOccurs="unbounded" minOccurs="0" name="arg0" type="xs:int" /> </xs:sequence> </xs:complexType> <xs:complexType name="getPlayersResponse"> <xs:sequence> <xs:element maxOccurs="unbounded" minOccurs="0" name="return" type="tns:player" /> </xs:sequence> </xs:complexType> <xs:complexType name="player"> <xs:sequence> <xs:element name="age" type="xs:int" /> <xs:element minOccurs="0" name="name" type="xs:string" /> <xs:element name="number" type="xs:int" /> </xs:sequence> </xs:complexType> <xs:complexType name="foo"> <xs:sequence /> </xs:complexType> <xs:complexType name="fooResponse"> <xs:sequence /> </xs:complexType> <xs:complexType name="updatePlayerByNumber"> <xs:sequence> <xs:element name="arg0" type="xs:int" /> <xs:element minOccurs="0" name="arg1" type="tns:player" /> </xs:sequence> </xs:complexType> <xs:complexType name="updatePlayerByNumberResponse"> <xs:sequence> <xs:element name="return" type="xs:boolean" /> </xs:sequence> </xs:complexType> <xs:complexType name="getTeam"> <xs:sequence /> </xs:complexType> <xs:complexType name="getTeamResponse"> <xs:sequence> <xs:element maxOccurs="unbounded" minOccurs="0" name="return" type="tns:player" /> </xs:sequence> </xs:complexType> </xs:schema> </wsdl:types> <wsdl:message name="getPlayers"> <wsdl:part element="tns:getPlayers" name="parameters"> </wsdl:part> </wsdl:message> <wsdl:message name="fooResponse"> <wsdl:part element="tns:fooResponse" name="parameters"> </wsdl:part> </wsdl:message> <wsdl:message name="getTeamResponse"> <wsdl:part element="tns:getTeamResponse" name="parameters"> </wsdl:part> </wsdl:message> <wsdl:message name="updatePlayerByNumber"> <wsdl:part element="tns:updatePlayerByNumber" name="parameters"> </wsdl:part> </wsdl:message> <wsdl:message name="foo"> <wsdl:part element="tns:foo" name="parameters"> </wsdl:part> </wsdl:message> <wsdl:message name="getPlayersResponse"> <wsdl:part element="tns:getPlayersResponse" name="parameters"> </wsdl:part> </wsdl:message> <wsdl:message name="updatePlayerByNumberResponse"> <wsdl:part element="tns:updatePlayerByNumberResponse" name="parameters"> </wsdl:part> </wsdl:message> <wsdl:message name="deletePlayer"> <wsdl:part element="tns:deletePlayer" name="parameters"> </wsdl:part> </wsdl:message> <wsdl:message name="getTeam"> <wsdl:part element="tns:getTeam" name="parameters"> </wsdl:part> </wsdl:message> <wsdl:message name="deletePlayerResponse"> <wsdl:part element="tns:deletePlayerResponse" name="parameters"> </wsdl:part> </wsdl:message> <wsdl:portType name="ITeamService"> <wsdl:operation name="deletePlayer"> <wsdl:input message="tns:deletePlayer" name="deletePlayer"> </wsdl:input> <wsdl:output message="tns:deletePlayerResponse" name="deletePlayerResponse"> </wsdl:output> </wsdl:operation> <wsdl:operation name="getPlayers"> <wsdl:input message="tns:getPlayers" name="getPlayers"> </wsdl:input> <wsdl:output message="tns:getPlayersResponse" name="getPlayersResponse"> </wsdl:output> </wsdl:operation> <wsdl:operation name="foo"> <wsdl:input message="tns:foo" name="foo"> </wsdl:input> <wsdl:output message="tns:fooResponse" name="fooResponse"> </wsdl:output> </wsdl:operation> <wsdl:operation name="updatePlayerByNumber"> <wsdl:input message="tns:updatePlayerByNumber" name="updatePlayerByNumber"> </wsdl:input> <wsdl:output message="tns:updatePlayerByNumberResponse" name="updatePlayerByNumberResponse"> </wsdl:output> </wsdl:operation> <wsdl:operation name="getTeam"> <wsdl:input message="tns:getTeam" name="getTeam"> </wsdl:input> <wsdl:output message="tns:getTeamResponse" name="getTeamResponse"> </wsdl:output> </wsdl:operation> </wsdl:portType> <wsdl:binding name="TeamServiceImplServiceSoapBinding" type="tns:ITeamService"> <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http" /> <wsdl:operation name="deletePlayer"> <soap:operation soapAction="" style="document" /> <wsdl:input name="deletePlayer"> <soap:body use="literal" /> </wsdl:input> <wsdl:output name="deletePlayerResponse"> <soap:body use="literal" /> </wsdl:output> </wsdl:operation> <wsdl:operation name="getPlayers"> <soap:operation soapAction="" style="document" /> <wsdl:input name="getPlayers"> <soap:body use="literal" /> </wsdl:input> <wsdl:output name="getPlayersResponse"> <soap:body use="literal" /> </wsdl:output> </wsdl:operation> <wsdl:operation name="foo"> <soap:operation soapAction="" style="document" /> <wsdl:input name="foo"> <soap:body use="literal" /> </wsdl:input> <wsdl:output name="fooResponse"> <soap:body use="literal" /> </wsdl:output> </wsdl:operation> <wsdl:operation name="updatePlayerByNumber"> <soap:operation soapAction="" style="document" /> <wsdl:input name="updatePlayerByNumber"> <soap:body use="literal" /> </wsdl:input> <wsdl:output name="updatePlayerByNumberResponse"> <soap:body use="literal" /> </wsdl:output> </wsdl:operation> <wsdl:operation name="getTeam"> <soap:operation soapAction="" style="document" /> <wsdl:input name="getTeam"> <soap:body use="literal" /> </wsdl:input> <wsdl:output name="getTeamResponse"> <soap:body use="literal" /> </wsdl:output> </wsdl:operation> </wsdl:binding> <wsdl:service name="TeamServiceImplService"> <wsdl:port binding="tns:TeamServiceImplServiceSoapBinding" name="TeamServiceImplPort"> <soap:address location="http://localhost:8080/spring-cxf-ws/ws/v/1/teamService" /> </wsdl:port> </wsdl:service> </wsdl:definitions>
Tal y como vemos todos los métodos de la interfaz se han publicado dentro del servicio web, ¿pero qué pasaría si no queremos exponer algún método? Puede ser el caso de que nuestro servicio de Spring sólo deba publicar al exterior un subconjunto de funcionalidades. Pues bien, vamos a evitar que se publique el método foo, y para ello anotamos el método de la siguiente forma siempre en la implementación y no en la interfaz:
@Override @WebMethod(exclude=true) public void foo(){ logger.info("Hello world!!!"); }
Si volvemos a desplegar la aplicación con este cambio podemos comprobar que ahora el método foo ya no forma parte de nuestro servicio web.
Ya tenemos listo nuestro servicio de Spring para ser consumido por otras aplicaciones. En la segunda parte del artículo veremos lo fácil que resulta hacerlo con JAX-WS, CXF y, opcionalmente, Spring. Para interactuar con servicios web SOAP utilizo habitualmente la herramienta Soap UI. Con la opción File->New SOAP Project creamos un proyecto para un WSDL y marcamos la opción «Create sample requests for all operations».
En el navegador de proyectos tendremos para cada operación una petición para poder probar el servicio.
Algunas limitaciones
La «magia» de JAX-WS está limitada por la tecnología subyacente, esto es, los mensajes XML SOAP. Esto nos obliga a tener cuidado con los servicios que publicamos, por ejemplo:
- Los parámetros utilizados no deben tener referencias cruzadas, esto es, constituir una estructura de datos recursiva. Esto es debido a que el mensaje de respuesta del WS es un XML en el que no se pueden representar estos datos. Si incurrimos en este error, se producirá un excepción en el servidor que recibiremos en el cliente con un mensaje que, afortunadamente, es bastante descriptivo y nos ayudará a identificar exactamente cuál es el error producido. Este mensaje comenzará más o menos así:
org.apache.cxf.interceptor.Fault: Marshalling Error: A cycle is detected in the object graph. This will cause infinitely deep XML
- Si queremos que el servicio sea totalmente estándar para usarlo con cualquier cliente/lenguaje, debemos utilizar como estructura de datos únicamente Arrays (o en su defecto List que son correctamente mapeados por CXF) en lugar de tipos propios de Java como HashMap o similares.
Código completo
El proyecto completo se encuentra en Github. Para más información sobre cómo utilizar GitHub, consultar este artículo.
Realmente muy buen aporte y esta super claro !!!
Hola,
Sé que este post es muy antiguo, pero me gustaría preguntar algo que no consigo entender.
Mi aplicación no tiene WS, consume los WS de otro proyecto externo al mío. En mi applicationContext.xml he añadido el código para que en el log aparezca en fomato XML la petición y respuesta de todas las llamadas a los diferentes WS con el siguiente código:
Pero por defecto me gustaría tenerlo desactivado para que los LOGS no se llenen con tanta información innecesaria (tal y como lo tenía antes de añadir el código anterior), pero no sé cómo ponerlo en el log4j.properties, ya que la única opción que he encontrado es volver a eliminar el trozo de código anterior, pero necesito tenerlo independiente en el log4j. He puesto log4j.logger.org.Apache.cxf=INFO así como eliminado dicha propiedad y el resultado es el mismo. ¿Podrías por favor decirme cómo debo proceder?
Muchas gracias 🙂