4.3. Invocación de Servicios Web

Vamos a ver ahora cómo invocar Servicios Web orientados a RPC desde Java. Para ello contamos con la API JAX-RPC, que se apoya en la API SAAJ para gestionar los mensajes SOAP orientados a RPC.

Con JAX-RPC podremos ejecutar procedimientos de forma remota, simplemente haciendo una llamada a dicho procedimiento, sin tener que introducir apenas código adicional. Será JAX-RPC quien se encargue de gestionar internamente la conexión con el servicio y el manejo de los mensajes SOAP de llamada al procedimiento y de respuesta.

Podemos encontrar las clases de la API de JAX-RPC dentro del paquete javax.xml.rpc y en subpaquetes de éste.

Nuestro cliente Java realizado con JAX-RPC será interoperable con prácticamente todos los servicios creados desde otras plataformas. Se pueden ver los resultados de los tests de interoperabilidad de JAX-RPC en la dirección:

http://java.sun.com/wsinterop/sb/index.html

4.3.1 Tipos de acceso

Tenemos varias posibilidades para acceder a un Servicio Web utilizando JAX-RPC:

4.3.2 Invocación mediante stub estático

Está será la forma más sencilla de acceder siempre que contemos con una herramienta que genera el stub de forma automática.

De esta forma, una vez generado el stub, sólo tendremos que utilizar este stub como si se tratase de nuestro servicio directamente. En el stub podremos hacer las mismas llamadas a métodos que haríamos directamente en la clase que implemente nuestro servicio, ya que ambos implementarán la misma interfaz. El stub generado implementará la interfaz Stub, además de la interfaz de nuestro servicio.

En WSDP podemos crear nuestro stub de forma sencilla a partir de la interfaz Java (RMI) de nuestro servicio, o bien del documento WSDL si no contamos con la interfaz Java. Para ello utilizaremos la herramienta xrpcc, o wscompile en las últimas versiones, para generar la parte del cliente.

Vamos a ver cómo se crearía mediante el ejemplo del servicio Conversion que vimos en el tema anterior, que ofrece métodos para convertir de euros a ptas y viceversa.

A partir de la interfaz Java

Supongamos que tenemos la interfaz de nuestro servicio definida en el fichero ConversionIF.java de la siguiente forma:

package utils;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface ConversionIF extends Remote {
  public int euro2ptas(double euro) throws RemoteException;
  public double ptas2euro(int ptas) throws RemoteException;
}

Necesitaremos además un fichero config.xml que defina la configuración del Servicio Web como el que se muestra a continuación:

<?xml version="1.0" encoding="UTF-8"?> 
<configuration 
  xmlns="http://java.sun.com/xml/ns/jax-rpc/ri/config">    
  <service name="Conversion" 
    targetNamespace="http://j2ee.ua.es/wsdl" 
    typeNamespace="http://j2ee.ua.es/types" 
    packageName="utils"> 
    <interface name="utils.ConversionIF" 
      servantName="utils.ConversionImpl"/> 
  </service> 
</configuration> 

En este fichero especificamos el nombre de nuestro Servicio Web, los espacios de nombres utilizados, el paquete donde guardaremos las clases del servicio, y el nombre de la interfaz del servicio y de la clase que implementa este servicio (servantName).

Una vez tenemos estos ficheros podemos generar las clases necesarias automáticamente mediante la herramienta xrpcc introduciendo el siguiente comando:

xrpcc.sh config.xml -client -classpath .  LINUX
xrpcc config.xml -client -classpath .     WINDOWS

En las nuevas versiones de WSDP esta herramienta se ha sustituido por la herramienta wscompile, aunque xrpcc todavía se mantenga por cuestiones de compatibilidad. Por ello será recomendable utilizar la nueva herramienta, que es similar a la anterior:

wscompile.sh config.xml -gen:client -classpath .  LINUX
wscompile config.xml -gen:client -classpath .     WINDOWS

NOTA: Si no especificamos el classpath explícitamente en la línea de comando es posible que no consiga localizar las clases necesarias.

A partir de la descripción WSDL

Si nosotros no hemos desarrollado el Servicio Web que queremos usar, lo normal será que no tengamos la interfaz Java de dicho servicio. Es más, puede ocurrir que el Servicio Web ni siquiera esté implementado en Java. En este caso deberemos recurrir al fichero WSDL que describa el servicio para generar nuestro cliente.

En este caso utilizaremos la herramienta xrpcc o wscompile al igual que en el caso anterior. Tendremos un fichero de configuración config.xml en el que deberemos indicar la dirección donde se encuentra la descripción WSDL del servicio, así como el paquete en el que se guardarán las clases generadas para el stub:

<?xml version="1.0" encoding="UTF-8"?> 
<configuration 
  xmlns="http://java.sun.com/xml/ns/jax-rpc/ri/config"> 
  <wsdl location="http://localhost:8080/conv/Conversion.wsdl"    
    packageName="utils"> 
  </wsdl> 
</configuration>

Para generar el stub deberemos introducir el siguiente comando con la antigua herramienta xrpcc:

xrpcc.sh config.xml -client  LINUX
xrpcc config.xml -client     WINDOWS

O utilizar la nueva herramienta wscompile, cosa que será recomendable ya que xrpcc podría desaparecer en próximas versiones:

wscompile.sh config.xml -gen:client		LINUX
wscompile config.xml -gen:client		WINDOWS

Esto nos generará la interfaz RMI del servicio dentro del paquete que hayamos indicado. Además habrá creado las clases auxiliares necesarias para invocar el servicio mediante JAX-RPC. La interfaz del servicio que podremos utilizar desde nuestro programas se habrá generado en una clase cuyo nombre será el nombre del tipo de puerto al que vayamos a acceder. Si utilizamos la opción -keep de wscompile, para que no elimine los fuentes, podremos consultar esta interfaz para conocer los métodos que podremos invocar sobre ella.

Tendremos que obtener un objeto Stub que implemente dicha interfaz, y que será quien nos dé acceso al servicio. Para ello nos habrá generado un fichero con el sufijo <Nombre_del_servicio>_Impl que podremos instanciar desde nuestro cliente mediante un constructor vacío, y a partir de él obtener el Stub para acceder a nuestro servicio. Para ello tendrá definido un método get<Nombre_del_tipo_de_puerto>() que nos devolverá el objeto Stub correspondiente al puerto para acceder al servicio, que podremos referenciar mediante la interfaz <Nombre_del_tipo_de_puerto>.

En el próximo punto veremos con más detalle como utilizar estos objetos en nuestro cliente para obtener el objeto Stub y acceder al servicio.

Implementación del cliente

Con cualquiera de estos dos métodos anteriores habremos generado una serie de clases necesarias para invocar el servicio. Entre ellas, tendremos una clase llamada <Nombre_del_servicio>_Impl que será la que deberemos utilizar dentro de nuestra aplicación cliente para obtener un stub. Esta clase tendrá un método get<Nombre_del_tipo_de_puerto>() que nos devolverá el stub que buscamos.

Stub stub = (Stub)(new Conversion_Impl().getConversionIFPort()); 

Deberemos indicar al stub al menos cual es la dirección donde se encuentra atendiendo el servicio (su endpoint). Esto lo estableceremos como una propiedad del objeto Stub obtenido:

stub._setProperty(javax.xml.rpc.Stub.ENDPOINT_ADDRESS_PROPERTY, 
   endpoint);  

Para finalizar, tendremos que hacer una conversión cast del stub obtenido a la interfaz de nuestro servicio, para poder invocar de esta forma los métodos de nuestro servicio.

En el caso del ejemplo anterior podremos acceder al stub (o proxy) generado de la siguiente forma:

private static ConversionIF creaProxy(String endpoint) {
  Stub stub = (Stub)(new Conversion_Impl().getConversionIFPort());
  stub._setProperty(javax.xml.rpc.Stub.ENDPOINT_ADDRESS_PROPERTY, 
    endpoint);
  return (ConversionIF)stub;
}

Este será el único código adicional que debamos insertar para acceder a nuestro servicio. Una vez obtenido este stub podremos invocar los métodos del servicio como si se tratase de un objeto local:

public static void main(String[] args) {
  try {
    ConversionIF conv = creaProxy(args[0]);
    int ptas = conv.euro2ptas(Double.parseDouble(args[1]));
    System.out.println(args[1] + " euros son " + ptas + " ptas");
  } catch (Exception e) {
    e.printStackTrace();
  }
} 

Crear el cliente con Tomcat

En lugar de tener que utilizar las herramientas en línea de comandos, podremos utilizar ant para automatizar la creación de capas del cliente. A continuación se muestra un ejemplo de buildfile para realizar esta tarea:

<project name="Conversion" default="client" basedir=".">

<!-- Propiedades --> <property name="jwsdp.home" value="c:\\jwsdp-1.3"/>
<property name="main.class" value="Cliente"/>

<property name="bin.home" value="${basedir}/bin"/>
<property name="src.home" value="${basedir}/src"/>

<property name="compile.debug" value="true"/>
<property name="compile.deprecation" value="false"/>
<property name="compile.optimize" value="true"/>

<property name="config.file" value="${basedir}/etc/config.xml"/>

<!-- Classpath -->

<path id="compile.classpath"> <fileset dir="${jwsdp.home}/jwsdp-shared/lib"> <include name="*.jar"/> </fileset> <fileset dir="${jwsdp.home}/jaxp/lib"> <include name="*.jar"/> </fileset> <fileset dir="${jwsdp.home}/jaxp/lib/endorsed"> <include name="*.jar"/> </fileset> <fileset dir="${jwsdp.home}/jaxrpc/lib"> <include name="*.jar"/> </fileset> <fileset dir="${jwsdp.home}/saaj/lib"> <include name="*.jar"/> </fileset> <fileset dir="${jwsdp.home}/apache-ant/lib"> <include name="*.jar"/> </fileset> </path>
<!-- Definicion de tareas -->
<taskdef name="wscompile"
classname="com.sun.xml.rpc.tools.ant.Wscompile"> <classpath refid="compile.classpath"/> </taskdef> <taskdef name="wsdeploy"
classname="com.sun.xml.rpc.tools.ant.Wsdeploy"> <classpath refid="compile.classpath"/> </taskdef> <!-- Objetivos -->

<target name="client"
description="Genera las capas del cliente">
<wscompile keep="true"
client="true"
base="${bin.home}"
sourcebase="${src.home}"
xPrintStackTrace="true"
verbose="true"
config="${config.file}">
<classpath>
<path refid="compile.classpath"/>
</classpath>
</wscompile>
</target>

<target name="compile" depends="client"
description="Compila el cliente">
<javac srcdir="${src.home}" destdir="${bin.home}"
debug="${compile.debug}"
deprecation="${compile.deprecation}"
optimize="${compile.optimize}">
<classpath refid="compile.classpath"/>
</javac>
</target>

<target name="run" depends="compile"
description="Ejecuta el cliente">
<java classname="${main.class}"
fork="true">
<classpath>
<path refid="compile.classpath"/>
<pathelement location="${bin.home}"/>
</classpath>
</java>
</target>

</project>

Para utilizar este buildfile deberemos tener nuestro directorio de desarrollo estructurado de la siguiente forma:

src Código fuente del cliente
etc Contendrá el fichero de configuración config.xml.

Tendremos disponibles las siguientes tareas de ant:

client: Crea las capas necesarias para acceder al servicio desde el cliente (stub). Generará en src los fuentes que implementan estas capas, y en bin estas mismas clases compiladas. Esto será lo primero que deberemos hacer, ya que para implementar el cliente deberemos contar con el stub para acceder al servicio. Después de generar el stub, escribiremos el código de nuestro cliente que utilice este stub para acceder al servicio. Una vez tengamos implementado el cliente deberemos compilarlo con el siguiente objetivo.

compile: Compila las clases de nuestra aplicación, cuyos fuentes están ubicados en el directorio src, produciendo las clases compiladas en el directorio bin.

run: Ejecuta la clase principal de nuestro cliente.

4.3.3 Invocación mediante proxy dinámico

Con este método no tendremos que haber generado previamente de forma estática el stub para nuestro servicio, sino que éste será generado de forma dinámica en tiempo de ejecución.

Para hacer esto, deberemos proporcionar la descripción WSDL del Servicio Web, además de una interfaz Java que implemente los métodos de dicho servicio.

Ahora ya no necesitamos ninguna herramienta para generar las clases del cliente, ya que no vamos a generar ninguna clase. Pasaremos directamente a introducir el código de nuestro cliente.

Deberemos introducir en nuestro programa los nombres que se le ha dado al servicio y a los puertos dentro del XML. Estos nombres normalmente constarán de su nombre local y del espacio de nombres al que pertenezcan, aunque pueden venir dados únicamente por un nombre local. Para representar estos nombres (qualified names) tenemos el objeto QName en java, que se puede construir de dos formas distintas, según si pertenece a un espacio de nombres o no:

QName nombre = new QName(namespace, nombre_local);
QName nombre = new QName(nombre_local);

Lo primero que deberemos hacer es obtener un objeto Service correspondiente al servicio que queremos utilizar. Para ello necesitamos previamente un ServiceFactory que nos permita construir objetos Service:

 ServiceFactory sf = ServiceFactory.newInstance();

Ahora podremos crear nuestro objeto Service indicando la dirección donde se encuentra el documento WSDL, y el nombre del servicio al que queremos acceder:

Service serv = sf.createService(
  new URL("http://localhost:8080/conversion/Conversion.wsdl"), 
  new QName("http://j2ee.ua.es/wsdl", "Conversion"));

Una vez tenemos el servicio, ya sólo nos queda acceder al puerto concreto que vayamos a utilizar del servicio. Tendremos que indicar el nombre del puerto, y la clase de una interfaz que se ajuste al puerto que queremos utilizar. En el caso del servicio de Conversion, podemos utilizar la interfaz que hemos definido anteriormente (ConversionIF):

ConversionIF conv = (ConversionIF) serv.getPort(
  new QName("http://j2ee.ua.es/wsdl", "ConversionIFPort"), 
  utils.ConversionIF.class); 

Una vez hecho esto podremos acceder al servicio a través de dicho objeto, de la misma forma que si accediésemos directamente al servicio, ya que implementará la misma interfaz:

int ptas = conv.euro2ptas(Double.parseDouble(args[1]));
System.out.println(args[1] + " euros son " + ptas + " ptas");

Este método nos obliga a introducir más código en el cliente para conectar con el servicio, pero tiene la ventaja de no necesitar utilizar herramientas adicionales, y si introducimos algún cambio en el servicio, no tendremos que volver a generar las clases del cliente.

4.3.4 Interfaz de invocación dinámica (DII)

Mediante esta interfaz, ya no utilizaremos un stub para invocar los métodos del servicio, sino que nos permitirá invocar los métodos de forma dinámica, indicando simplemente el nombre del método que queremos invocar como una cadena de texto, y sus parámetros como un array de objetos.

Esto nos permitirá utilizar servicios que no conocemos previamente. De esta forma podremos implementar por ejemplo un broker de servicios. Un broker es un servicio intermediario, al que podemos solicitar alguna tarea que necesitemos. Entonces el broker intentará localizar el servicio más apropiado para dicha tarea en un registro de servicios, y lo invocará por nosotros. Una vez haya conseguido la información que requerimos, nos la devolverá. De esta forma la localización de servicios se hace totalmente transparente para nosotros.

Podremos acceder con esta interfaz tanto si contamos con un documento WSDL como si no contamos con él, pero en el caso de que no tengamos el WSDL deberemos especificar en el código todos los datos incluidos en estos documentos que necesitemos y de los que en este caso no disponemos (endpoint, parámetros y tipos, etc).

A partir de un documento WSDL

Vamos a ver el caso en el que contamos con el documento WSDL que describe el servicio. El primer paso será conseguir el objeto Service igual que hicimos en el caso anterior:

ServiceFactory sf = ServiceFactory.newInstance(); 
Service serv = sf.createService(
  new URL("http://localhost:8080/conversion/Conversion.wsdl"), 
  new QName("http://j2ee.ua.es/wsdl", "Conversion"));

Utilizaremos el objeto Call para hacer las llamadas dinámicas a los métodos del servicio. Deberemos crear un objeto Call correspondiente a un determinado puerto y operación de nuestro servicio:

Call call = serv.createCall(
  new QName("http://j2ee.ua.es/wsdl", "ConversionIFPort"),
  new QName("http://j2ee.ua.es/wsdl", "euro2ptas"));

El último paso será invocar la llamada que hemos creado:

Integer result = (Integer) call.invoke(
                 new Object[] { new Double(30.0) });

A este método le debemos proporcionar un array de objetos como parámetro, ya que debe poder utilizarse para cualquier operación, con diferente número y tipo de parámetros. Como tampoco se conoce a priori el valor devuelto por la llamada, deberemos hacer una conversión cast al tipo que corresponda, ya que nos devuelve un Object genérico.

Sin un documento WSDL

Si no contamos con el WSDL del servicio, crearemos un objeto Service proporcionando únicamente el nombre del servicio:

ServiceFactory sf = ServiceFactory.newInstance(); 
Service serv = sf.createService( 
  new QName("http://j2ee.ua.es/wsdl", "Conversion"));

A partir de este objeto podremos obtener un objeto Call para realizar una llamada al servicio de la misma forma que vimos en el caso anterior:

Call call = serv.createCall(
  new QName("http://j2ee.ua.es/wsdl", "ConversionIFPort"),
  new QName("http://j2ee.ua.es/wsdl", "euro2ptas"));

En este caso el objeto Call no tendrá ninguna información sobre las características del servicio, ya que no tiene acceso al documento WSDL que lo describe, por lo que deberemos proporcionárselas nosotros explícitamente.

En primer lugar, deberemos especificar el endpoint del servicio, para que sepa a qué dirección debe conectarse para acceder a dicho servicio:

call.setTargetEndpointAddress(endpoint);

Una vez especificada esta información, deberemos indicar el tipo de datos que nos devuelve la llamada a la operación que vamos a invocar (en nuestro ejemplo euro2ptas):

QName t_int = 
  new QName("http://www.w3.org/2001/XMLSchema", "int");
call.setReturnType(t_int);

Por último, indicaremos los parámetros de entrada que toma la operación y sus tipos:

QName t_double = 
  new QName("http://www.w3.org/2001/XMLSchema", "double");
call.addParameter("double_1", t_double, ParameterMode.IN);

Una vez hecho esto, podremos invocar dicha operación igual que en el caso anterior:

Integer result = (Integer) call.invoke(
                 new Object[] { new Double(30.0) });