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

2 comentarios en “Buenas prácticas JAXWS. Utilizando RequestWrapper.

  1. Avatar de Miguel Miguel dijo:

    Que tal me ha parecido interesante tu publicacion, pero no me ha quedado claro el por que no es buena idea el usar el encapsulamiento de las peticiones.

    Saludos.

    • Muy buenas Miguel, ante todo gracias por comentar.

      Me imagino que te refieres a mi consejo de no encapsular, en los servicios web, los parámetros de entrada de un método en objetos java complejos.

      Fíjate que en el ejemplo «bueno» el XML para llamar al método es:

         <soapenv:Body>
            <jax:request>
               <codigo1>?</codigo1>
               <descripcion1>?</descripcion1>
               <codigo2>?</codigo2>
               <descripcion2>?</descripcion2>
            </jax:request>
         </soapenv:Body>
      

      Y, el XML en el ejemplo «malo» es:

         <soapenv:Body>
            <jax:request>
               <objetoComplejo>
                  <codigo1>?</codigo1>
                  <descripcion1>?</descripcion1>
                  <codigo2>?</codigo2>
                  <descripcion2>?</descripcion2>
               </objetoComplejo>
            </jax:request>
         </soapenv:Body>
      

      El nivel extra objetoComplejo que se genera no aporta ninguna ventaja respecto al ejemplo «bueno» (quizás alguna organizativa en Java, aunque tampoco lo veo claro), sin embargo, me ha pasado que utilizando herramientas de cacheo de servicios web, este nivel extra hace que el cacheo no se produzca. El motivo es que para la herramienta de caché el objeto complejo, aunque lleve los mismos datos, es una instancia distinta en memoria, y por tanto no procede a su cacheo.

      Este ejemplo, que te he puesto me hizo pensar que cuanto más fácil es el esquema de los datos de entrada mejor.

      Espero haberme explicado.

Replica a Miguel Cancelar la respuesta