Spring WS: Construcción de Clientes de Servicios Web con Spring
En el siguiente tutorial vamos a ver algunas de las aportaciones que nos ofrece Spring en relación a la construcción de clientes
de servicios web.
Se presupone que el lector ya posee conocimientos de Servicios Web, Maven y Spring.
Si estás interesado en la construcción de servicios web con Spring, puedes ver el siguiente tutorial
Índice de contenido:
- Introducción.
- Ejemplo de construcción de un cliente de servicio web con Spring.
- Entorno.
- Describiendo la petición y la respuesta de comunicación con el servicio web.
- Código fuente del cliente.
- Archivo de configuración de Spring 2
(/main/resources/applicationContext.xml)
. - Archivo de configuración de Maven 2: pom.xml.
- Configuración de logs para monitorizar el tráfico SOAP:
/main/resources/log4j.xml
. - Invocación del servicio Web.
- Referencias
- Conclusiones
Introducción
Al igual que en otros temas en Spring como JMS o DAO, Spring apuesta por el uso de plantillas que nos ahorren tener que
realizar las tareas comunes evitándonos además cometer ciertos errores ligados a estas partes.
Por ejemplo, en el caso de DAO, las plantillas nos evitan tener que abrir conexiones, liberar recursos, etc.
En el caso de los servicios web, Spring nos proporciona la plantilla org.springframework.ws.client.core.WebServiceTemplate
a tráves de la cual podremos enviar
mensajes (payload) a un servicio Web, y será la plantilla la que se encarge de envolverla en sobres SOAP, de establecer conexiones, liberar recursos, controlar errores de obligada captura, etc.
Además esta plantilla nos proporciona imnumerables métodos para enviar mensajes con cualquier tipo de mensajería y médio transporte.
Cabe destacar que para construir un cliente de un servicio web usando Spring necesitas entender el WSDL que define el servicio web, es decir:
- Protocolo de comunicación.
- Dónde escucha la peticiones.
- Formatos peticiones, respuestas y fallos.
Entender un WSDL no es tan dificil como a veces se puede pensar, en un par de tardes con ganas los lees sin problemas 🙂
Nuestro cliente usará por dentro una instancia de WebServiceTemplate
(normalmente inyectada por Spring) y está plantilla se deberá de configurar dos parámetros (en la configuración de Spring):
- La fábrica de mensajes SOAP: Envuelve el Payload que envias a través de la plantilla en sobres SOAP Envelope.
- La clase de envio de menssajes SOAP: Se encarga del envio en si del mensaje construido anteriormente.
Tendrás información más detallada en los comentarios del archivo de configuración de Spring que se verá más adelante en este mismo tutorial.
Ejemplo de construcción de un cliente de servicio web con Spring:
A continuación vamos a ver un completo ejemplo en donde construiremos un cliente para el Servicio Web de búsqueda de libros que definimos en un tutorial anterior. Ver tutorial.
El código fuente de este tutorial puede ser descargado desde aquí (proyecto Eclipse con Maven 2).
Entorno
El siguiente ejemplo está construido en el siguiente entorno:
- HP Pavilion.
- Windows Vista Home Premium.
- Eclipse Ganymede.
- Java 6.
- Maven 2.
- Plugin Maven 4QE para Eclipse.
Describiendo la petición y la respuesta de comunicación con el servicio web:
Spring SÓLO APUESTA por el modelo contrato-primero (Contract-First), por lo que debemos de enviar y recibir la información con el formato que se especifique en el contrato del servicio Web (WSDL)
.
A continuación vemos el formato de datos de la comunicación con el servicio web y al cual debemos ceñirnos estrictamente.
- Formato de la petición a enviar al servicio web.
- Formato de la respuesta que nos genera el servicio web.
Código fuente del cliente:
com.autentia.tutoriales.spring.ws.biblioteca.cliente.BibliotecaCliente
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
package com.autentia.tutoriales.spring.ws.biblioteca.cliente; import java.util.ArrayList; import java.util.List; import java.io.StringReader; import javax.xml.transform.dom.DOMResult; import javax.xml.transform.stream.StreamSource; import org.springframework.ws.client.core.WebServiceTemplate; import org.w3c.dom.*; /** * Spring cliente WS del servicio de consulta de libros * @author Carlos García. Autentia. * @see http://www.mobiletest.es */ public class BibliotecaCliente { private final String URI = "http://localhost:8080/bibliotecaWS/services"; private WebServiceTemplate webServiceTemplate; /** * Será inyectada por Spring */ public void setWebServiceTemplate(WebServiceTemplate webServiceTemplate) { this.webServiceTemplate = webServiceTemplate; } /** * @param categoria Categoría del libro a consultar * @param nivel Nivel (avanzado, medio o básico) * @return Devuelve la lista de libros que cumplen los criterios especificados */ public java.util.List<Libro> getLibros(String categoria, String nivel) { String xmlRequest = this.getPeticion(categoria, nivel); StreamSource peticion = new StreamSource(new StringReader(xmlRequest)); DOMResult respuesta = new DOMResult(); List<Libro> libros = null; boolean hayRespuesta = webServiceTemplate.sendSourceAndReceiveToResult(URI, peticion, respuesta); if (hayRespuesta){ libros = this.resultToBooks((Document) respuesta.getNode()); } return libros; } /** * @return Genera la petición (payload) que se enviará al servicio Web */ private String getPeticion(String categoria, String nivel){ StringBuffer buffer = new StringBuffer(1024); buffer.append("<BooksInfoRequest xmlns='http://www.adictosaltrabajo.com/spring/ws/schemas'>"); buffer.append("<categoria>"); buffer.append(categoria); buffer.append("</categoria>"); buffer.append("<nivel>"); buffer.append(nivel); buffer.append("</nivel>"); buffer.append("</BooksInfoRequest>"); return buffer.toString(); } /** * @return Devuelve una lista de Libro a partir del DOM */ private List<Libro> resultToBooks(Document doc){ NodeList nodos = doc.getFirstChild().getChildNodes(); Node current = null; Libro libro = null; ArrayList<Libro> libros = new ArrayList<Libro>(); for (int i = 0, num = nodos.getLength(); i < num; i++){ current = nodos.item(i); libro = new Libro(); libro.setEditorial(this.getProperty(current, "editorial")); libro.setTitulo(this.getProperty(current, "titulo")); libro.setPrecio(Integer.parseInt(this.getProperty(current, "precio"))); libro.setPaginas(Integer.parseInt(this.getProperty(current, "paginas"))); libros.add(libro); } return libros; } /** * * @param node Nodo padre en el cual se busca la propiedad * @param tag Nombre del elemento buscado * @return Devuelve el valor de un elemento <padre><tag1>xxx<tag1><tagN>xxx<tagN></padre> */ private String getProperty(Node nodo, String tag){ NodeList props = nodo.getChildNodes(); Node prop = null; String value = null; for (int j = 0, num = props.getLength(); j < num; j++){ prop = props.item(j); if (tag.equals(prop.getNodeName())){ value = prop.getFirstChild().getNodeValue(); break; } } return value; } } |
com.autentia.tutoriales.spring.ws.biblioteca.cliente.Libro
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
package com.autentia.tutoriales.spring.ws.biblioteca.cliente; /** * Representación de un libro * @author Carlos García. Autentia * @see http://www.mobiletest.es */ public class Libro { private String editorial; private String titulo; private int paginas; private int precio; public String getEditorial() { return editorial; } public void setEditorial(String editorial) { this.editorial = editorial; } public String getTitulo() { return titulo; } public void setTitulo(String titulo) { this.titulo = titulo; } public int getPaginas() { return paginas; } public void setPaginas(int paginas) { this.paginas = paginas; } public int getPrecio() { return precio; } public void setPrecio(int precio) { this.precio = precio; } } |
Archivo de configuración de Spring 2 (/main/resources/applicationContext.xml)
:
El archivo está autocomentado.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
<?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:util="http://www.springframework.org/schema/util" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.5.xsd"> <!-- Wrapper del cliente --> <bean id="biblioStub" class="com.autentia.tutoriales.spring.ws.biblioteca.cliente.BibliotecaCliente"> <property name="webServiceTemplate" ref="wsTemplate"/> </bean> <!-- Plantilla para comunicarnos con el WS --> <bean id="wsTemplate" class="org.springframework.ws.client.core.WebServiceTemplate"> <property name="defaultUri" value="http://localhost:8080/bibliotecaWS/services"/> <!-- Creación de mensajes SOAP --> <property name="messageFactory"> <!-- Saaj usa DOM, si se quiere más rendimiento y consumir menos recursos puede usar: org.springframework.ws.soap.axiom.AxiomSoapMessageFactory que usa AXIOM. --> <bean name="innermf" class="org.springframework.ws.soap.saaj.SaajSoapMessageFactory"> <property name="soapVersion"> <util:constant static-field="org.springframework.ws.soap.SoapVersion.SOAP_12"/> </property> </bean> </property> <!-- Envio de mensajes, Spring proporciona dos clases el envio de mensajes sobre HTTP: a) org.springframework.ws.transport.http.HttpUrlConnectionMessageSender: Usa HTTPConnection (funcionalidad limitada) b) org.springframework.ws.transport.http.CommonsHttpMessageSender: Usa HTTPClient (proporciona más funcionalidad) http://www.adictosaltrabajo.com/tutoriales/tutoriales.php?pagina=HTTPClient --> <property name="messageSender"> <bean class="org.springframework.ws.transport.http.HttpUrlConnectionMessageSender"/> </property> </bean> </beans> |
Archivo de configuración de Maven 2: pom.xml
:
A continuación exponemos el archivo de configuración de Maven, se presupone que el lector ya tiene nociones de Maven.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
<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/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.autentia.tutoriales</groupId> <artifactId>bibliotecaWSClienteSpring</artifactId> <packaging>jar</packaging> <version>1.0-SNAPSHOT</version> <name>bibliotecaWSClienteSpring</name> <url>http://www.adictosaltrabajo.com</url> <build> <plugins> <!-- Sintáxis Java 5 --> <plugin> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.5</source> <target>1.5</target> <encoding>UTF-8</encoding> </configuration> </plugin> </plugins> </build> <dependencies> <!-- Clases de Spring WS --> <dependency> <groupId>org.springframework.ws</groupId> <artifactId>spring-ws-core</artifactId> <version>1.5.6</version> </dependency> <!-- Para volcar las trazas. (sólamente dejamos trazas SOAP) --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.14</version> </dependency> <!-- Para nuestros tests --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.3.1</version> <scope>test</scope> </dependency> </dependencies> </project> |
Configuración de log para monitorizar el tráfico SOAP: /main/resources/log4j.xml
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE log4j:configuration SYSTEM "dtds/log4j.dtd"> <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" debug="false"> <!-- Las trazas SOAP irán a parar al siguiente archivo: --> <appender name="soapFile" class="org.apache.log4j.RollingFileAppender"> <param name="File" value="bibliotecaWSClienteSOAP.log" /> <param name="MaxFileSize" value="2000000" /> <param name="MaxBackupIndex" value="5" /> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="%n%d{yyyy-MM-dd HH:mm:ss} [%-5p] [%l] %n%m%n" /> </layout> </appender> <!-- Monitorizamos el tráfico SOAP --> <category name="org.springframework.ws.client.MessageTracing"> <priority value="debug" /> <appender-ref ref="soapFile" /> </category> </log4j:configuration> |
Invocación del servicio Web:
A continuación nos creamos un test funcional con JUnit para probar la invocación y respuesta del servicio Web.
com.autentia.tutoriales.spring.biblioteca.cliente.test.BibliotecaTest:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
package com.autentia.tutoriales.spring.biblioteca.cliente.test; import org.junit.Assert; import org.springframework.context.ApplicationContext; import org.springframework.context.support.FileSystemXmlApplicationContext; import com.autentia.tutoriales.spring.ws.biblioteca.cliente.BibliotecaCliente; import com.autentia.tutoriales.spring.ws.biblioteca.cliente.Libro; /** * Tests de verificación del servicio web de consulta de libros * @author Carlos García. Autentia * @see http://www.mobiletest.es */ public class BibliotecaTest { private ApplicationContext factory; /** * Inicializamos el contexto de Spring */ @org.junit.Before public void initTests(){ this.factory = new FileSystemXmlApplicationContext("classpath:applicationContext.xml"); } /** * En verdad esto no es un test, simplemente lo pongo aqui por comodidad y no * tener que crearme un proyecto independenciente con una aplicación que realize lo mismo * que puedo hacer con este "test". */ @org.junit.Test public void test1(){ BibliotecaCliente stub = (BibliotecaCliente) factory.getBean("biblioStub"); java.util.List<Libro> libros = stub.getLibros(".net", "avanzado"); if (libros != null){ for (int i = 0, lcount = libros.size(); i < lcount; i++){ Libro libro = libros.get(i); System.out.println(libro.getEditorial() + " " + libro.getTitulo() + " " + libro.getPaginas() + " " + libro.getPrecio()); } } else { System.out.println("No hay libros"); } Assert.assertTrue(true); } } |
Y para terminar, ejecutamos los tests y vemos la salida generada mvn test
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
........ ........ Editorial libro 0 Titulo libro 0 100 50 Editorial libro 1 Titulo libro 1 101 51 Editorial libro 2 Titulo libro 2 102 52 Editorial libro 3 Titulo libro 3 103 53 Editorial libro 4 Titulo libro 4 104 54 Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.681 sec Results : Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 ........ ........ |
Vemos los logs que nos ha dejado la aplicación (bibliotecaWSClienteSOAP.log)
:
1 2 3 4 5 |
2009-03-29 23:05:39 [TRACE] [org.springframework.ws.client.core.WebServiceTemplate.sendRequest(WebServiceTemplate.java:581)] Sent request [<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope"><env:Header/><env:Body><BooksInfoRequest xmlns="http://www.adictosaltrabajo.com/spring/ws/schemas"><categoria>.net</categoria><nivel>avanzado</nivel></BooksInfoRequest></env:Body></env:Envelope>] 2009-03-29 23:05:39 [TRACE] [org.springframework.ws.client.core.WebServiceTemplate.logResponse(WebServiceTemplate.java:637)] Received response [<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope"><env:Header/><env:Body><BooksInfoResponse xmlns="http://www.adictosaltrabajo.com/spring/ws/schemas"><libro><editorial>Editorial libro 0</editorial><titulo>Titulo libro 0</titulo><paginas>100</paginas><precio>50</precio></libro><libro><editorial>Editorial libro 1</editorial><titulo>Titulo libro 1</titulo><paginas>101</paginas><precio>51</precio></libro><libro><editorial>Editorial libro 2</editorial><titulo>Titulo libro 2</titulo><paginas>102</paginas><precio>52</precio></libro><libro><editorial>Editorial libro 3</editorial><titulo>Titulo libro 3</titulo><paginas>103</paginas><precio>53</precio></libro><libro><editorial>Editorial libro 4</editorial><titulo>Titulo libro 4</titulo><paginas>104</paginas><precio>54</precio></libro></BooksInfoResponse></env:Body></env:Envelope>] for request [<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope"><env:Header/><env:Body><BooksInfoRequest xmlns="http://www.adictosaltrabajo.com/spring/ws/schemas"><categoria>.net</categoria><nivel>avanzado</nivel></BooksInfoRequest></env:Body></env:Envelope>] |
Referencias
- http://static.springframework.org/spring-ws/sites/1.5/reference/html/index.html
- http://static.springsource.org/spring-ws/sites/1.5/reference/html/client.html
Conclusiones
Si lo piensas bien, este forma de comunicación con servicios Web puede parecer que requiere un gran esfuerzo en comparación con otras técnicas, pero no es para tanto, usando técnicas
OXM (Object XML Mapping) y el uso de plantillas (WebServiceTemplate
) el esfuerzo se reduce enormemente y lo que es mejor, para las personas que nos gusta tener el control sobre lo que está pasando, esta solución es ideal,
pues todo son mensajes en XML fácilmente depurables.
Otra cosa, recuerda que esto no es más que un tutorial, hay más puntos al respecto sobre los que profundizar.
En Autentia impartimos constantemente cursos a medida sobre estas y otras tecnologías, espero nos tengais en cuenta en caso de que necesiteis formación.
Un saludo, espero que os haya parecido útil este tutorial.
Carlos García. Creador de MobileTest, un complemento educativo para los profesores y sus alumnos.
Hola, lo primero de todo, gracias por el tutorial. Es muy bueno.
Aplicando dicho tutorial a mi aplicación, resulta que cuando ejecuta la línea:
boolean hayRespuesta = webServiceTemplate.sendSourceAndReceiveToResult(URI, peticion, respuesta);
realiza la petición al WS, y devuelve la respuesta (lo veo a través del log de SOAP), pero resulta que \\\»respuesta\\\» está a null e incluso, la variable \\\»peticion\\\», que antes contenía el string de la peticón xml, ahora también está a null. ¿Alguna sugerencia?
Gracias.