Buenas prácticas JAXWS. Utilizando RequestWrapper.

Desde la aparición de la arquitectura SOA (Service-Oriented Architecture) y la posterior llegada de SCA (Service Component Architecture), la utilización de servicios web se ha multiplicado en los desarrollos software de las empresas.

En el mundillo java surgieron diferentes tecnologías para facilitar la creación de los servicios web (Axis1, Axis2, CXF, etcétera), pero a raíz de la llegada de la implementación de la especificación JSR 224 – JAXWS a partir de la JDK 1.5, es cuando la creación de servicios web se ha hecho más inter-operable y gracias a la utilización de anotaciones se ha facilitado enormemente.

En esta entrada me centro en la anotación JAXWS @javax.xml.ws.RequestWrapper, como usarla y la utilidad que tiene.

Un caso de uso.

Cuando se realiza un servicio web con JAX-WS se crea una clase java con anotaciones JSR 181 (@WebService, @WebMethod, etcétera) que describen el contrato WSDL que luego utilizan los clientes para invocar al servicio.

Para explicar esta entrada, voy a utilizar el siguiente ejemplo de servicio web básico, compuesto por un método que recibe una serie de parámetros.

package com.egv.webservices.jaxws;

import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebService;
import javax.jws.soap.SOAPBinding;

@WebService ( name="WebServiceEjemplo", 
              targetNamespace="http://egv.com/webservices/jaxws")
@SOAPBinding(  style=SOAPBinding.Style.DOCUMENT, 
               use=SOAPBinding.Use.LITERAL, 
               parameterStyle=SOAPBinding.ParameterStyle.WRAPPED)
public class WebServiceEjemplo {
	
	@WebMethod(operationName="metodoEjemplo")
	public void metodoEjemplo(  @WebParam(name="codigo1")String codigo1,
                                @WebParam(name="descripcion1")String descripcion1,
                                @WebParam(name="codigo2")Integer codigo2,
                                @WebParam(name="descripcion2")String descripcion2) {
		System.out.println("codigo1=" + codigo1);
		System.out.println("descripcion1=" + codigo1);
		System.out.println("codigo2=" + codigo2);
		System.out.println("descripcion2=" + descripcion2);
	}

}

Esta clase java genera un método metodoEjemplo en el servicio web con las siguiente Request de tipo SOAP:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:jax="http://egv.com/webservices/jaxws">
   <soapenv:Header/>
   <soapenv:Body>
      <jax:metodoEjemplo>
         <!--Optional:-->
         <codigo1>?</codigo1>
         <!--Optional:-->
         <descripcion1>?</descripcion1>
         <!--Optional:-->
         <codigo2>?</codigo2>
         <!--Optional:-->
         <descripcion2>?</descripcion2>
      </jax:metodoEjemplo>
   </soapenv:Body>
</soapenv:Envelope>

Hasta aquí el caso más básico de creación de un servicio web. Pero, qué ocurre si queremos complicarlo haciendo que los parámetros codigo1 y codigo2 se requieran de forma obligatoria.

Para conseguir esta obligatoriedad podemos optar por dos tipos de soluciones: una aproximación a posteriori o una aproximación a priori.

La aproximación a posteriori

La solución a posteriori se introduce una vez que el servicio web se ha invocado y se consigue añadiendo en el código una comprobación de parámetros mediante condicionales. Por ejemplo:

if (codigo1 == null || codigo2 == null) {
throw new ParametrosObligatoriosException();
}

Esta comprobación es funcionalmente correcta pero no es óptima. El motivo es que para realizar la comprobación de parámetros se hace una llamada al servicio web, la ejecución del método y el envío de la respuesta.

La aproximación a priori

La solución a priori se basa en el uso conjunto de la anotación javax.xml.ws.RequestWrapper de JAXWS combinado con el uso de las anotaciones JSR 222 – JAXB. Esto puede sonar complejo, e inicialmente nos va a obligar a codificar más líneas de código, sin embargo, la flexibilidad que proporciona a la hora de jugar con los parámetros, tipo, obligatoriedad, etcétera, es mucho más óptima.

El primer paso para llevar a cabo esta solución es hacer un wrapper para envolver la request del método. Este wrapper debe contener los campos declarados como parámetros del método con la anotación WebParam. Cada campo deber ir anotado con anotaciones JAXB que indican las características del campo concreto.

package com.egv.webservices.jaxws;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;

@XmlType(propOrder = { "codigo1", "descripcion1", "codigo2", "descripcion2" })
@XmlAccessorType(XmlAccessType.FIELD)
public class WebServiceEjemploWrapperRequest {
	
	@XmlElement(name="codigo1", required=true)
	public String codigo1;

	@XmlElement(name="descripcion1")
	public String descripcion1;

	@XmlElement(name="codigo2", required=true)
	public Integer codigo2;

	@XmlElement(name="descripcion2")
	public String descripcion2;

	[…] //Getter y Setter para cada variable.

La anotación @XmlElement indica el nombre de la variable y en las variables codigo1 y codigo2 además incluye el indicador de obligatoriedad required=true.

El segundo paso es anotar el método del servicio web en su clase con la anotación RequestWrapper

	@WebMethod(operationName="metodoEjemplo")
	@RequestWrapper(localName = "metodoEjemploRequestWrapper", 
    className = "com.egv.webservices.jaxws.WebServiceEjemploWrapperRequest")
	public void metodoEjemplo(  @WebParam(name="codigo1")String codigo1,
                                @WebParam(name="descripcion1")String descripcion1,
                                @WebParam(name="codigo2")Integer codigo2,
                                @WebParam(name="descripcion2")String descripcion2) {

El resultado final podemos observarlo en la request SOAP generada:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:jax="http://egv.com/webservices/jaxws">
   <soapenv:Header/>
   <soapenv:Body>
      <jax:metodoEjemploRequestWrapper>
         <codigo1>?</codigo1>
         <!--Optional:-->
         <descripcion1>?</descripcion1>
         <codigo2>?</codigo2>
         <!--Optional:-->
         <descripcion2>?</descripcion2>
      </jax:metodoEjemploRequestWrapper>
   </soapenv:Body>
</soapenv:Envelope>

Los campos codigo1 y codigo2 han dejado de ser opcionales y ahora han pasado a ser obligatorios. Sin embargo, si ejecutamos desde un cliente y dejamos sin informar alguno de los campos obligatorios, el servicio web se ejecuta y da respuesta.

El método se ejecuta pese a no rellenar los campos obligatorios

La explicación a este supuesto mal funcionamiento es que la validación del esquema del servicio web consume bastantes recursos afectando al rendimiento y por tanto no se activa por defecto. Si se quiere activar la validación del esquema bastará con añadir la anotación com.sun.xml.ws.developer.SchemaValidation a nivel de servicio web.

@WebService (name="WebServiceEjemplo", targetNamespace="http://egv.com/webservices/jaxws")
@SOAPBinding(style=SOAPBinding.Style.DOCUMENT, 
			use=SOAPBinding.Use.LITERAL, 
			parameterStyle=SOAPBinding.ParameterStyle.WRAPPED)
@SchemaValidation
public class WebServiceEjemplo {
[…]

Una vez añadida la anotación, al invocar el servicio web sin rellenar el campo codigo1, la respuesta es:

   <S:Body>
      <S:Fault xmlns:ns4="http://www.w3.org/2003/05/soap-envelope">
         <faultcode>S:Server</faultcode>
         <faultstring>com.sun.istack.XMLStreamException2: org.xml.sax.SAXParseException: cvc-complex-type.2.4.a: Invalid content was found starting with element 'descripcion1'. One of '{codigo1}' is expected.</faultstring>
         <detail>
            <ns2:exception class="javax.xml.ws.WebServiceException" note="To disable this feature, set com.sun.xml.ws.fault.SOAPFaultBuilder.disableCaptureStackTrace system property to false" xmlns:ns2="http://jax-ws.dev.java.net/">
               <message>com.sun.istack.XMLStreamException2: org.xml.sax.SAXParseException: cvc-complex-type.2.4.a: Invalid content was found starting with element 'descripcion1'. One of '{codigo1}' is expected.</message>
               <ns2:stackTrace>
                  <ns2:frame class="com.sun.xml.ws.util.pipe.AbstractSchemaValidationTube" file="AbstractSchemaValidationTube.java" line="242" method="doProcess"/>
[…]

La validación del esquema hace también comprobaciones de tipos de datos, de forma que si en el campo Integer introducimos ‘abc’ se lanzará una excepción.

<message>com.sun.istack.XMLStreamException2: org.xml.sax.SAXParseException: cvc-datatype-valid.1.2.1: 'abc' is not a valid value for 'integer'.</message>

Ideas a evitar

Se podría idear una solución intermedia sin utilizar la anotación RequestWrapper. Esta solución intermedia pasaría por incluir los campos de la request en un objeto wrapper, como el objeto WebServiceEjemploWrapperRequest del ejemplo, y hacer que este objeto sea el único parámetro del método.

	@WebMethod(operationName="metodoMalEjemplo")
	public void metodoMalEjemplo(	@WebParam(name="objetoComplejo")WebServiceEjemploWrapperRequest objetoComplejo) {
		System.out.println("codigo1=" + objetoComplejo.getCodigo1());
		System.out.println("descripcion1=" + objetoComplejo.getDescripcion1());
		System.out.println("codigo2=" + objetoComplejo.getCodigo2());
		System.out.println("descripcion2=" + objetoComplejo.getDescripcion2());
	}

Esta programación dará como resultado la siguiente petición SOAP:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:jax="http://egv.com/webservices/jaxws">
   <soapenv:Header/>
   <soapenv:Body>
      <jax:metodoMalEjemplo>
         <!--Optional:-->
         <objetoComplejo>
            <codigo1>?</codigo1>
            <!--Optional:-->
            <descripcion1>?</descripcion1>
            <codigo2>?</codigo2>
            <!--Optional:-->
            <descripcion2>?</descripcion2>
         </objetoComplejo>
      </jax:metodoMalEjemplo>
   </soapenv:Body>
</soapenv:Envelope>	

El funcionamiento de los clientes que llaman al método con la request configurada de esta manera es similar a la configuración con RequestWrapper. Sin embargo, añadir el encapsulado adicional <objetoComplejo> no es una buena idea ya que cuanto más simple permanezca el modelo más compatible será éste.

Conclusión

La utilización de la anotación RequestWrapper permite utilizar JAXB para configurar las características de cada campo que participa en la request de un método de un servicio web.

Esta anotación permite generar una request lo más simple posible lo que, entre otras cosas, hará que los métodos sean completamente compatibles con sistemas de cacheo que normalmente no funcionan bien con objetos complejos.

Links

Publicar servicios web JAX-WS 2.1 en Tomcat 6

La llegada de la crisis se está dejando ver en todos los ámbitos y la informática no es una excepción. Hasta hace no mucho las empresas no daban excesiva importancia al coste de sus servidores y se pagaban las licencias de software religiosamente. Pero, la crisis ha dado un vuelco a esta situación. El gasto se controla más y allí donde antes se utilizaba un servidor de pago para dar servicio, ahora se busca sustituirlo por versiones de código abierto y gratuitas.

Concretamente, en el cliente para el que trabajo actualmente se ha decido empezar a utilizar Apache Tomcat para aquellas aplicaciones que no necesitan de la pila Enterprise de Java.

Esta entrada recoge cómo publicar servicios web JAX-WS en un Apache Tomcat 6.x.

Requisitos

Antes de comenzar con el código puro y duro voy a hacer un pequeño repaso del entorno necesario para seguir la solución propuesta en la entrada.

La máquina virtual Java es la Jrockit-jdk1.6.0_45-R28.2.7-4.1.0. Utilizo esta versión porque en los entornos productivos es esta máquina virtual la que se instala. El ejemplo puede funcionar igualmente con la HotSpot 1.6. La versión en concreto que estoy utilizando contiene la versión JAX-WS RI 2.1.6. Si quieres averiguar que versión de JAX-WS incorpora tu máquina virtual basta con ejecutar el siguiente comando:

${java-home}/bin/wsimport -version

El servidor Tomcat elegido para las pruebas es la versión 6.0.37. No utilizo la versión 7 de Tomcat porque los entornos productivos están configurados con la versión Tomcat 6. Esta versión admite las especificaciones Servlet/2.5, JSP/2.1 y requiere la versión 1.5 o versiones superiores de la máquina virtual Java.

Instalación básica de Tomcat.

El primer paso es realizar una instalación básica de Tomcat.

El servidor se puede descargar de la página oficial de Apache Tomcat. La instalación básica es muy sencilla, basta con descomprimir el fichero descargado en un directorio que a partir de ahora voy a llamar ${TOMCAT_HOME}.

Antes de arrancar el servidor hay que configurar el usuario administrador para poder acceder a la aplicación de despliegue de Tomcat. Los usuarios de Tomcat se configuran en el fichero ${TOMCAT_HOME}/conf/tomcat-users.xml. En este fichero se añaden las siguientes líneas:

<role rolename="manager"/>
<role rolename="admin"/>
<user username="admin" password="admin" roles="admin,manager"/>

No tendría que decirlo, pero por si acaso, ni que decir tiene que el nombre del usuario administrador así como su password no tiene porque coincidir con el ejemplo, queda a elección de la persona que instala el servidor crear los roles y usuarios que más le convengan.

Para el ejemplo básico creo el rol manager, el rol admin y el usuario admin que estará compuesto por estos dos roles.

Una vez modificado este fichero se puede poner en marcha el servidor. Para arrancar y parar, el propio servidor proporciona dos scripts que facilitan enormemente la tarea. El script de arranque es ${TOMCAT_HOME}/bin/startup.bat (en entornos Windows) o ${TOMCAT_HOME}/bin/startup.sh (en entornos unix). Para parar el servidor el script es ${TOMCAT_HOME}/bin/shutdown.bat (Windows) y ${TOMCAT_HOME}/bin/shutdown.sh (Unix).

Creación de la aplicación de ejemplo con servicios web JAXWS.

La aplicación de ejemplo se crea utilizando Maven 3.x para facilitar el manejo del ciclo de vida en general y de las dependencias en particular.

Desde Eclipse se crea un nuevo Maven Project a partir del arquetipo org.apache.maven.archetypes – maven-archetype-webapp en su versión RELEASE. Esto proporciona la base de una aplicación web dinámica. La estructura se parece a lo que se ve en la siguiente captura:

Arquitectura creada por Maven para una aplicación web.

Para facilitar el desarrollo, publicación y pruebas de la aplicación se incluye en el fichero pom.xml que ha generado el arquetipo la configuración de dos plugins; maven-war-plugin y maven-clean-plugin.

El plugin maven-war-plugin configura el empaquetado WAR de la aplicación con ciertas características:

  • Se va a publicar la aplicación directamente en el directorio de despliegues por defecto de Tomcat. Esto se hace añadiendo el tag <webappDirectory> en las configuración del plugin y permite hacer pruebas inmediatas de la aplicación sin necesidad de estar publicándola constantemente desde el Deployer.
  • Se va a dejar una versión empaquetada en un fichero WAR en el directorio dist de la aplicación. Esto se consigue con el tag <outputDirectory>. La idea es poder tener un WAR independiente para poder enviar la aplicación a otros entornos de forma fácil.
<build>
    […]
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-war-plugin</artifactId>
        <version>2.3</version>
        <configuration>
          <webappDirectory>E:\srv\dominio\aplic\dominio_tomcat\apache-tomcat-6.0.37\webapps\ExampleWS</webappDirectory>
          <outputDirectory>./dist</outputDirectory>
          <warName>ExampleWS-${version}</warName>
        </configuration>
      </plugin>
    </plugins>
	 […]
</build>

El plugin maven-clean-plugin limpia los compilados de la aplicación. Como se ha añadido la generación del fichero war en un directorio específico, se configura este plugin para que lo elimine cuando se ejecute un mvn clean.

  <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-clean-plugin</artifactId>
    <version>2.5</version>
    <configuration>
      <filesets>
         <fileset>
          <directory>./dist</directory>
          <followSymlinks>false</followSymlinks>
          <useDefaultExcludes>true</useDefaultExcludes>
          <includes>
            <include>*.war</include>
          </includes>
        </fileset>
      </filesets>
    </configuration>
  </plugin>

Adicionalmente, se añaden las dependencias necesarias para que la aplicación entienda y publique los servicios web JAXWS:

  • jaxws-rt v2.1.6
  <dependencies>
  […]
    <dependency>
      <groupId>>com.sun.xml.ws</groupId>
      <artifactId>jaxws-rt</artifactId>
      <version>2.1.7</version>
    </dependency>
  […]
  </dependencies>

Creación el servicio web de ejemplo.

Desde que la JDK 1.5 incorporó las anotaciones y JAX-WS hizo uso de ellas, crear un servicio web de ejemplo se ha convertido en una tarea sencilla.

package com.egv.webservices.jaxws;

import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebService;
import javax.jws.soap.SOAPBinding;

@WebService (name="WebServiceUno", targetNamespace="http://egv.com/webservices/jaxws")
@SOAPBinding(style=SOAPBinding.Style.DOCUMENT, 
			use=SOAPBinding.Use.LITERAL, 
			parameterStyle=SOAPBinding.ParameterStyle.WRAPPED)
public class WebServiceUno {
	
	@WebMethod(operationName="HolaMundo")
	public void HolaMundo(@WebParam(name="nombre")String nombre) {
		System.out.println("Hola mundo! Te saluda " + nombre);
	}

}  

Este servicio web se llama WebServiceUno y tiene un único método que se llama HolaMundo que recibe un nombre como parámetro y que una vez ejecutado escribe en consola Hola mundo! te saluda ${nombre}. Un servicio web muy simple pero perfecto para probar la publicación de WS JAXWS en Tomcat 6.

Configuración de la aplicación para publicar el servicio web en Tomcat.

Para publicar los servicios web en tomcat es necesario que la aplicación publique un servlet específico de JAXWS en el arranque. Este servlet se encuentra en el paquete com.sun.xml.ws.transport.http.servlet.WSServlet y viaja con la implementación de JAXWS que se ha descargado con la dependencia oportuna. En el fichero web.xml de la aplicación se incluye el siguiente código:

<web-app>
  	[…]
    <servlet>
        <servlet-name>WebServiceUno</servlet-name>
        <servlet-class>
        	com.sun.xml.ws.transport.http.servlet.WSServlet
        </servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>WebServiceUno</servlet-name>
        <url-pattern>/wsUno</url-pattern>
    </servlet-mapping>
    <session-config>
        <session-timeout>120</session-timeout>
    </session-config>
	[…]  
</web-app>

El servlet WSServlet ejecuta los servicios web que se incluyan en la aplicación. Pero, para que Tomcat sepa que servicios web tiene que publicar hay que incluir el fichero sun-jaxws.xml. Este fichero contiene la configuración de los endpoints de los servicios web.

<?xml version="1.0" encoding="UTF-8"?>
<endpoints
  xmlns="http://java.sun.com/xml/ns/jax-ws/ri/runtime"
  version="2.0">
  <endpoint
      name="WebServiceUno"
      implementation="com.egv.webservices.jaxws.WebServiceUno"
      url-pattern="/wsUno"/>
</endpoints>

Tomcat entiende este fichero gracias al listener com.sun.xml.ws.transport.http.servlet.WSServletContextListener específico de JAXWS. Este listener es el encargado de parsear el fichero y crear un HttpAdapter por cada endpoint definido. El listener hay que incluirlo en la aplicación añadiendo las siguientes líneas en el fichero web.xml:

<web-app>
[…]
<listener>
        <listener-class>
                com.sun.xml.ws.transport.http.servlet.WSServletContextListener
        </listener-class>
    </listener> 
[…]
</web-app>

El war generado presenta la siguiente estructura:

Estructura del fichero War.

Desplegar y probar.

Una vez que todo está creado, se ejecuta mvm install.

Si la compilación es correcta, ésta ejecución debería dejar en el directorio de despliegue de tomcat la aplicación correctamente publicada. En el deployer de tomcat se verá la aplicación correctamente publicada.

Aplicación correctamente desplegada.

Accediendo a la URI del servicio web se ve el servicio web publicado.

Web Service correctamente desplegado.

Probando la aplicación a través de SOAPUi se comprueba el funcionamiento correcto.

Ejecución desde SOAPUi

En la consola del servidor encontramos la traza escrita.

Consola de salida del servidor.

Conclusión

Como se ha visto a lo largo de la entrada, no es especialmente difícil publicar servicios web JAXWS en Apache Tomcat. La ventaja es que Tomcat cuenta con una licencia de código abierto ahorrando a las empresas bastantes euros en costosas licencias de servidores de aplicación.

Por tanto, si la aplicación que se desea desarrollar no tiene exigencias Enterprise y va a tener una carga normal de trabajo, ésta configuración se puede utilizar sin problemas.