WS-Addressing con Apache CXF

De ChuWiki

Vamos a ver un ejemplo de WS-Addressing con CXF. Tienes un proyecto maven completo con cliente + web service con main() + web service con tomcat en https://github.com/chuidiang/chuidiang-ejemplos-google-code/tree/ejemplo_cxf/ws-addressing-sobre-tomcat En src/test/java tienes los main de cliente y servidor. La parte de src/main/java se deja para el despliegue en tomcat.

WS-Addressing es una característica de los Web Services que nos permite enviar el resultado de una llamada a otro sitio. Si un cliente llama a un método de un Web Service, con WS-Addressing podemos hacer que el Web Service envíe el resultado a otra URL.

Para ello, en el envío de la petición por parte del cliente, en la cabecera SOAP XML, van varias cosas, entre las que destacan un identificador y la URL a la que enviar la respuesta

  <soap:Header>
    <Action xmlns="http://www.w3.org/2005/08/addressing">http://cxf.ejemplos.chuidiang.com/Calculadora/suma</Action>
    <MessageID xmlns="http://www.w3.org/2005/08/addressing">urn:uuid:1332091918017</MessageID>
    <To xmlns="http://www.w3.org/2005/08/addressing">http://localhost:8080/ejemplo_cxf/Calculadora</To>
    <ReplyTo xmlns="http://www.w3.org/2005/08/addressing">
      <Address>http://localhost:8080/ejemplo_cxf/Resultado</Address>
    </ReplyTo>
  </soap:Header>
  ...

El MessageID se nos devolverá con la respuesta de forma que sepamos a qué petición corresponde. Podemos poner en código el MessageID que queramos. El Address dentro de ReplyTo indica la URL a la que queremos que se devuelva el resultado. En el caso del ejemplo, el cliente llama al Web Service http://localhost:8080/ejemplo_cxf/Calculadora, al método suma() y quiere que el resultado de la suma se lo pasen al web service http://localhost:8080/ejemplo_cxf/Resultado. Este Web Service tendrá que tener un método adecuado para recoger el resultado.

El servidor, por su parte, en la respuesta, enviará la siguiente cabecera SOAP XML

  <soap:Header>
    <Action xmlns="http://www.w3.org/2005/08/addressing">http://cxf.ejemplos.chuidiang.com/Calculadora/sumaResponse</Action>
    <MessageID xmlns="http://www.w3.org/2005/08/addressing">urn:uuid:2e8bbbaf-8a4a-4432-b615-b58267ee13db</MessageID>
    <To xmlns="http://www.w3.org/2005/08/addressing">http://localhost:8080/ejemplo_cxf/Resultado</To>
    <RelatesTo xmlns="http://www.w3.org/2005/08/addressing">urn:uuid:1332091918017</RelatesTo>
  </soap:Header>

Envía la respuesta http://localhost:8080/ejemplo_cxf/Resultado y añadeun RelatesTo con el messageId que le pasamos en la petición, permitiéndonos identificar a qué petición corresponde este resultado.

Veamos como hacer todo esto con Apache CXF.


Web Services[editar]

Usando el método de hacer primero las clases java en vez de los WSDL, vamos a hacer dos clases que harán de web services, una es a la que llamará el cliente y la otra es la que recibirá el resultado. Aunque un método double suma(sum1,sum2) que es el que vamos a hacer es muy simple, lo complicamos un poco haciendo que el primer parámetro sum1 sea de entrada y salida simultáneamente, así vemos como afecta esto al método que recibe los resultados.

El primer Web Service, una clase Calculadora con suma() y resta()

package com.chuidiang.ejemplos.cxf;

import javax.jws.WebParam;
import javax.jws.WebService;
import javax.xml.ws.Holder;

@WebService(endpointInterface = "com.chuidiang.ejemplos.cxf.Calculadora")
public class CalculadoraImpl implements Calculadora {

   public double suma(@WebParam(name = "sum1") Holder<Double> a,
         @WebParam(name = "sum2") double b) {
      a.value = a.value + 1;
      return a.value + b - 1;
   }

   public double resta(double a, double b) {
      return a - b;
   }

}

Es importante dar nombre a los parámetros con @WebParam, de forma que luego en el Web Service que recibe el resultado podamos identificarlos. De hecho, el único realmente importante es el sum1, ya que al ser tipo Holder<Double> (de entrada y salida), el Web Service que recibe el resultado recibirá el return y este parámetro sum1. El parámetro sum2 podríamos dejarlo sin @WebParam.

El WebService que recibe el resultado

package com.chuidiang.ejemplos.cxf;

import javax.jws.WebParam;
import javax.jws.WebService;

@WebService
public class Resultado {

   public void sumaResponse(@WebParam(name = "return") double sumaResponse,
         @WebParam(name = "sum1") double arg1) {
      System.out.println(sumaResponse);
      System.out.println(arg1);
   }
}

El método se llama sumaResponse, que es el nombre del método suma más la palabra Response. Los parámetros que recibe son uno de nombre "return", que será el return del método suma(), es decir, la suma. El segundo es el sum1, ya que al indicar en suma() que es parámetro también de salida, se nos enviará a este método. Es importante darle el mismo nombre con @WebParam en los dos métodos.


Preparando el servidor[editar]

Ahora que tenemos las clases, vamos a preparar el servidor. Lo haremos con un main() y con un tomcat.


Con un método main()[editar]

El código para lanzar esto con WS-Addressing es el siguiente

package com.chuidiang.ejemplos.cxf;

import org.apache.cxf.interceptor.LoggingInInterceptor;
import org.apache.cxf.interceptor.LoggingOutInterceptor;
import org.apache.cxf.jaxws.EndpointImpl;
import org.apache.cxf.ws.addressing.WSAddressingFeature;

public class ServerCalculadora {

   /**
    * @param args
    */
   public static void main(String[] args) {

      EndpointImpl.publish("http://localhost:8080/ejemplo_cxf/Resultado",
            new Resultado());

      EndpointImpl endpoint = (EndpointImpl) EndpointImpl
            .create(new CalculadoraImpl());

      endpoint.getFeatures().add(new WSAddressingFeature());
      endpoint.publish("http://localhost:8080/ejemplo_cxf/Calculadora");

      LoggingInInterceptor logIn = new LoggingInInterceptor();
      logIn.setPrettyLogging(true);
      endpoint.getInInterceptors().add(logIn);
      LoggingOutInterceptor logOut = new LoggingOutInterceptor();
      logOut.setPrettyLogging(true);
      endpoint.getOutInterceptors().add(logOut);
   }
}

Publicamos primero el de Resultado, que es más sencillo. Basta con un EndpointImpl.publish().

El Calculadora es un poco más complejo puesto que tenemos que activarle la parte de WS-Addressing, de forma que haga caso a todo lo que hemos visto de la cabecera SOAP y envíe los resultados a otro sitio. Con EnpointImpl creamos el Web Service, pero no lo publicamos todavía. Le añadimos un WSAddressingFeature y ya está listo. Sólo nos queda publicarlo con publish() indicando la URL donde lo queremos accesible.

El resto del código, los LogginInInterceptor y LogginOutInterceptor son solo para mostrar por consola las cabeceras SOAP que se envían y reciben de forma que podamos depurar un poquito y ver lo que está pasando. Podemos quitar todo ese código con tranquilidad si no queremos ver las cabeceras.


Web Service en Tomcat[editar]

Si queremos publicar todo esto en Tomcat, tenemos que poner los siguiente en el web.xml y beans.xml.

<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">


<web-app>
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>WEB-INF/beans.xml</param-value>
	</context-param>

	<listener>
		<listener-class>
			org.springframework.web.context.ContextLoaderListener
		</listener-class>
	</listener>

	<servlet>
		<servlet-name>CXFServlet</servlet-name>
		<display-name>CXF Servlet</display-name>
		<servlet-class>
			org.apache.cxf.transport.servlet.CXFServlet
		</servlet-class>
		<load-on-startup>1</load-on-startup>
	</servlet>

	<servlet-mapping>
		<servlet-name>CXFServlet</servlet-name>
		<url-pattern>/*</url-pattern>
	</servlet-mapping>
</web-app>

Este es el web.xml. Simplemente pone una serie de "cosas" de Spring Framework y de CXF (todo ello viene con CXF) para poner en marcha las clases que hemos indicado que son web services. En este fichero indicamos a Spring Framework que lea el fichero beans.xml, que es el siguiente

<?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:jaxws="http://cxf.apache.org/jaxws"
	xsi:schemaLocation="
	http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
	http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd">


	<import resource="classpath:META-INF/cxf/cxf.xml" />
	<import resource="classpath:META-INF/cxf/cxf-servlet.xml" />

	<jaxws:endpoint id="resultado"
		implementor="com.chuidiang.ejemplos.cxf.Resultado" address="/Resultado">
		<jaxws:features>
			<wsa:addressing xmlns:wsa="http://cxf.apache.org/ws/addressing" />
		</jaxws:features>

	</jaxws:endpoint>

	<jaxws:endpoint id="calculadora"
		implementor="com.chuidiang.ejemplos.cxf.CalculadoraImpl" address="/Calculadora">
		<jaxws:inInterceptors>
			<bean class="org.apache.cxf.interceptor.LoggingInInterceptor">
				<property name="prettyLogging" value="true" />
			</bean>
		</jaxws:inInterceptors>

		<jaxws:outInterceptors>
			<bean class="org.apache.cxf.interceptor.LoggingOutInterceptor">
				<property name="prettyLogging" value="true" />
			</bean>
		</jaxws:outInterceptors>

		<jaxws:features>
			<wsa:addressing xmlns:wsa="http://cxf.apache.org/ws/addressing" />
		</jaxws:features>

	</jaxws:endpoint>
</beans>

Aquí estamos indicando qué clases son nuestros Web Services, aparecen en los tagas <jaxws:endpoint>. Simplemente les habilitamos el WS-Addressing con los tags <jaxws:features><wsa:addressing>. Hemos añadido también los Logging para ver los mensajes SOAP, sólo en el de Calculadora. Con todo esto y construyendo un war con las librerías necesarias (las de CXF más nuestras clases), podemos desplegar los Web Services en un servidor Tomcat.


El cliente[editar]

Vamos ahora a hacer el cliente, es un poco más complejo. Lo primero el cliente a partir de la interfaz

import javax.xml.ws.Service;
...
      URL wsdlURL = new URL(
            "http://localhost:8080/ejemplo_cxf/Calculadora?wsdl");
      QName SERVICE_NAME = new QName("http://cxf.ejemplos.chuidiang.com/",
            "CalculadoraImplService");
      Service service = Service.create(wsdlURL, SERVICE_NAME);
      Calculadora calculadora = service.getPort(Calculadora.class);

Ahora debemos habilitarle la parte de WS-Addressing

      Client client = ClientProxy.getClient(calculadora);
      Endpoint endPoint = client.getEndpoint();
      endPoint.getActiveFeatures().add(new WSAddressingFeature());
      client.getOutInterceptors().add(new MAPAggregator());
      client.getOutInterceptors().add(new MAPCodec());

Hemos puesto la Feature de Addressing y hemos puesto unos interceptores que nos pide la documentación de CXF. Sin estos no funcionará. La documentación de hecho pide que pongamos estos interceptores también a los InInterceptors y a los FaultOutInterceptor y FaultInInterceptor. En fin, sólo con lo puesto funciona.

Y ya está listo. Ahora sólo tenemos, antes de cada llamada a un método del cliente, rellenar los parámetros que irán en la cabecera SOAP indicando el número de mensaje, la URL a la que responder, etc, etc. Podemos no rellenar nada y CXF los rellenará por nosotros, poniendo un número de mensaje e indicando que la respuesta nos la deben devolver a nuestro propio cliente, funcionando todo como un cliente normal, aunque por debajo lleve el WS-Addressing.

Vamos a rellenar parámetros

private static final ObjectFactory WSA_OBJECT_FACTORY = new ObjectFactory();


      // get Message Addressing Properties instance
      AddressingBuilder builder = AddressingBuilder.getAddressingBuilder();
      AddressingProperties maps = builder.newAddressingProperties();

      // set MessageID property
      AttributedURIType messageID = WSA_OBJECT_FACTORY
            .createAttributedURIType();
      messageID.setValue("urn:uuid:" + System.currentTimeMillis());
      maps.setMessageID(messageID);

      AttributedURIType replyTo = WSA_OBJECT_FACTORY.createAttributedURIType();
      replyTo.setValue("http://localhost:8080/ejemplo_cxf/Resultado");
      EndpointReferenceType eprt = WSA_OBJECT_FACTORY
            .createEndpointReferenceType();
      eprt.setAddress(replyTo);
      maps.setReplyTo(eprt);

Listo, componemos un messageId con un currentTimeMillis() e indicamos que se debe llamar al web service Resultado para pasar los resultados. Ahora debemos meter ese maps en la petición y hacer la llamada

         Map<String, Object> requestContext = ((BindingProvider) calculadora)
               .getRequestContext();
         requestContext.put(JAXWSAConstants.CLIENT_ADDRESSING_PROPERTIES, maps);

         Holder<Double> holder = new Holder<Double>();
         holder.value = 11.2;
         calculadora.suma(holder, 33.4);

El resultado de la llamada lo recibirá nuestra clase Resultado.