Web Services con Estado
Introducción
Este tutorial está motivado por la pregunta de un alumno de un curso, acerca de mantener una sesión de diálogo entre un web service y su cliente. Veamos un ejemplo de lo que se viene al caso a denominar web services con estado (stateful webservices).
El código fuente del tutorial puede descargarse desde aquí.
Requisitos
Partimos del siguiente software
- Implementación de referencia del estándar JAX-WS 2.0/2.1 (https://jax-ws.dev.java.net/), versión de distribución 2.1.4, que podemos descargar de aquí: https://jax-ws.dev.java.net/2.1.4/. JAX-WS es el núcleo de METRO (conjunto de componentes que forman una WS stack, o pila de web services, de Sun).
- Por comodidad, usaré Eclipse como IDE.
- Servidor web Apache Tomcat 6.0.18, que podremos descargar desde http://tomcat.apache.org/download-60.cgi.
Si estás con Windows vista, la manera más adecuada de instalar la distribución JAX-WS es mediante el comando indicado en la web de descarga, es decir:
1 |
C:\TutorialWS>java -jar JAXWS2.1.4-20080502.jar |
Creará la carpeta C:\TutorialWS\jaxws-ri. Crearemos una variable de entorno llamada JAXWS_HOME con el valor C:\TutorialWS\jaxws-ri y añadiremos JAXWS_HOME\bin al PATH del sistema.
Creación del web service con estado
Vamos a hacer un ejemplo relacionado con los tiempos que corren: un broker hace especulaciones en bolsa utilizando las operaciones expuestas por un web service con estado. El broker se validará, realizará operaciones y finalizará la ‘sesión’. Un web service habitual (sin estado y habitualmente síncrono) no sabría llevar la cuenta de lo ganado por el broker entre operación y operación, la información se pierde. Pero el que vamos a crear, sí mantiene memoria de lo acumulado, sin utilizar ningún tipo de persistencia en la lógica implementada.
En Eclipse creamos un nuevo proyecto web dinámico para facilitarnos posteriormente la generación del WAR, y agregamos las dependencias necesarias al Build Path, que serán los .jar existentes en JAXWS_HOME\lib.
Creamos un paquete llamado com.autentia.ws.bolsa, y ahí creamos las siguientes dos clases:
CuentaInversion.java:
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 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 |
package</code> com.autentia.ws.bolsa; import</code> java.util.Hashtable; import</code> java.util.Iterator; import</code> java.util.Map; import</code> java.util.Random; import</code> java.util.Set; import</code> javax.jws.WebMethod; import</code> javax.jws.WebService; import</code> javax.xml.ws.soap.Addressing; import</code> com.sun.xml.ws.developer.Stateful; import</code> com.sun.xml.ws.developer.StatefulWebServiceManager; /** * <p> * Cuenta de inversion de un broker. Trabaja con un conjunto de titulos * conocidos * </p> * <p> * El broker puede manejar su cuenta de inversion desde cualquier parte ya que * se ofrece como un web service con estado * </p> * * @author Ivan Garcia Puebla - www.autentia.com * @version 1.0 */ @Stateful @WebService @Addressing public</code> <code class="keyword">class</code> CuentaInversion { // cartera de titulos de companias private</code> Map<String, Activo> cartera; public</code> <code class="keyword">final</code> <code class="keyword">static</code> String IENEGE = <code class="quote">"IENEGE"</code>; public</code> <code class="keyword">final</code> <code class="keyword">static</code> String BEBEUVA = <code class="quote">"BEBEUVA"</code>; public</code> <code class="keyword">final</code> <code class="keyword">static</code> String TECNOGUAY = <code class="quote">"TECNOGUAY"</code>; public</code> <code class="keyword">final</code> <code class="keyword">static</code> String INVESTIS = <code class="quote">"INVESTIS"</code>; public</code> <code class="keyword">final</code> <code class="keyword">static</code> String CASARIS = <code class="quote">"CASARIS"</code>; /** * Alta de una cuenta de inversion */ public</code> CuentaInversion() { cartera = <code class="keyword">new</code> Hashtable<String, Activo>(5); cartera.put(IENEGE, <code class="keyword">new</code> Activo(IENEGE)); cartera.put(BEBEUVA, <code class="keyword">new</code> Activo(BEBEUVA)); cartera.put(TECNOGUAY, <code class="keyword">new</code> Activo(TECNOGUAY)); cartera.put(INVESTIS, <code class="keyword">new</code> Activo(INVESTIS)); cartera.put(CASARIS, <code class="keyword">new</code> Activo(CASARIS)); } /** * Metodo que compra titulos sobre un precio minimo * * @param precioMinimo * Precio minimo de compra * @param cantidad * Cantidad de titulos */ public</code> <code class="keyword">void</code> comprarPrecio(<code class="keyword">int</code> precioMinimo, <code class="keyword">int</code> cantidad) { if</code> (precioMinimo < 0 || cantidad < 0) return</code>; // compramos titulos sobre el precio minimo Set<String> set = cartera.keySet(); Iterator<String> it = set.iterator(); while</code> (it.hasNext()) { Activo a = cartera.get(it.next()); if</code> (a.getCotizacion() >= precioMinimo) a.adquirir(cantidad); } } /** * Adquirir titulos de una compania * * @param ticker * Ticker de la compania * @param cantidad * Cantidad de titulos */ public</code> <code class="keyword">void</code> comprarCompania(String ticker, <code class="keyword">int</code> cantidad) { Activo a = cartera.get(ticker); if</code> (a != <code class="keyword">null</code>) { a.getCotizacion(); a.adquirir(cantidad); } } /** * Obtiene los ingresos del broker * * @return Ingreso de la inversion actua */ public</code> <code class="keyword">int</code> getIngresos() { int</code> total = 0; Set<String> set = cartera.keySet(); Iterator<String> it = set.iterator(); while</code> (it.hasNext()) { total += ((Activo) cartera.get(it.next())).getMontante(); } return</code> total; } /** * * Este objeto es inyectado por la implementacion de referencia JAX-WS, y * ofrece varios metodos para manejar web services con estado. * */ public</code> <code class="keyword">static</code> StatefulWebServiceManager<CuentaInversion> manager; /** * Fin de sesion explicito para liberar memoria */ public</code> <code class="keyword">void</code> finSesion() { manager.unexport(<code class="keyword">this</code>); } /** * Entidad Activo Financiero * * @author Ivan Garcia Puebla - www.autentia.com * @version 1.0 */ class</code> Activo { private</code> String ticker; <code class="comment">// id de empresa que cotiza private</code> <code class="keyword">int</code> numero; <code class="comment">// cantidad de titulos adquiridos private</code> <code class="keyword">int</code> montante; <code class="comment">// capital de la inversion private</code> <code class="keyword">int</code> cotizacion; <code class="comment">// precio de cotizacion /** * Constructor */ public</code> Activo(String ticker) { this</code>.ticker = ticker; numero = 0; montante = 0; cotizacion = 0; } /** * @return el ticker del activo */ public</code> String getTicker() { return</code> ticker; } /** * @param ticker * establece el ticker del activo */ public</code> <code class="keyword">void</code> setTicker(String ticker) { this</code>.ticker = ticker; } /** * @return cantidad de titulos adquiridos */ public</code> <code class="keyword">int</code> getNumero() { return</code> numero; } /** * @return El montante */ public</code> <code class="keyword">int</code> getMontante() { return</code> montante; } /** * Obtiene el precio de cotizacion actual * * @return Precio actual */ public</code> <code class="keyword">int</code> getCotizacion() { Random random = <code class="keyword">new</code> Random(); cotizacion = random.nextInt(10); return</code> cotizacion; } /** * Método que efectua la compra de acciones al precio actual de * cotizacion * * @param numero * de acciones a comprar */ public</code> <code class="keyword">void</code> adquirir(<code class="keyword">int</code> cantidad) { numero += cantidad; montante += (cantidad * cotizacion); } } } |
El código es sencillo de entender. Disponemos de una clase Activo.java, que representa una entidad de tipo activo financiero. Este activo tiene unas variables de clase que almacenan su estado y un método para comprar determinadas cantidades de ellos.
La clase CuentaInversion.java es la que nos interesa, pues simplemente anotándola con @WebService, @Stateful y @Addressing la estamos convirtiendo en un web service con estado al que podemos invocar sea cual sea el protocolo de transporte. ¡Todo eso con 3 palabras! Small is Beauty 😉
Destacamos dos métodos:
- StatefulWebServiceManager, método que JAX-WS implementará por nosotros, encargado de manejar el estado. Es recomendable consultar el JAX-WS javadoc.
- finSesion(). Sino invocamos el unexport del manager, tendremos pérdida de memoria, al no librear recursos tras dejar de utilizar el web service. Otra solución es establecer un timeout, como indica la documentación del punto anterior.
La otra clase que compondrá la lógica de nuestro web service es Cartera.java:
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 |
package</code> com.autentia.ws.bolsa; import</code> javax.jws.WebMethod; import</code> javax.jws.WebService; import</code> javax.xml.ws.wsaddressing.W3CEndpointReference; /** * Este es un servicio web sin estado endpoint del que tiene estado * * @author Ivan Garcia Puebla - www.autentia.com * */ @WebService public</code> <code class="keyword">class</code> Cartera { /** * Metodo que proporciona al cliente una referencia a una cuenta de * inversion * * @param usuario * Usuario * @param password * Clave * @return Referencia a una instancia cuenta de inversion */ @WebMethod public</code> <code class="keyword">synchronized</code> W3CEndpointReference accesoCartera(String usuario, String password) { CuentaInversion cuenta = <code class="keyword">null</code>; if</code> (!validar(usuario, password)) return</code> <code class="keyword">null</code>; cuenta = <code class="keyword">new</code> CuentaInversion(); return</code> CuentaInversion.manager.export(cuenta); } // TODO refactorizar este metodo /** * Metodo de validacion * * @param usuario * Usuario * @param password * Clave * @return Acceso valido (true) o denegado (false) * */ protected</code> <code class="keyword">static</code> <code class="keyword">boolean</code> validar(String usuario, String password) { final</code> String usuarioSecreto = <code class="quote">"Alberto"</code>; final</code> String passwordSecreta = <code class="quote">"emc2"</code>; return</code> (usuarioSecreto.equals(usuario) && passwordSecreta .equals(password)); } } |
Esta clase también es un web service pero sin estado. Mediante el método accesoCartera() estamos validando un usuario y devolviendo al otro extremo de la invocación (el cliente), una referencia al web service con estado que realmente nos interesa.
Teniendo estas clases, podemos generar el web service con los comandos:
1 |
wsgen -cp . -keep com.autentia.ws.bolsa.CuentaInversion |
y
1 |
wsgen -cp . -keep com.autentia.ws.bolsa.Cartera |
Nos habrá generado el conjunto de clases necesarias para el funcionamiento del web service, en un nuevo paquete llamado com.autentia.ws.bolsa.jaxws.
Asimismo implementamos los ficheros web.xml y sun-jaxws.xml (más información en: Metro: pila de webservices de Sun). Puedes ver su contenido en la descarga de los fuentes del tutorial.
En este punto, el proyecto tiene el siguiente aspecto:
Proyecto del web service en Eclipse
A continuación generamos el ensamblado war: BrokerWS.war y lo copiamos en el directorio webapps de Tomcat para su despliegue. Podremos acceder al estado del web service en la dirección http://localhost:8080/BrokerWS/ws/cartera:
Estado de los web services cartera y cuentainversion en Tomcat
Pasamos a generar el cliente.
Creación del cliente del web service con estado
Creamos un nuevo proyecto de tipo Java en Eclipse, y agregamos las librerias de JAX-WS, como en el caso del proyecto del servidor.
Para generar las clases necesarias para la comunicación con el web service ya publicado, ejecutamos los comandos:
1 |
wsimport -s src -p com.autentia.broker -Xnocompile http://localhost:8080/BrokerWS/ws/cartera?wsdl |
y
1 |
wsimport -s src -p com.autentia.broker -Xnocompile http://localhost:8080/BrokerWS/ws/cuentainversion?wsdl |
y se habrán creado en el paquete que hemos especificado, com.autentia.broker. En un paquete com.autentia.broker.client creamos una clase llamada TestBroker.java donde implementaremos la lógica del cliente:
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 |
package</code> com.autentia.broker.client; import</code> com.autentia.broker.Cartera; import</code> com.autentia.broker.CarteraService; import</code> com.autentia.broker.CuentaInversion; import</code> com.autentia.broker.CuentaInversionService; /** * * <p> * Cliente del web service de sesion de bolsa de un broker * </p> * <p> * Ejemplo de uso de un web service con estado * </p> * * * @author Ivan Garcia Puebla - www.autentia.com * @version 1.0 * */ public</code> <code class="keyword">class</code> TestBroker { /** * @param args */ public</code> <code class="keyword">static</code> <code class="keyword">void</code> main(String[] args) { // creamos los objetos necesarios para el dialogo Cartera cartera = <code class="keyword">new</code> CarteraService().getCarteraPort(); CuentaInversionService cuentaService = <code class="keyword">new</code> CuentaInversionService(); try</code> { // nos validamos ante el servicio: CuentaInversion cuenta = cuentaService.getPort(cartera .accesoCartera(<code class="quote">"Alberto"</code>, <code class="quote">"emc2"</code>), CuentaInversion.<code class="keyword">class</code>); // realizamos operaciones sobre nuestra cuenta de inversion // compramos 25 titulos a mayor precio que 1$ cuenta.comprarPrecio(3, 25); cuenta.getIngresos(); System.out.println(<code class="quote">"Montante acumulado: "</code> + cuenta.getIngresos()); // compramos 100 titulos de una compania cuenta.comprarCompania(<code class="quote">"BEBEUVA"</code>, 100); System.out.println(<code class="quote">"Montante acumulado: "</code> + cuenta.getIngresos()); // compramos 33 titulos de una compania que no es del broker cuenta.comprarCompania(<code class="quote">"ssd"</code>, 33); System.out.println(<code class="quote">"Montante acumulado: "</code> + cuenta.getIngresos()); // compramos 1850 titulos de una compania cuenta.comprarCompania(<code class="quote">"CASARIS"</code>, 1850); System.out.println(<code class="quote">"Montante acumulado: "</code> + cuenta.getIngresos()); // finalizamos la sesion cuenta.finSesion(); } <code class="keyword">catch</code> (NullPointerException excepcionUsuarioNoAutorizado) { System.out.println(<code class="quote">"broker ID o password incorrectos"</code>); } } } |
El código se explica por sí solo.
En este punto el estado del proyecto en Eclipse es:
Proyecto del cliente del web service en Eclipse
Ejecutamos la clase Test.java y obtenemos lo siguiente:
Resultado de la inversión de nuestro broker
Todo esto puedes ejecutarlo tú mismo con el código fuente del tutorial, preparado para ser importado como ‘proyecto existente‘ en Eclipse (JEE). Sólo tendrás que añadir las librerías de JAXWS_HOME\lib a ambos proyectos.
Conclusión
Este tutorial muestra otra de las capacidades de los servicios web. Resulta cuando menos interesante seguir aprendiendo sobre ellos, ¿verdad David? 🙂
Buenas.
¿Podría mandarme alguien el wsdl generado para este web service?
Un saludo y muchas gracias de antemano.
Carlos.