miércoles, 23 de diciembre de 2009

Creando un servicio web a partir de su interfaz WSDL

Fte: http://www.adictosaltrabajo.com/tutoriales/tutoriales.php?pagina=WSDL2Java

En este tutorial se resalta la importancia de definir la interfaz de un servicio web antes de implementarlo, y cómo hacer todo esto con Eclipse y Apache Axis v1.

Por Javier Cámara (jcamara@softwareag.es)
Arquitecto de software en el BCS Competence Center de Software AG España.

Introducción

Una de las tareas más importantes a la hora de crear una SOA es definir su modelo de servicios: o sea, qué servicios hay y qué tareas en concreto hace cada uno. Esto aclara mucho las tareas a realizar por el sistema y qué elemento del mismo las llevará a cabo, y permite validar que esos elementos implementarán las necesidades del usuario, previamente definidas. Lo cual en cualquier sistema es siempre el grueso del diseño arquitectónico del mismo. Por ello, de la bondad de este resultado depende en gran parte el éxito de la SOA.

Más tarde o más temprano, en una SOA basada en servicios web este modelo se plasmará en documentos WSDL que definan en detalle las interfaces de cada servicio: operaciones, datos recibidos, datos devueltos y errores que pueden ocurrir. Estos WSDLs son casi imprescindibles a la hora de crear los clientes de un servicio, pues facilitan enormemente la tarea de invocarlo y gestionarlo.

La mayoría de las herramientas de creación de servicios web, como Apache Axis o Visual Studio .Net, facilitan que primero se implemente el servicio (o un esqueleto del mismo), por ejemplo en Java, y a partir de él se genere automáticamente el WSDL. Pero lo cierto es que esta forma de trabajar que promueven estas herramientas es incorrecta por varias razones. Para empezar, lo normal es crear la interfaz de algo antes de implementarlo, lo que permite crear los clientes y los servidores en paralelo. Pero sobre todo es que si creamos el WSDL a partir del código, tenemos bastantes posibilidades de que los detalles de este WSDL dependan de la herramienta que hemos usado para generarlo. Por tanto, si luego queremos que ese servicio sea implementado usando una herramienta diferente, o incluso por una versión superior de esa misma herramienta, puede que ese WSDL cambie, con lo cual tendríamos que cambiar los clientes de ese servicio. Por ello, lo apropiado es que la herramienta se adapte al WSDL, y no al revés.

Estos posibles problemas no son imaginaciones, y ha habido proyectos en Software AG que han tenido problemas por esto, por ejemplo para llamar a servicios creados con Axis desde el Sun Java Web Services Developer Pack (JWSDP). Los WSDLs generados por Axis pueden contener referencias a tipos de Java (que no funcionan en .Net), o a construcciones propias de Axis (que no funcionan en otros clientes de servicios web Java), o incluso cuya sintaxis no sigue siquiera el estándar WSDL. Eso sí, un cliente Axis no tiene problemas para conectarse a un servicio web Axis. Pero para conseguir eso no necesitábamos todas estas complejidades; el beneficio clave de los servicios web es la interoperabilidad, que se fundamenta en la independencia de las plataformas.

Por todo eso, lo aconsejable es crear el WSDL antes del código, y no al revés. De esta forma el modelo de los servicios de nuestra SOA no dependerá de las herramientas con las que se implemente, sino al revés, y los clientes de los servicios pueden conectarse a ellos independientemente de si está implementado con Axis, con JWSDP, con .Net o con cualquier otra herramienta. Y eso es lo que vamos a ver en este tutorial, usando Apache Axis, claro, que para eso es el más popular. Como todos los entornos de servicios web, Axis trae una herramienta para crear esqueletos de servicios a partir de WSDL, tanto para los clientes como para los servicios, llamada WSDL2Java. No es la herramienta definitiva: el código que genera a veces no compila, y cuando compila puede no cumplir la interfaz definida por el WSDL. Pero bueno, en muchos casos sí funciona bien, y en cualquier caso cuando no lo hace nos da una aproximación al resultado que podemos luego completar.

En este tutorial vamos a usar todo el rato Java, así que si tu PC no lo tiene, tendrás que instalarte el Developer Kit de Java Standard Edition descargándotelo desde http://java.sun.com/javase/downloads/.

El resto del tutorial contiene las siguientes secciones:

Diseñando nuestra interfaz
Diseñando el WSDL en Eclipse
Generando el esqueleto de nuestro servicio
Disgresiones adicionales
Diseñando nuestra interfaz

Como hemos dicho, antes que ponernos a implementarlo primero hay que pensar cuál será la interfaz de nuestro servicio. En nuestro caso vamos a crear un servicio que podría ser usado por un banco y que informaría de las sucursales del mismo que están próximas a un código postal. Por ello, tendría una interfaz como esta:

Entrada: código postal (ej. "28760"), o parte del mismo (ej. "28" o "760")
Salida: lista de sucursales cuyo código postal contiene al menos parte del recibido, posiblemente vacía. A su vez, para cada sucursal se informará de lo siguiente:
Código de la sucursal
Dirección
Código postal
Ahora tenemos que plasmar eso en WSDL. WSDL es un lenguaje XML y se puede escribir a mano, pero es más complicado de lo que uno se imaginaría y no es lo más divertido o sencillo de escribir, así que lo normal es usar una herramienta. Hay una bastante buena para ello, el XML Spy, pero la versión gratuita del mismo no permite crear WSDLs; así que usaremos otra gratuita que se puede usar en Eclipse.

Diseñando el WSDL en Eclipse

Para esto vamos a tener que hacer las siguientes cosas que se describen a continuación:

Instalar Eclipse
Instalar Tomcat
Creación de un proyecto web dinámico
El portType de WSDL
El XML Schema de nuestros mensajes
El binding de WSDL
El servicio en WSDL
Instalar Eclipse

Para poder manejar WSDLs en Eclipse vamos a usar el Web Tools Project. En este tutorial utilizaremos la versión 1.5 de Junio de 2006, que precisa de Eclipse 3.2. Si tienes Eclipse 3.2 puedes instalarte los numerosos plugins que las webtools contienen y necesitan, pero aquí vamos a tirar por el camino fácil: vamos a descargar una versión preconfigurada de Eclipse 3.2 con todo lo necesario para el WTP, o sea el archivo wtp-all-in-one-sdk-R-1.5.0-200606281455-win32.zip, que ocupa 182Mb.

Una vez descomprimido ese ZIP en un directorio de nuestra elección, ejecutamos el eclipse.exe que contiene y, después de un ratito, ya tendremos el Eclipse arrancado.

Si tu PC necesita de un proxy para salir a Internet, te aconsejo que configures el Eclipse para que lo use, pues debido al uso de XML Schema externos, a veces se intentará conectar al exterior para recuperar esos esquemas (ej. debido al web.xml a veces intentará recuperar http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd). Si estamos detrás de un proxy, no podrá hacerlo y tardará un rato, retardando nuestro trabajo. Aquí no se cuenta cómo se hace, pero está en las preferencias de Eclipse y es sencillo.

Instalar Tomcat

Para evitarnos problemas futuros como que Eclipse nos cree un proyecto web que luego dice que no se puede desplegar porque no está soportado por el servidor, o que no podamos cambiar cuál es el servidor de un proyecto debido a errores esotéricos, lo mejor será que definamos cuanto antes en Eclipse el servidor de aplicaciones que vamos a usar. Como de costumbre, en este tutorial utilizaremos Apache Tomcat, que es gratis y conveniente. Para instalarlo en Eclipse:

Descarga Tomcat desde http://tomcat.apache.org/ e instálalo (aquí no contamos cómo se hace, pero es fácil)
Una vez instalado, defínelo en Eclipse así:




Para nuestro tutorial esto ya vale, pero si queremos que ese Tomcat pueda ejecutar JSPs, debe tener acceso no sólo a un JRE sino a un JDK. Si ese es tu caso, además de ésto primero tienes que tener instalado un JDK, luego lo defines en Eclipse desde ese botón de "Installed JREs", y lo asocias a Tomcat.

Por cierto, un comentario relevante respecto a este Tomcat: las aplicaciones web que ejecutes con él desde dentro de Eclipse no serán las que estén en el directorio webapps del propio Tomcat, sino que Eclipse utiliza su propio directorio de despliegue y fichero de configuración de servidor. Este fichero de configuración lo puedes ver y editar en la ventana de layout del Eclipse, y ese directorio de despliegue está en tu (Eclipse workspace)\.metadata\.plugins\org.eclipse.wst.server.core\tmp0\webapps .

Creación de un proyecto web dinámico

Lo siguiente que haremos será crear un nuevo Proyecto web, diciéndole que lo despliegue en ese servidor que hemos creado:



Lo llamaremos sucursales:


Nos preguntará si queremos cambiar a la perspectiva J2EE. En este tutorial no se ha hecho así, pero yo creo que da igual.

Una vez creado el proyecto, crearemos un nuevo archivo WSDL dentro de él. Intentaremos explicar la menor cantidad posible de conceptos de WSDL, pues la versión 1 de este estándar tiene un modelo innecesariamente complejo (ya ha salido la versión 2, pero aún no la soporta casi nadie, y he leído alguna crítica demoledora).

Hay diversas variantes para definir nuestro servicio, pero nosotros no vamos a entrar en ellas y nos quedaremos directamente con la variante document/literal, que es la que ofrece mayor interoperabilidad. Aunque no entremos en detalles, quiero señalar que otra opción llamada "RPC/literal", al contrario de lo que mucha gente piensa, es igualmente válida y estándar en lo referente a interoperabilidad, y más sencilla en muchos casos. Pero lo cierto es que document/literal es la que se está popularizando más.

El wizard de las web tools ofrece bastantes ayudas para editar el WSDL y nos creará ya un esqueleto de para que luego lo adaptemos:



¿Cómo llamar a nuestro servicio? A mí esto de llamar a los servicios "AlgoServicio" me parece tan redundante como llamar a las clases "AlgoClase", así que como nuestro servicio va a manejar datos de sucursales, lo llamaremos Sucursales.wsdl:


Y cambiaremos el Target namespace y su prefijo por algo adaptado a nuestro ejemplo:


Así, el wizard ya nos creará un esqueleto de servicio, con una operación llamada NewOperation:


Y ahora vamos a adaptar ese esqueleto a nuestras necesidades.

El portType de WSDL

"PortType" es un término poco afortunado de WSDL v1 para referirse a una interfaz independiente del medio de transporte: define qué operaciones tiene nuestro servicio y qué recibe y qué devuelve cada una, de forma independiente del mecanismo de comunicación usado (SOAP, HTTP GET, correo-e, etc). Esta aparente flexibilidad de definir una única interfaz abstracta válida para diferentes transportes ha demostrado ser inútil en el mundo real, con lo que es una de las complejidades innecesarias que WSDL v1 incorpora a la vida del desarrollador. Pero es lo que hay.

Esa interfaz abstracta está compuesta de una serie de operaciones, y a su vez cada una de ellas puede tener una entrada y una salida (también declarar errores, pero eso no lo vemos aquí). Esas entrada y salida se define cada una en base a un mensaje. Cualquiera pensaría que un mensaje es simplemente un documento XML, y en nuestro caso de document/literal esto es prácticamente así, pero en otros casos no lo es. Pero bueno, el caso es que nosotros tenemos que definir la estructura XML de esos mensajes de entrada y salida en base a elementos en XML Schema.

En fin, el wizard ya nos ha creado un portType llamado Sucursales, con una operación y elementos XML asociados para su entrada y salida. Inicialmente la operación se llama NewOperation, y pinchando sobre ese nombre podemos cambiarlo por buscaSucursales, que es el que queremos nosotros:


Ahora vamos a definir exactamente el XML que esa operación recibirá y devolverá.

El XML Schema de nuestros mensajes

En WSDL la definición de nuestros mensajes puede estar incrustada dentro del propio WSDL, o ser incluida desde documentos externos. Ésto último suena más apropiado para un entorno de sistemas de información empresariales, pero lo cierto es que más frecuentemente los tipos van definidos dentro del propio WSDL, y algunas herramientas dan problemas si no es así.

Sea como sea, en nuestro caso para definir ese XML debemos abrir el XML schema incrustado en nuestro WSDL, lo que hacemos desde la ventanita de Outline:



Como vemos, el wizard ya nos ha creado esqueletos de las estructuras XML de entrada y salida de nuestra operación (incluso las ha renombrado cuando la hemos renombrado, siguiendo las conveniciones Request y Response), pero ahora tenemos que cambiarlas según nuestras necesidades.

En el esqueleto, el elemento XML que define el XML que recibiremos, buscaSucursalesRequest, es un elemento simple que sólo contiene una string. Eso podría valernos puesto que sólo pediremos un trozo de código postal, pero va a quedar más claro si creamos un elemento con un nombre más indicativo para enviar ese trozo de código postal, como "parteCodPostal" o algo.

Para eso tenemos que hacer que buscaSucursalesRequest sea un elemento complejo que contenga un "parteCodPostal". Esto, que es algo muy frecuente en XML Schema, por alguna razón el editor de XML Schema de Eclipse no nos lo pone fácil. Según él tendríamos que crear un tipo con nombre, en vez de crear sólo un elemento complejo con tipo anónimo, que es el pan nuestro de cada día en XML Schema. Bueno, pues como yo creo que eso es innecesario, recurriremos al código fuente, que tampoco es tan complicado. Pinchamos en la pestaña inferior "Source", y cambiamos un poco la declaración de buscaSucursalesRequest. Donde ponía:


Vamos a poner esto otro:


Si ahora volvemos a la pestaña de "Design", vemos que la declaración buscaSucursalesRequest ha cambiado y el tipo es ahora "**anonymous**" (que es una opción que lamentablemente no teníamos antes). Si pinchamos en buscaSucursalesRequest, veremos su estructura interna:


El "(buscaSucursalesRequestType)" corresponde con ese tipo anónimo. El XML Spy lo muestra mucho más clarito que el Eclipse, pero así es suficiente. Ahora vamos a añadirle el sub-elemento que representará el código postal parcial, parteCodPostal:



Pinchando en el icono volveremos a ver el schema.

Ahora vamos a definir el resultado de nuestra operación, buscaSucursalesResponse. Este elemento debe contener una lista de 0 o más estructuras, cada una con información sobre una de las sucursales encontradas. Así que lo primero vamos a definir cuál es la información que vamos a devolver para cada sucursal, y ésto lo haremos con un nuevo tipo complejo de XML, InfoSucursal, que va a contener una sequence y dentro tres elementos, codSucursal, direccion y codPostal:






Ahora vamos a definir buscaSucursalesResponse. Lo podemos hacer al menos de dos maneras, que resultarían en las dos variantes siguientes de XML que se devolvería:

Opción 1: todas las infoSucursal directamente bajo buscaSucursalesResponse Opción 2: un único elemento sucursales para contener la lista completa


...

...

...





...

...

...



Es una cuestión en parte de gusto, pero yo prefiero la segunda porque admite con más facilidad que haya más cosas junto a las sucursales. Por ejemplo, en un servicio más realista, posiblemente no se devolviesen todas las sucursales, sino sólo las primeras y se permitiría luego recuperar las demás, así que la respuesta podría incluir también un flag diciendo si hay más, y cómo continuar recuperándolas. Y además, como veremos más adelante, WSDL2Java puede dar problemas con la primera opción.

Así pues escogemos la segunda opción. De nuevo, el editor de XML Schema de Eclipse no nos ayuda mucho, así que tiraremos de código fuente para dejar la declaración de buscaSucursalesResponse así:


El visor gráfico del XML Schema no nos muestra mucho, pero confiemos en que esté bien.

Con esto ya hemos definido las estructuras de entrada y de salida, pero aún nos queda un detallito relacionado con los namespaces, que son un mecanismo de XML que a menudo causa problemas pero que en el fondo es útil.

Hasta donde yo sé, en servicios web SOAP lo normal es que todos los elementos de los XML intercambiados tengan un namespace. Por ejemplo,

Elementos con namespace "uri:myns" Elementos con namespace "uri:myns" (exactamente igual que el anterior) Elementos sin namespace

..
..


..
..


..
..

Los dos primeros casos son dos formas diferentes de escribir exactamente lo mismo: elementos cuyo namespace URI es "uri:myns". Sin embargo, el tercero son elementos sin namespace, o sea con namespace URI "", lo cual aunque no lo parezca hace una gran diferencia.

En SOAP 1.2 se requiere que el hijo del Body (o sea, en nuestro caso buscaSucursalesRequest) tenga un namespace, y sobre los hijos de ese primer elemento se recomienda que también lo tengan. Por eso, yo casi siempre he visto que los elementos de los mensajes SOAP llevan namespace. Y nuestro servicio no va a ser menos.

Para conseguir esto, la forma más fácil es especificar en nuestro schema un atributo elementFormDefault="qualified". Pero resulta que el editor de XML Schema de Eclipse tampoco nos deja establecer este atributo, lo que es una carencia incluso más importante que la de los elementos complejos de antes. Pero bueno, recurrimos de nuevo al código fuente y dejamos nuestra declaración como sigue:


Si ahora cerramos y salvamos nuestro "Inline Schema of Sucursales.wsdl" volveremos al WSDL. Como nuestra operación buscaSucursales ya apuntaba a los elementos que hemos definido, no tenemos que hacer nada más para definir su entrada y salida.

El binding de WSDL

Éste es otro término poco afortunado de WSDL 1. Un binding es la particularización de un portType para un transporte particular. O sea, define una interfaz (operaciones, parámetros, etc) para un tipo de transporte particular. En nuestro caso, el portType Sucursales tiene ya asociado un binding SOAP, que podemos ver en el editor de WSDL si pinchamos en el icono correspondiente , o navegando hasta él en la ventana de Outline. El esqueleto ya nos lo ha generado, pero aún así tenemos que cambiar una cosa que Eclipse olvidó adaptar cuando renombramos la operación: el soapAction. Su valor es una cabecera HTTP que se usa en SOAP, y supongo que está pensado para permitir redirigir peticiones sin tener que analizar el XML. No estoy seguro de que sirva de mucho, pero lo apropiado es ponerlo en cualquier caso. De nuevo, incomprensiblemente Eclipse no nos deja editar este valor, pero siempre podemos recurrir al fuente para cambiar el valor incorrecto de http://banquito.com/Sucursales/NewOperation por el correcto de http://banquito.com/Sucursales/buscaSucursales:


El servicio y puerto en WSDL

Igualmente, el esqueleto ya contiene nuestro servicio, llamado Sucursales. Dentro de él podemos ver una cosa llamada "SucursalesSOAP", que es lo que en WSDL se llama port (puerto) y es el punto de entrada que implementa el binding, que a su vez está asociado al portType. O sea, la interfaz definitiva de nuestro servicio. Lo que vamos a cambiar ahí es la dirección, poniendo la que luego usará Axis, o sea http://localhost:8081/sucursales/services/SucursalesSOAP:


Ya que Axis por omisión pone en el URL el nombre del puerto ("SucursalesSOAP"), decisión que a mí no me parece adecuada en absoluto, pero bueno. Se puede cambiar, siempre que te acuerdes de hacerlo cada vez que generas el servicio web.

Esto de cablear las direcciones en el WSDL es una de las peores cosas de los servicios web y que más problemas trae luego, pues gracias a la ocultación que hacen las herramientas luego quedan guardados en sitios inverosímiles y escondidos que cuesta encontrar y cambiar (porque obviamente "localhost:8081" no es normalmente una dirección válida para un servicio de producción). Pero ésto no vamos a comentarlo aquí porque podría dar para varios tutoriales más.

Vale la pena mencionar la importante característica de WSDL de que un servicio puede implementar varios puertos. Esto es, el mismo servicio puede implementar a la vez diferentes interfaces (o lo que es lo mismo, diferentes versiones de la misma interfaz), en direcciones distintas (o iguales, dependiendo de lo listo que sea el servicio).

Generando el esqueleto de nuestro servicio

Una vez tenemos un WSDL, ya podríamos crear el cliente del servicio en cualquier plataforma, pero nosotros lo que vamos a crear es el servidor. Esto lo podemos hacer fuera de Eclipse invocando manualmente a la utilidad WSDL2Java de Axis, pero ya que estamos en Eclipse lo haremos con la opción de crear un nuevo servicio web, que por debajo llama a esa utilidad:




Si al elegir el New teníamos seleccionado el WSDL, por omisión nos sale como Web service type = Top down Java bean Web Service, que es justo lo que queremos: crear el Java a partir desde WSDL. El otro tipo (Bottom up Java bean Web service) corresponde a crear el WSDL a partir de Java, que es justo lo que Eclipse y Axis promocionan pero que yo (y más gente, eh) pensamos que es incorrecto. Una vez seleccionado el tipo Top down Java bean Web Service, si en Service definition no tenemos nuestro WSDL, deberemos seleccionarlo.

Por omisión, el selector vertical de la izquierda está marcado hasta "Start service". Podemos dejarlo ahí, o con "Install service", da igual. Luego le vamos dando a Next (o directamente a Finish, si preferimos) y después de esperar un ratito, y si todo va bien (copiar el runtime de Axis, crear el Java, arrancar Tomcat y desplegar el servicio), pues al final tendremos el Tomcat funcionando y el esqueleto del servicio generado:


El fichero SucursalesSOAPImpl es justo el que implementa las operaciones del servicio web, así que vamos a editarlo y ponerle una implementación sencilla:

/**
* SucursalesSOAPImpl.java
*
* This file was auto-generated from WSDL
* by the Apache Axis 1.3 Oct 05, 2005 (05:23:37 EDT) WSDL2Java emitter.
*/

package com.banquito.Sucursales;

import java.util.List;
import java.util.ArrayList;

public class SucursalesSOAPImpl implements com.banquito.Sucursales.Sucursales_PortType{
protected static List Sucursales=new ArrayList();
static {
Sucursales.add(new InfoSucursal("6565","Av. Colmenar, 13", "28760"));
Sucursales.add(new InfoSucursal("34734","Pº Castellana, 145", "28034"));
Sucursales.add(new InfoSucursal("9832","C/ Mayor, 12", "19001"));
}

public com.banquito.Sucursales.BuscaSucursalesResponse buscaSucursales
(com.banquito.Sucursales.BuscaSucursalesRequest buscaSucursalesRequest)
throws java.rmi.RemoteException {

List results=new ArrayList();
String parteCodPostal=buscaSucursalesRequest.getParteCodPostal();
for (InfoSucursal suc:Sucursales)
if (parteCodPostal==null || suc.getCodPostal().indexOf(parteCodPostal)>=0)
results.add(suc);
InfoSucursal[] resultsA=new InfoSucursal[results.size()];
results.toArray(resultsA);
BuscaSucursalesResponse resp=new BuscaSucursalesResponse(resultsA);

return resp;
}

}
Cuando salvemos el fuente, si todo está bien Eclipse se ocupará de reiniciar lo que haga falta para que funcione, así que con eso ya debería estar nuestro servicio. Asegúrate de que Tomcat está arrancado (Status: started), y vamos a probar el servicio con una utilidad de Eclipse que sirve para invocar a cualquier servicio web:





Parece que ha funcionado. También podemos ver el XML intercambiado con nuestro servicio, pinchando en el enlace Source:


Ahora podríamos probar con otros clientes de servicios web, que a partir del WSDL podrían acceder a nuestro servicio. Pero no conozco ningún otro cliente que sea gratis y relevante, así que esto lo vamos a dejar para otro tutorial en el que crearemos una aplicación AJAX que permitirá acceder a nuestro servicio de sucursales. Además, en otro tutorial se muestra cómo utilizar el producto AmberPoint Express para monitorizar la actividad de nuestro servicio web.

Disgresiones adicionales

¿Para qué tengo que ocuparme de todos estos malditos detalles del WSDL y el XML Schema, si Axis lo puede hacer por mí?

Como ya he dicho, en mi empresa hay ejemplos de problemas de interoperabilidad entre herramientas por no preocuparse del WSDL. En mi opinión, la creación de servicios de esa forma "bottom up" debería estar prohibida, excepto para casos muy concretos, pues esta aproximación causa problemas reales de interoperabilidad, o sea se carga el argumento mismo para usar servicios web. Imagino que las sucesivas versiones de Axis irán mejorando el asunto, pero aún así yo considero que el diseño del modelo de servicios de una SOA es una pieza de la misma demasiado importante como para arriesgarse a que quede limitado por las manías o defectos de una herramienta, y así dificultar su futura evolución fuera de esa herramienta.

Y demonios, no es tan difícil. La SOA es el concepto más importante en la informática de estos tiempos (no, no es sólo marketing), así que aprender a usar unas herramientas, en vez de usar sólo Java no parece fuera de lugar. Y además, seamos realistas: siempre hay problemas, y cuando ocurren a menudo las herramientas te dejan tirado y es muy útil conocer cosas tan vitales como estas.

¿Y qué hay de malo en el WSDL2Java?

Bueno, el WSDL2Java de Axis 1 tiene al menos dos problemas que yo conozca:

Por omisión no maneja correctamente los elementos XML repetitivos (arrays)
No maneja bien los tipos simples que extienden xs:string, ej. para poner strings con atributos
Las dos son problemáticas, pero la primera además contribuye a no respetar el modo de trabajo que Eclipse llama "Top-down", o sea hacer primero los WSDLs y luego el Java. Vamos a ilustrar esto.

Si creamos el XML Schema con la segunda opción para definir buscaSucursalesResponse que hemos comentado antes, podemos ver alguno de esos problemas. Por ejemplo, crea un nuevo proyecto web "sucursales2", copia en él Sucursales.wsdl y luego cambia la definición de buscaSucursalesResponse así:


Que en el diseño visual se ve así:


También debemos cambiar el URL de nuestro servicio en el WSDL para reflejar el nuevo proyecto, http://localhost:8081/sucursales2/services/SucursalesSOAP.

Si generamos un nuevo servicio web "Top down" a partir de eso, veremos que el esqueleto de nuestro servicio se creará diferente. En vez de que el método devuelva una estructura, como antes:

public com.banquito.Sucursales.BuscaSucursalesResponse buscaSucursales
(com.banquito.Sucursales.BuscaSucursalesRequest buscaSucursalesRequest)
throws java.rmi.RemoteException {
ahora tendremos que devuelve directamente un array:

public com.banquito.Sucursales.InfoSucursal[] buscaSucursales
(com.banquito.Sucursales.BuscaSucursalesRequest buscaSucursalesRequest)
throws java.rmi.RemoteException {
Si rellenamos ese esqueleto con código similar al del otro servicio:

package com.banquito.Sucursales;

import java.util.List;
import java.util.ArrayList;

public class SucursalesSOAPImpl implements com.banquito.Sucursales.Sucursales_PortType{
protected static List Sucursales=new ArrayList();
static {
Sucursales.add(new InfoSucursal("6565","Av. Colmenar, 13", "28760"));
Sucursales.add(new InfoSucursal("34734","Pº Castellana, 145", "28034"));
Sucursales.add(new InfoSucursal("9832","C/ Mayor, 12", "19001"));
}

public com.banquito.Sucursales.InfoSucursal[] buscaSucursales
(com.banquito.Sucursales.BuscaSucursalesRequest buscaSucursalesRequest)
throws java.rmi.RemoteException {

List results=new ArrayList();
String parteCodPostal=buscaSucursalesRequest.getParteCodPostal();
for (InfoSucursal suc:Sucursales)
if (parteCodPostal==null || suc.getCodPostal().indexOf(parteCodPostal)>=0)
results.add(suc);
InfoSucursal[] resultsA=new InfoSucursal[results.size()];
results.toArray(resultsA);

return resultsA;
}

}
Después, cuando esté el Tomcat arrancado, probamos el servicio. Todo funciona, pero si miramos el XML devuelto por el servicio, veremos que contiene cosas inesperadas:





6565
Av. Colmenar, 13
28760


34734
Pº Castellana, 145
28034




¿"item"? ¿Cómo que "item"? Nosotros no hemos puesto nada sobre "item" en nuestro XML Schema, así que no deberían aparecer. Cualquier cliente que utilice nuestro WSDL original no entenderá esa respuesta, pues "item" no es "sucursal".

Ocurrirá lo mismo si usamos el WSDL original pero, en vez de document/literal, usamos rpc/literal. WSDL2Java (que es la herramienta de Axis subyacente que nos ha generado ese código) parece tener un problema con los elementos XML repetitivos (o sea, arrays). En casos como este, el código funciona pero genera el XML que no es (así como Axis genera luego un WSDL incorrecto), pero en otros casos directamente el código de WSDL2Java no compila porque pone clases que extienden de un array o que intentan lanzan arrays como excepción.

Pero bueno, afortunadamente WSDL2Java tiene una especie de switch que viene a decirle "genera el código correcto". Lamentablemente, el sencillo wizard de Eclipse no parece permitirnos poner ese switch ni lo pone él por omisión, así que tendremos que utilizar directamente la utilidad WSDL2Java desde la línea de comandos. Para ello:

Abrimos una caja de comandos (ejecutamos cmd.exe) y establecemos el classpath de Axis. Aquí vamos a usar el mismo Axis que ya tenemos dentro de Eclipse, que es la versión 1.3, pero puedes usar cualquier Axis que ya tengas instalado por ahí:

C:> set p=C:\Archivos de programa\eclipse32\plugins
En la línea anterior tienes que poner el directorio donde esté el Eclipse en tu máquina
C:> set a=%p%\org.apache.axis_1.3.0.v200606181221\lib
C:> set classpath=%a%\axis.jar;%a%\wsdl4j-1.5.1.jar;%a%\commons-discovery-0.2.jar;
%a%\jaxrpc.jar;%a%\saaj.jar;
%p%\org.apache.commons_logging_1.0.4.v200606131651\lib\commons-logging-1.0.4.jar
Luego nos cambiamos a un directorio temporal y ejecutamos WSDL2Java:

C:> cd c:\temp\w
C:\temp\w> java org.apache.axis.wsdl.WSDL2Java --wrapArrays --server-side "
C:\Trabajo\varios\wsdl2java\eclipsews\sucursales2\Sucursales.wsdl"
En la línea anterior debes poner el camino correcto hasta tu WSDL modificado, dentro de tu workspace de Eclipse
Esa opción --wrapArrays (o -w) es ese switch de "genera el código correcto" que decía antes: le dice a WSDL2Java que, en vez de usar arrays directamente, les ponga alrededor una clase. En nuestro caso, eso le da la posibilidad a WSDL2Java de generar la metainformación del nombre de los elementos individuales del array; o sea "sucursal" en vez de "item". Si tienes curiosidad, esta metainformación está en el código que WSDL2Java ha generado (ahora) para la clase BuscaSucursalesResponse:

// Type metadata
private static org.apache.axis.description.TypeDesc typeDesc =
new org.apache.axis.description.TypeDesc(BuscaSucursalesResponse.class, true);

static {
typeDesc.setXmlType(new javax.xml.namespace.QName
("http://banquito.com/Sucursales/", ">buscaSucursalesResponse"));
org.apache.axis.description.ElementDesc elemField = new org.apache.axis.description.ElementDesc();
elemField.setFieldName("sucursal");
elemField.setXmlName(
new javax.xml.namespace.QName("http://banquito.com/Sucursales/", "sucursal"));
elemField.setXmlType(
new javax.xml.namespace.QName("http://banquito.com/Sucursales/", "InfoSucursal"));
elemField.setMinOccurs(0);
elemField.setNillable(false);
elemField.setMaxOccursUnbounded(true);
typeDesc.addFieldDesc(elemField);
}
Que parece que no se puede poner si no se crea esta clase que envuelva al array.

Debo confesar que yo no he conocido este switch -w hasta el otro día (lamentablemente, después de haber publicado la primera versión de este tutorial), que lo vi casualmente, y hasta entonces pensaba que WSDL2Java era incapaz de generar este código correcto. No entiendo bien por qué no hace ésto por omisión, aunque supongo que se debe a razones históricas, pero a mí me parece que también se debe a poco respeto a la interoperabilidad que, en mi opinión, destila Axis: como luego el WSDL que genera Axis es coherente con el código de WSDL2Java, pues da igual que no sea compatible con el original.

En fin, el código generado lo tenemos en el subdirectorio com de ese directorio temporal. Ahora lo tenemos que meter en nuestro proyecto Eclipse. Eso es fácil: sólo hay que machacar los fuentes. Puedes hacerlo desde Eclipse, desde el explorador de archivos o desde la línea de comandos:

C:\temp\w> xcopy /s/y com "C:\Trabajo\varios\wsdl2java\eclipsews\sucursales2\src\com"
Recuerda poner el camino correcto hasta tu proyecto en el workspace de Eclipse
Después de lo cual podemos tener que darle a la opción de Refresh en Eclipse, en el menú contextual del directorio "src" de nuestro proyecto. Luego veremos que la declaración del método buscaSucursales de nuestra clase SucursalesSOAPImpl vuelve a devolver una clase BuscaSucursalesResponse, en vez de un array. Así que le podemos copiar como implementación del servicio el código de nuestra clase original.

Pero esto no es todo: tenemos también que modificar la información de despliegue de Axis. Axis trae herramientas de línea de comandos para ello, pero nosotros lo vamos a hacer dentro de Eclipse. Para ello,

Abrimos el fichero deploy.wsdd que está en el directorio src de nuestro proyecto y que también ha sido generado por WSDL2Java:


Ahí seleccionamos toda la declaración de nuestro servicio, desde hasta , y la copiamos en el portapapeles.

Luego, dentro del directorio WebContent/WEB-INF de nuestro proyecto, abrimos el archivo server-config.wsdd:


Ahí buscamos el texto Sucursal, y encontraremos la declaración del servicio SucursalesSOAP:


Vamos a sustituir toda esa declaración, desde hasta , por lo que hemos copiado antes desde deploy.wsdd y tenemos ahora en el portapapeles, y salvamos el fichero:


Tras esos cambios, Eclipse redesplegará lo necesario a nuestro Tomcat y, cuando accedamos con el Web Services Explorer, podremos ver que esta vez el servicio devuelve la respuesta adecuada, o sea una con varias etiquetas dentro.

Ahora bien, si resulta que no usamos WSDL2Java sino sólo Eclipse, o si usamos WSDL2Java pero no sabemos que para que genere el código correcto hay que darle la opción -w (como me pasaba a mí), pues nos vamos a encontrar con que el servicio de Axis no cumple con nuestro WSDL. Es posible que ni nos demos cuenta, pues en vez de usar el WSDL original (correcto) pasemos a usar el que nos genera Axis (erróneo). Si todos los demás clientes lo usan también, pues bueno; pero si no, pues en algún momento futuro encontraremos que no hay interoperabilidad.

El segundo problema de WSDL2Java que mencionaba arriba se ve por ejemplo cuando uno intenta generar stubs complejos como los necesarios para acceder a UDDI v3 a partir de su WSDL. Aún si tenemos en cuenta la opción --wrapArrays, el resultado no compila, pues a partir de tipos que derivan de xs:string, genera clases que no extienden de nadie pues no se puede extender de la clase String (que vaya otra idea feliz, ésta) pero llaman a su super().

Pero al menos, una vez arreglado esto (a mano clase por clase), parece que los mensajes intercambiados con el servidor son correctos. En fin, al menos por ahora no he encontrado una opción que haga que WSDL2Java trate bien esto, pero si lo encuentro modificaré el tutorial.

En mi opinión, Axis es una herramienta útil pero tiene un grave problema: está diseñado desde el principio para funcionar en modo "Bottom-up", o sea primero hacer el Java y luego que Axis genere el XML. Como ya he dicho, esta facilidad precisamente hay que evitarla.

No hay comentarios:

Publicar un comentario