Programación en castellano
Inicio > Tutoriales > J2EE > Escribir Aplicaciones Avanzadas para la Plataforma Java 2
-Tutoriales

Escribir Aplicaciones Avanzadas para la Plataforma Java 2


Cálculo Distribuido

Tan recientemente como hace diez años, el cálculo distribuido generalmente significaba que teniamos clientes PCs en una habitación con un servidor en otra. El problema con esta arquitectura es que si se pierde la conexión con el servidor, los clientes no pueden actualizar las bases de datos de la compañía.

Para evitar esta pérdida de tiempo, se crearon los diferentes modelos de red. Un ejemplo es el modelo de servidor maestro y esclavo donde si el maestro falla, el esclavo toma el relevo. El problema con los distintos modelos de red es que todos requieren alguna forma de intervención manual y se unieron con un sistema operativo o un lenguaje. Y aunque estas aproximaciones consiguieron reducir el tiempo de parada, no cumplen con los sistemas de distribución heterogénea que consiste en una mezcla de protocolos de red y máquinas.

La plataforma Java combinada con otros avances como Common Object Request Broker Architecture (CORBA), servidores multi-fila, y redes sin cables han llevado un paso mas allá la realización de la computación totalmentedistribuida, de la tradicional aproximación cliente y servidor.

Ahora podemos construir aplicaciones que incluyan servicios de redundancia por defecto. Si una conexión de servidor falla, podemos usar un servicio de otro servidor. CORBA y los puentes "Distributed Component Object Model" (DCOM) significan que los objetos pueden ser transferidos entre casi todas las máquinas y lenguajes. Y con el nuevo sistema de software Jini, el entorno de cálculo distribuido pude estar pronto en todos los hogares, oficinas o escuelas.

. Servicios de Búsqueda

Los servicios de búsqueda permiten las comunicaciones a través de la red. Un programa cliente puede usar un protocolo de búsqueda para obtener información sobre programas remotos o máquinas que usen esa información para establecer una comunicación.

  • Un servicio de búsqueda común con el que podríamos estar familiarizados es el Directory Name Service (DNS). Mapea direcciones de Internet Protocol (IP) a nombres de máquinas. Los programas usan el mapeo DNS para buscar direcciones IP asociadas con un nombre de máquina y usar la dirección IP para establecer una comunicación.
  • De la misma forma, el AuctionServlet presentado en el Capítulo 2 usa el servicio de nombres interno de la arquitectura de JavaBeans Enterprise para buscar unas referencias a Beans Enterprise registrados con el servidor de JavaBeans Enterprise.

Además de los servicios de nombres, algunos protocolos de búsqueda proporcionan servicios de directorio. Este servicios como el Lightweight Directory Access Protocol (LDAP) y el NIS+ de Sun proporcionan otra información y servicios más allá de los disponibles con el servicio de nombres. Por ejemplo, NIS+ asocia un atributo workgroup con una cuenta de usuario. Este atributo puede usarse para restringir el acceso a una máquína, por lo que sólo los usuarios especificados en el workgroup tienen acceso.

Este capítulo describe como se usa el "Naming and Directory Interface (JNDI)" de Java en la aplicación de subastas para buscar los Beans de Enterprise. También explica como usar algunos de los otros muchos servicios de búsqueda que tenemos disponibles. El código para usar estos servicios no es tan sencillo como el código de la búsqueda en la aplicación de la subasta del capítulo 2, pero las ventajas que ofrecen estos otros servicios hacen que algunas veces merezca la pena ese código más complejo.

. Java Naming and Directory Interface (JNDI)

El API de JNDI hace sencillo conectar servicios de búsqueda de varios proveedores en un programa escrito en lenguaje Java. Siempre que el cliente y el servidor usen el mismo servicio de búsqueda, el cliente puede fácilmente buscar información registrada en el servidor y establecer una comunicación.

Los Beans de sesión de la aplicación de subasta usan JNDI y una fábrica de nombres JNDI especial de BEA Weblogic para buscar Beans de entidad. Los servicios JNDI normalmente inicializan la fábrica de nombres como una propiedad de la línea de comandos o como un valor de inicialización.

Primero, la fábrica de nombres weblogic.jndi.TengahInitialContextFactory se pone dentro de un objeto java.util.Property, luego este objeto se pasa como parámetro al constructor de InitialContexT. Aquí tenemos un ejemplo del método ejbCreate:

  Context ctx; //JNDI context

  public void ejbCreate() 
	throws CreateException, RemoteException {
    Hashtable env = new Hashtable();
    env.put(Context.INITIAL_CONTEXT_FACTORY, 
     "weblogic.jndi.TengahInitialContextFactory");
    try{
     ctx = new InitialContext(env);
    }catch(Exception e) {
     System.out.println("create exception: "+e);
    }
  }

Una vez creado, el contexto JNDI se usa para buscar los interfaces principales de los Beans Enterprise. En este ejemplo, se recupera una referencia a un Bean Enterprise uinda al nombre registration y se usa para operaciones posteriores:

  RegistrationHome rhome =
        (RegistrationHome) ctx.lookup("registration");
  RegistrationPK rpk=new RegistrationPK();
  rpk.theuser=buyer;
  Registration newbidder =
        rhome.findByPrimaryKey(rpk);

En el lado del servidor, el descriptor de desarrollo para el RegistrationBean tiene su valor beanhomename como registration. Las herramientas de JavaBeans de Enterprise generan el resto del código de nombres para el servidor.

El servidor llama a ctx.bind para unir el nombre registration al contexto JNDI. El parámetro this referencia a la clase _stub que representa el RegistrationBean.

  ctx.bind("registration", this);

JNDI no es la única forma de localizar objetos remotos. Los servicios de búsqueda también están disponibles en las plataformas RMI, JNI y CORBA. Podemos usar directamente los servicios de búsqueda de estas plataformas directamente desde el API del JNDI. JNDI permite a las aplicaciones cambiar el servicio de nombres con poco esfuerzo. Por ejemplo, aquí está el código que hace que el método BidderBean.ejbCreate use el servicio de búsqueda de org.omb.CORBA en vez del servicio de búsqueda por defecto de BEA Weblogic.

  Hashtable env = new Hashtable();
  env.put("java.naming.factory.initial", 
	"com.sun.jndi.cosnaming.CNCtxFactory");
  Context ic = new InitialContext(env);
>

. Servico de Nombres CORBA

El "Common Object Request Broker Architecture" (CORBA) define una especificación para que los objetos de un sistema distribuido se comuniquen unos con otros. Los objetos que usan la especificación CORBA para comunicarse se llaman objetos CORBA, y consisten en objetos cliente y servidor.

Los objetos CORBA puede estar escritos en cualquier lenguaje con el mapeo "Interface Definition Language" (IDL). Estos lenguajes incluyen lenguajes de programación como Java, C++, y muchos otros lenguajes tradicionales no orientados a objetos.

El servicio de búsqueda de nombres, al igual que otras especificaciones CORBA, está definido en términos de IDL. El módulo IDL para el servicio de búsqueda CORBA se llama CosNaming. Cualquier plataforma con un mapeo IDL, como la herramienta idltojava, puede usar este servicio para descubrir objetos CORBA. El módulo IDL para este servicio de búsqueda CORBA está disponible en la plataforma Java 2 en el paquete org.omg.CosNaming.

El interface clave en el módulo CosNaming es NamingContext. Este interface define métodos para unir objetos a un nombre, listar estas uniones, y recuperar referencias a dichos objetos.

Además de estos interfaces públicos hay clases de ayuda. La clase NameComponent se usa en programas cliente y servidor CORBA para construir el nombre completo para el nombre del objeto referencia. El nombre completo es un array de uno o más NameComponents que indica donde encontrar los objetos. El esquema de nombrado puede ser específico de la aplicación.

Por ejmplo en la aplicación de subastas, el nombre completo puede ser definido para usar auction como la raíz del contexto de nombres y RegistrationBean y AuctionItemBean como hijos del contexto raíz. Esto en efecto utiliza un esquema de nombres similar al usado a los paquetes de clases.

En este ejemplo, la aplicación de subastas a adaptado SellerBean a un servicio de nombres CORBA para buscar el RegistrationBean CORBA. El siguiente código se ha extraído de SellerBean, y actúa como un cliente CORBA, y el servidor CORBA RegistrationServer.

. CORBA RegistrationServer

Este código del programa RegistrationServer crea un objeto NameComponent que indica dónde está localizado el RegistrationBean usando auction y RegistrationBean como el nombre completo:

  NameComponent[] fullname = new NameComponent[2];
  fullname[0] = new NameComponent("auction", "");
  fullname[1] = new NameComponent(
                      "RegistrationBean", "");

El siguiente códido une el fullname como un nuevo contexto. Los primeros elementos en el nombre completo (auction en este ejemplo) son huecos para construir el árbol del contexto de nombrado. El último elemento del nombre completo (RegistrationBean en este ejemplo) es el nombre enviado para unirlo al objeto:

  String[] orbargs = { "-ORBInitialPort 1050"};
  ORB orb = ORB.init(orbargs, null) ;

  RegistrationServer rs= new RegistrationServer();
  orb.connect(rs);

  try{
    org.omg.CORBA.Object nameServiceObj = 
	orb.resolve_initial_references("NameService");
    NamingContext nctx = 
	NamingContextHelper.narrow(nameServiceObj);
    NameComponent[] fullname = new NameComponent[2];
    fullname[0] = new NameComponent("auction", "");
    fullname[1] = new NameComponent(
                        "RegistrationBean", "");

    NameComponent[] tempComponent = 
                      new NameComponent[1];
    for(int i=0; i < fullname.length-1; i++ ) {
       tempComponent[0]= fullname[i];
       try{
          nctx=nctx.bind_new_context(tempComponent);
       }catch (Exception e){}
    }
    tempComponent[0]=fullname[fullname.length-1];

    // finally bind the object to the full context path
    nctx.bind(tempComponent, rs);

Una vez que se ha unido el objeto RegistrationServer, puede ser localizado con una búsqueda JNDI usando el proveedor de servicio CosNaming como se describe al final de la sección JNDI, o usando el servicio de búsquedas de nombres CORBA. De cualquier forma, el servidor de nombres CORBA debe arrancarse antes de que pueda suceder cualquier búsqueda. En la plataforma Java 2, el nameserver CORBA se arranca con este comando:

  tnameserv

Esto arranca el RegistrationServer CORBA en el puerto TCP por defecto 900. Si necesitamos usar otro puerto diferente, podemos arrancar el servidor de esta forma. En sistemas Unix, sólo el administrador puede acceder a los números de puerto inferiores a 1025,

  tnameserv -ORBInitialPort 1091

. CORBA SellerBean

En el lado del cliente, la búsqueda CORBA usa el objeto NameComponent para construir el nombre. Arrancamos el servidor de objetos de esta forma:

java registration.RegistrationServer

La diferencia en el cliente es que este nombre se pasa al método resolve que devuelve el objeto CORBA. El siguiente código del objeto SellerBean ilustra este punto:

  String[] args = { "-ORBInitialPort 1050"};
  orb = ORB.init(args, null) ;
  org.omg.CORBA.Object nameServiceObj = 
	orb.resolve_initial_references("NameService") ;
  nctx= NamingContextHelper.narrow(nameServiceObj);

  NameComponent[] fullname = new NameComponent[2];
  fullname[0] = new NameComponent("auction", "");
  fullname[1] = new NameComponent(
                      "RegistrationBean", "");

  org.omg.CORBA.Object cobject= nctx.resolve(fullname);

El método narrow, desde el método Helper, es generado por el compilador IDL, que proporciona una mapeo detallado para traducir cada campo CORBA en su respectivo campo del lenguaje Java. Por ejemplo, el método SellerBean.insertItem busca un objeto CORBA registrado usando el nombre RegistrationBean, y devuelve un objeto RegistrationHome. Con el objeto RegistrationHome podemos devolver un registro Registration llamando a su método findByPrimaryKey.

  org.omg.CORBA.Object cobject= nctx.resolve(fullname);
  RegistrationHome regHome=
  RegistrationHomeHelper.narrow(cobject);
  RegistrationHome regRef = 
	RegistrationHomeHelper.narrow(
	  nctx.resolve(fullname));
  RegistrationPKImpl rpk= new RegistrationPKImpl();
  rpk.theuser(seller);
  Registration newseller = 
	RegistrationHelper.narrow(
	  regRef.findByPrimaryKey(rpk));
  if((newseller == null)|| 
	(!newseller.verifyPassword(password))) {
    return(Auction.INVALjdcbook_USER);
  }

. Interoperable Object References (IOR)

Usar un servicio de nombres CORBA funciona para la mayoría de las aplicaciones CORBA, especialmente cuando el (ORB) está suministrado por un vendedor. Sin embargo, podríamos encontrar que el servicio de nombres no es totalmente compatible con todos los ORBs, y podríamos obtener el frustante mensaje COMM_FAILURE cuando el cliente CORBA intente conectarse con el servidor CORBA.

La solución es usar un "Interoperable Object Reference" (IOR) en su lugar. Este está disponible en los ORBs que soportan el protocolo "Internet Inter-ORB Protocol" (IIOP). Conteine la información que el servicio de nombres podría mantener para cada objeto como el host y el puerto donde reside el objeto, una única clave de búsqueda para el objeto en ese host, y qué version de IIOP soporta.

. Servidor IOR

Para crear un IOR todo lo que tenemos que hacer es llamar al método object_to_string desde la clase ORB y pasarle un ejemplar del objeto. Por ejemplo, para convertir el objeto RegistrationServer en un IOR, necesitamos añadir la línea String ref = orb.object_to_string(rs); del siguiente código en el programa principal:

  String[] orbargs=  {"-ORBInitialPort 1050"};
  ORB orb = ORB.init(orbargs, null);
  RegistrationServer rs = new RegistrationServer();
//Add this line
  String ref = orb.object_to_string(rs);

Por eso, en lugar de recuperar la información de este objeto desde un servicio de nombres, hay otra forma para que el servidor envíe esta información a un cliente. Podemos registrar el string devuelto con un nombre sustitutivo del servidor, que puede ser un sencillo servidor web HTTP porque el objeto ya está en un formato transmitible.

. Cliente IOR

Este ejemplo usa una conexión HTTP para convertir el string IOR de nuevo en un objeto. Podemos llamar al método string_to_object desde la clase ORB. Este método llama al IOR desde el RegistrationServer y devuelve el string ORB. Este string se pasa al ORB usando el método ORB.string_to_object, y el ORB devuelve la referencia al objeto remoto:

  URL iorserver = new URL(
        "http://server.com/servlet?object=registration");
  URLConnection con = ioserver.openConnection();
  BufferedReader br = new BufferReader(
        new InputStreamReader(con.getInputStream));
  String ref = br.readLine();
  org.omg.CORBA.Object cobj = orb.string_to_object(ref);
  RegistrationHome regHome =
        RegistrationHomeHelper.narrow(cobj);

El nombre sustituto del servidor puede mantener registros persistentes IOR que pueden sobrevivir a paradas si es necesario.

. Remote Method Invocation (RMI)

El API "Remote Method Invocation" (RMI) originalmente usaba su propio protocolo de comunicación llamado "Java Remote Method Protocol" (JRMP), que resultaba en tener su propio servicio de búsqueda. Las nuevas versiones de RMI pueden usar el protocolo IIOP, además de JRMP, RMI-IIOP se cubre en la siguiente sección.

El servicio de nombrado del JRMP RMI es similar a otros servicios de búsqueda y nombrado. La búsqueda real se consigue llamando a Naming.lookup y pasándole un parámetro URL a este método. La URL especifica el nombre de la máquina, y opcionalmente el puerto donde está el servidor de nombres, rmiregistry, que sabe que objeto se está ejecutando, y el objeto remoto que queremos referenciar para llamar a sus métodos.

Por ejemplo:

  SellerHome shome =
    (SellerHome)Naming.lookup(
    "rmi://appserver:1090/seller");

Este código devuelve la referencia remota de SellerHome desde el objeto unido al nombre seller en la máquina llamada appserver. La parte rmi de la URL es opcional y podríamos haber visto URLs RMI sin ella, pero si estámos usando JNDI o RMI-IIOP, incluir rmi en la URL nos ahorra confusiones posteriores. Una vez que tenemos la referencia a SellerHome, podemos llamar a sus métodos.

En contraste con la búsqueda JNDI realizada por AuctionServlet.java, que requiere una búsqueda de dos estados para crear un contexto y luego la búsqueda real, RMI inicializa la conexión con servidor de nombres RMI, rmiregistry, y también obtiene la referencia remota con una llamada.

Esta referencia remota será el cliente inquilino de rmiregistry. Inquilino significa que a menos que el cliente informe al servidor de que todavía necesita una referencia al objeto, el alquiler expira y la memoria es liberada. Esta operación de alquiler es transparente para el usuario, pero puede ser ajustada seleccionando el valor de la propiedad java.rmi.dgc.leaseValue en el servidor, en milisegundos cuando se arranca el servidor de esta forma:

  java -Djava.rmi.dgc.leaseValue=120000 myAppServer

. RMI sobre Internet Inter-ORB Protocol (IIOP)

La ventaja de RMI sobre "Internet Inter-ORB Protocol " (IIOP), significa que el código RMI existente puede referenciar y buscar un objeto con el servicio CosNaming de CORBA. Esto nos ofrece una gran interoperatividad entre arquitecturas con un pequeño cambio en nuestro código RMI existente.

Nota: El compilador rmic proporciona la opción -iiop para generar el stub y las clases tie necesarias para RMI-IIOP.

. Servidor IIOP

El protocolo RMI-IIOP se implementa como un plug-in JNDI, por lo que como antes, necesitamos crear un InitialContext:

  Hashtable env = new Hashtable();
  env.put("java.naming.factory.initial",
        "com.sun.jndi.cosnaming.CNCtxFactory");
  env.put("java.naming.provider.url",
        "iiop://localhost:1091");
  Context ic = new InitialContext(env);

La factoría de nombres debería parecer familiar como el mismo servicio de nombres usado en la sección CORBA. La principal diferencia es la adicción de un valor URL especificando el servicio de nombres al que conectarse. El servicio de nombres usado aquí es el programa tnameserv arrancado en el puerto 1091:

  tnameserv -ORBInitialPort 1091

El otro cambio principal en el lado del servidor es reemplazar las llamadas a Naming.rebind para usar el método rebind de JNDI en el ejemplar InitialContext. Por ejemplo:

Viejo código RMI:

  SellerHome shome=(SellerHome)Naming.lookup(
  "rmi://appserver:1090/seller");

Nuevo código RMI:

  Hashtable env = new Hashtable();
  env.put("java.naming.factory.initial",
	"com.sun.jndi.cosnaming.CNCtxFactory");
  env.put("java.naming.provider.url",
	"iiop://localhost:1091");
  Context ic = new InitialContext(env);

  SellerHome shome= 
	(SellerHome)PortableRemoteObject.narrow(
	ic.lookup("seller"), SellerHome)

. Ciente IIOP

En el lado del cliente, la búsqueda RMI se cambia para usar un ejemplar del InitialContext en lugar del Naming.lookup de RMI. El objeto devuelto es mapeado al objeto requerido usando el método narrow de la clase javax.rmi.PortableRemoteObject. PortableRemoteObject reemplaza UnicastRemoteObject que estaba disponible anteriormente en código de servidor RMI.

Viejo código de búsqueda RMI:

  SellerHome shome= new SellerHome("seller");
  Naming.rebind("seller", shome);

Nuevo código RMI:

   Hashtable env = new Hashtable();
   env.put("java.naming.factory.initial",
	"com.sun.jndi.cosnaming.CNCtxFactory");
   env.put("java.naming.provider.url", 
	"iiop://localhost:1091");
   Context ic = new InitialContext(env);
   
   SellerHome shome= new SellerHome("seller");
   ic.rebind("seller", shome);

El PortableRemoteObject reemplaza al UnicastRemoteObject disponible anteriormente en el código del servidor RMI. El código RMI debería extender UnicastRemoteObject o llamar al método exportObject de la clase UnicastRemoteObject. PortableRemoteObject También contiene un método exportObject equivalente. En la implementación actual, es mejor eliminar explícitamente los objetos no utilizados mediante llamadas a PortableRemoteObject.unexportObject().

. Servicios de Búsqueda JINI

(Para hacerlo más tarde)

. Aumentar el Rendimiento de la Búsqueda

Cuando ejecutemos nuestra aplicación, si encontramos que llevar el objeto a otro ordenador a través de un diskette será más rápido, es que tenemos un problema de configuración de la red. La fuente del problema es cómo se resuelven los nombres de host y las direcciones IP, y aquí tenemos un atajo.

RMI y otros servicios de nombres usan la clase InetAddress para resolver los nombres de host y direcciones IP. InetAddress almacena los resultados para mejorar las llamadas subsecuentes, pero cuando se le pasa una nueva dirección IP o un nombre de servidor, realiza una referencia cruzada entre la dirección IP y el nombre del host. Si suministramos el nombre del host como una dirección IP, InetAddress todavía intentará verificar el nombre del host.

Para evitar este problema, incluimos el nombre del host y la dirección IP en un fichero host en el cliente.

Sistemas Unix: En Unix, el fichero host normalmente es /etc/hosts.

Windows: En windows 95 ó 98, el fichero host es c:\windows\hosts, (el fichero hosts.sam es un fichero de ejemplo). En windows NT, el fichero host es c:\winnt\system32\drivers\etc\hosts.

Todo lo que necesitamos hacer es poner estas líneas en nuestro ficheo host. Las entradas myserver1 y myserver2 son los host donde se ejecutan el servidor remoto y rmiregistry:

127.0.0.1  localhost
129.1.1.1  myserver1
129.1.1.2  myserver2

. Invocación Remota de Métodos

El API de Invocación Remota de Métodos (RMI) permite las comunicaciones entre cliente y servidor a través de la red entre programas escritos en Java. El servidor de JavaBeans Enterprise implementa de forma transparente el código RMI necesario para que el programa cliente pueda referenciar a los Beans Enterprise que se ejecutan en el servidor y acceder a ellos como si se estuvieran ejecutando localmente en el programa cliente.

El tener el RMi incluido internamente el servidor JavaBeans de Enterprise es muy conveniente y nos ahorra tiempo de codificación, pero si necesitamos usar características avanzadas de RMI o integrar RMI con una aplicación existente, necesitamos sobreescribir la implementación por defecto RMI y escribir nuestro propio código RMI.

El capítulo reemplaza el RegistrationBean manejado por contenedor del Capítulo 2: Beans de Entidad y de Sesión con un servidor de registro basado en RMI. El Bean SellerBean del capítulo 2, también se modifica para llamar al nuevo servidor de registro RMI usando una llamada a lookup de Java 2 RMI.

. Sobre RMI

El API RMI nos permite accede a un servidor de objetos remoto desde un programa cliente haciendo sencillas llamadas a métodos del servidor de objetos. Mientras que otras arquitecturas distribuidas para acceder a servidores de objetos remotos como "Distributed Component Object Model" (DCOM) y "Common Object Request Broker Architecture" (CORBA) devuelven referencias al objeto remoto, el API RMI no sólo devuelve referencias, si no que proporciona beneficios adicionales.

  • El API RMI maneja referencias a objetos remotos (llamadas por referencia) y también devuelve una copia del objeto (llamada por valor).
  • Si el programa cliente no tiene acceso local a la clase para la que se ejemplarizó un objeto remoto, los servicios RMI pueden descargar el fichero class.

. Serialización y colocación de Datos

Para transferir ojbjetos, el API RMI usa el API Serialization para empaquetar (colocar) y desempaquetar (descolocar) los objetos. Para colocar un objeto, el API Serialization convierte el objeto a un Stream de bytes, y para descolocar el objeto, el API Serialization convierte un stream de bytes en un objeto.

. RMI sobre IIOP

Una de las desventajas iniciales del RMI era que la única relación con la plataforma Java para escribir interfaces hacen díficil la intregración con sistemas legales existentes. Sin embargo, RMI sobre "Internet Inter-ORB Protocol" (IIOP) explicado en el Capítulo 4: Servicios de Búsqueda permite a RMI comunicarse con cualquier sistema o lenguaje que soporte CORBA.

Si combinamos la integración mejorada con la habilidad de RMI para trabajar a través de firewalls usando proxies HTTP, podríamos encontrar distribuciones para la lógica de nuestro negocio usando RMI más fáciles que una solución basada en sockets.

Nota: La transferencia de código y datos son partes clave de la especificación JINI. De hecho, si añadieramos un servicio de uniones a los servicios RMI crearíamos algo muy similar a los que obtenemos con la arquitectura JINI.

. RMI en la aplicación de Subastas

El RegistrationServer basado en RMI tiene los siguientes métodos nuevos:

  • Un nuevo método create para crear un nuevo usuario.
  • Un nuevo método find para buscar un usuario.
  • Un nuevo método search para la búsqueda personalizada de usuarios en la base de datos.

La nueva búsqueda personalizada sevuelve los resultados al cliente llamante mediante una llamada a un Callbak RMI. Está búsqueda es similar a los métodos finder usados en los Beans de ejemplos usados en los capítulos 2 y 3, excepto en que la versión RMI, puede tardar más tiempo en generar los resultados porque el sevidor de registros remoto llama al método remoto exportado por el cliente SellerBean basado en RMI.

Si el cliente llamante está escrito en Java, y no es, por ejemplo, una página web, el servidor puede actualizar el cliente tan pronto como los resultados estuvieran listos. Pero, el protocolo HTTP usado en la mayoría de los navegadores no permite que los resultados sean enviados sin que haya una petición. Esto significa que el resultado de una página web no se crea hasta que los resultados estén listos, lo que añade un pequeño retraso.

. Introducción a las Clases

Las dos clases principales en la implementación de la subasta basada en RMI son SellerBean y el remoto RegistrationServer. SellerBean es llamado desde AuctionServlet para insertar un ítem para la subasta en la base de datos, y chequear balances negativos en las cuentas.

Los modelos de ejemplo de la arquitectura JavaBeans Enterprise en los que los detalles de registro del usuario se han separado del código para crear y encontrar detalles de registro. Es decir, los detalles de registro de usuario proporcionados por la clase Registration.java se separan del código para crear y encontrar un objeto Registration, que está en la clase RegistrationHome.java.

La implementación del interface remoto de RegistrationHome.java está unida al rmiregistry. Cuando un programa cliente quiere manipular detalles del registro del usuario, primero tiene que buscar la referencia al objeto RegistrationHome.java en el rmiregistry.

. Sumario de Ficheros

Todo los fciheros de código fuente para el ejemplo basado en RMI se describen en la siguiente lista.

  • SellerBean.java: Programa cliente que llama a los métodos remotos RegistrationServer.verifypasswd y RegistrationServer.findLowCreditAccounts. SellerBean también exporta su método updateResults que llama a RegistrationServer cuando completa su búsqueda RegistrationServer.findLowCreditAccounts.
  • RegistrationServer.java: Servidor de objetos remotos que implementa los interfaces remotos RegistrationHome y Registration.
  • Registration.java: Interface remoto que declara los métodos remotos getUser, verifypasswd, y otros métodos para el manejo de los detalles de registro del usuario.
  • RegistrationHome.java: Interface remoto que declara los métodos remotos create, findByPrimaryKey, y findLowCreditAccounts que crean o devuelven ejemplares de detalles de registro.
  • RegistrationImpl.java: El fichero fuente RegistrationServer.java incluye la implementación para el interface remoto Registration como la clase RegistrationImpl.
  • RegistrationPK.java: Clase que representa los detalles de registro de usuario usando sólo la clave primaria del registro de la base de datos.
  • ReturnResults.java: Interface remoto que declara el método updateResults la clase SellerBean lo implementa como callback.
  • AuctionServlet.java: Versión modificada de la clase original AuctionServlet conde las cuentas de registro se crean mediante llamadas directas al RegistrationServer de RMI. El servelt de subasta también llama al método SellerBean.auditAccounts, que devuelve una lista de los usuarios con un bajo balance en la cuenta.

    El método auditAccounts es llamado con la siguiente URL, donde hace un simple chequeo para verificar que la petición viene del host local.

    http://phoenix.eng.sun.com:7001/
    	AuctionServlet?action=auditAccounts
    

También necesitaremos un fichero de policía java.policy para conceder los permisos necesarios para ejecutar el ejemplo en plataformas Java 2.

La mayoría de las aplicaciones RMI necesitan dos permisos socket, para accesos a los socket y a HTTP para especificar los puertos. Los dos permisos de threads fueron listados en una pila cuando sea necesario por la clase RegistrationImpl para crear un thread interno.

En la plataforma Java 2, cuando un programa no tiene todos los permisos que necesita, la "Máquina Virtual Java" genera una pila de seguimiento que lista los permisos que necesitan ser añadidos al fichero de policía de seguridad.

grant {
  permission java.net.SocketPermission 
     "*:1024-65535", "connect,accept,resolve";
  permission java.net.SocketPermission "*:80", 
                        "connect";
  permission java.lang.RuntimePermission 
                         "modifyThreadGroup";
  permission java.lang.RuntimePermission 
                         "modifyThread";
};

. Compilar el Ejemplo

Antes de describir el código basado en RMI de las clases anteriores, aquí está la secuencia de comandos para compilar el ejemplo en las plataformas Unix y Win32:

Unix:
javac registration/Registration.java
javac registration/RegistrationPK.java
javac registration/RegistrationServer.java
javac registration/ReturnResults.java
javac seller/SellerBean.java
rmic -d . registration.RegistrationServer
rmic -d . registration.RegistrationImpl
rmic -d . seller.SellerBean

Win32:
javac registration\Registration.java
javac registration\RegistrationPK.java
javac registration\RegistrationServer.java
javac registration\ReturnResults.java
javac seller\SellerBean.java
rmic -d . registration.RegistrationServer
rmic -d . registration.RegistrationImpl
rmic -d . seller.SellerBean

. Arrancar el Registro RMI

Como estamos usando nuestro propio código RMI, tenemos que arrancar explícitamente el RMI Registry para que el objeto SellerBean pueda encontrar los Beans remotos de Enterprise. El RegistrationServer usa el registro RMI para registrar o unir los Beans enterprise que pueden ser llamados de forma remota. El cliente SellerBean contacta con el registro para buscar y obtener las referencias a los Beans AuctionItem y Registration.

Como RMI permite que el código y los datos sean transferidos, debemos asegurarnos que el sistema classloader no carga clases extras que puedan ser enviadas erróneamente al cliente. En este ejemplo, las clases extras podrían ser las clases Stub y Skel, y las clases RegistrationSever y RegistrationImpl, y para evitar que lo sean cuando arrancamos el registro RMI. Como el path actual podría ser incluido automáticamente, necesitamos arrancar el RMI Registry desde fuera del espacio de trabajo.

Los siguientes comandos evitan el envío de clases extras, desconfigurando la variable CLASSPATH antes de arrancar el Registro RMI en el puerto 1099. Podemos especificar un puerto diferente añadiendo el número de puerto de esta forma: rmiregistry 4321 &. Si cambiamos el número de puerto debemos poner el mismo número en las llamadas al cliente lookup y al servidor rebind.

Unix:
export CLASSPATH=""
rmiregistry &

Win32:
unset CLASSPATH
start rmiregistry

. Arrancar el Servidor Remoto

Una vez que rmiregistry se está ejecutando, podemos arrancar el servidor remoto, RegistrationServer. El programa RegistrationServer registra el nombre registration2 con el servidor de nombres rmiregistry, y cualquier cliente puede usar este nombre para recuperar una referencia al objeto remoto, RegistrationHome.

Para ejecutar el ejemplo, copiamos las clasesRegistrationServer y RegistrationImpl y las clases stub asociadas a un área accesible de forma remota y arrancamos el programa servidor.

Unix:
cp *_Stub.class 
	/home/zelda/public_html/registration
cp RegistrationImpl.class 
/home/zelda/public_html/registration
cd /home/zelda/public_html/registration
java -Djava.server.hostname=
	phoenix.eng.sun.com RegistrationServer

Windows:
copy *_Stub.class 
	\home\zelda\public_html\registration
copy RegistrationImpl.class 
	\home\zelda\public_html\registration
cd \home\zelda\public_html\registration
java -Djava.server.hostname=
	phoenix.eng.sun.com RegistrationServer

Las siguientes propiedades se usan para configurar los clientes y servidores RMI. Estas propiedades pueden seleccionarse dentro del programa o suministrarlas como propiedades en la línea de comandos para la JVM.

  • La propiedad java.rmi.server.codebase especifica dónde se localizan las clases accesibles públicamente. En el servidor esto puede ser un simple fichero URL para apuntar al directorio o fichero JAR que contiene las clases. Si el URL apunta a un directorio, debe terminar con un carácter separador de ficheros , "/".

    Si no usamos un fichero URL, tampoco necesitaremos un servidor HTTP para descargar las clases remotas o tener que enviar manualmente el stub del cliente y las clases de interfaces remotos, por ejemplo, un fichero JAR.

  • La propiedad java.rmi.server.hostname es el nombre completo del host del servidor donde residen las clases con acceso público. Esto es sólo necesario si el servidor tiene problemas para generar por sí mismo un nombre totalmente cualificado.
  • La propiedad java.rmi.security.policy especifica el policy file con los permisos necesarios para ejecutar el objeto servidor remoto y para acceder a la descarga de las clases del servidor remoto.

. Establecer Comunicaciones Remotas

Los programas clientes se comunican unos con otros a través del servidor. El programa servidor consiste en tres ficheros. Los ficheros de interfaces remotos Registration.java y RegistrationHome.java definen los métodos que pueden ser llamados de forma remota, y el fichero RegistrationServer.java de clase define las clases RegistrationServer y RegistrationImpl que implementan los métodos.

Para establecer comunicaciones remotas, tanto el programa cliente como el servidor necesitan acceder a las clases del interface remoto. El servidor necesita las clases del interface para generar la implementación del interface, y el cliente usa el interface remoto para llamar a las implementaciones de los métodos del servidor remoto.

Por ejemplo, SellerBean crea una referencia a el interface RegistrationHome, y no RegistrationServer, la implementación, cuando necesita crear un regisro de usuario.

Junto con los interfaces del servidor y las clases, necesitamos las clases Stub y Skel para establecer comunicaciones remotas. Estas clases se generan cuando ejecutamos el comando del compilador rmic sobre las clases RegistrationServer y SellerBean.

Las clases SellerBean, SellerBean_Stub.class y SellerBean_Skel.class generadas son necesarias para la llamada desde el servidor hasta el cliente SellerBean. Es el fichero _Stub.class en el cliente que coloca y descoloca los datos desde el servidor, mientras que la clase _Skel.class hace los mismo en el servidor.

Nota: En la plataforma Java 2, el fichero del lado delservidor, _Skel.class ya no es necesario porque sus funciones han sido reemplazadas por las clases de la "Java Virtual Machine".

. Colocar Datos

Colocar y descolocar los datos significa que cuando llamamos al método RegistrationHome.create desde SellerBean, esta llamada es reenviada al método RegistrationServer_Stub.create. El método RegistrationServer_Stub.create envuelve los argumentos del método y los envía a un stream serializado de bytes para el método RegistrationServer_Skel.create.

El método RegistrationServer_Skel.create desenvuelve el stream de bytes serializado, re-crea los argumentos de la llamada original a RegistrationHome.create, y devuelve el resultado de la llamada real RegistraionServer.create de vuelta, junto con la misma ruta, pero esta vez, se empaquetan los datos en el lado del servidor.

Colocar y descolocar los datos tiene sus complicaciones. El primer problema son los objetos serializados que podrían ser incompatiles entre versiones del JDK. Un objeto serializado tiene un identificador almacenado con el objeto que enlaza el objeto serializado con su versión. Si el cliente RMI y el servidor son incompativles con su ID de serie, podríamos necesitar generar Stubs y Skels compatibles usando la opción -vcompat del compilador rmic.

Otro problema es que no todos los objetos son serializables por defecto. El objeto inicial RegistrationBean está basado en la devolución de un objeto Enumeration que contiene elementos Registration en un Vector. Devolver la lista desde el método remoto, funciona bien, pero cuando intentamos envíar un vector como un parámetro a un objeto remoto, obtendremos una excepción en tiempo de ejecución en la plataforma Java 2.

Afortunadamente, en el API Collections, la plataforma Java ofrece alternativas a la descolocación de objetos anterior. En este ejemplo, un ArrayList del API Collections reemplaza el Vector. Si el API Collections no es una opción, podemos crear una clase envoltura que extienda Serializable y proporcione implementaciones para los métodos readObject y writeObject para convertir el objeto en un stream de bytes.

. La clase RegistrationServer

La clase RegistrationServer extiende java.rmi.server.UnicastRemoteObject e implementa los métodos create, findByPrimaryKey y findLowCreditAccounts declarados en el interface RegistrationHome. El fichero fuente RegistrationServer.java también incluye la implementación del interface remoto Registration como la clase RegistrationImpl. RegistrationImpl también extiende UnicastRemoteObject.

. Exportar un Objeto Remoto

Cualquier objeto que querramos que se accesible remotamente necesita extender el interface java.rmi.server.UnicastRemoteObject o usar el método exportObject de la clase UnicastRemoteObject. Si extendemos UnicastRemoteObject, también obtendremos los métodos equals, toString y hashCode para el objeto exportado.

. Pasar por Valor y por Referencia

Aunque la clase RegistrationImpl no está unida al registro, todavía está referenciada remotamente porque está asociada con los resultados devueltos por RegistrationHome. RegistrationImpl extiende UnicastRemoteObject, sus resultados son pasados por referencia, y sólo una copia del Bean de registro del usuario existente en la Java VM a la vez.

En el caso de reportar resultados como en el método RegistrationServer.findLowCreditAccounts, la clase RegistrationImpl se puede usar una copia del objeto remoto. Si no extendemos la clase UnicastRemoteObject en la definición de la clase RegistrationImpl, se devolverá un nuevo objeto Registration en cada petición. En efecto los valores son pasados pero no la referencia al objeto en el servidor.

. Recolección de Basura Distribuida

Al usar referencias remotas a objetos en el servidor desde fuera del cliente el recolector de basura del servidor introduce algunos problemas potenciales con la memoria. ¿Cómo conoce el servidor cuando se mantiene una referencia a un objeto Registration que no está siendo usado por ningún cliente porque abortó o se cayó la conexión de red?

Para evitar bloqueos de memoria en el servidor desde los clientes, RMI usa un mecanismo de alquiler cuando ofrecen las referencias a los objetos exportados. Cuando se exporta un objeto, la JVM incrementa la cuenta del número de referencias a este objeto y configura el tiempo de expiración, o tiempo de préstamo, por el número de referencias del objeto.

Cuando el alquiler expira, la cuenta de referencias de este objeto se decrementa y si alcanza 0, el objeto es seleccionado para la recolección de basura por la JVM. Hay que configurar el cliente que mantiene un pico de referencia al objeto remoto a que renueve el alquiler si necesita el objeto más alla del tiempo de alquiler. Este pico de referencia es una forma de referirse a un objeto en la memoria sin mantenerlo lejos del recolector de basura.

Este tiempo de alquiler es una propiedad configurable medida en segundos. Si tenemos una red rápida, podríamos acortar el valor por defecto, y crear un gran número de referencias a objetos transitorias.

El siguiente código selecciona el tiempo de alquiler a 2 minutos.

    Property prop = System.getProperties();
    prop.put("java.rmi.dgc.leaseValue", 120000);

Los métodos create y findByPrimaryKey son prácticamente idénticos a las otras versiones del servidor Registration. La principal diferecia es que en el lado del servidor, el registro registration es referenciado como RegistrationImpl, que es la implementación de Registration. En el lado del cliente, se usa Registration en su lugar.

El método findLowCreditAccounts cosntruye un ArrayList de objetos RegistrationImpl serializables y llama al método remoto en la clase SellerBean para pasar el resultado de vuelta. Los resultados on generado por una clase Thread interna porque el método retorna antes de que el resultado esté completo. El objeto SellerBean espera a que sea llamado el método updateAccounts antes de mostrar la página HTML. En un cliente escrito en Java, no sería necesario esperar, podríamos mostrar la actualización en tiempo real.

public class RegistrationServer 
	extends UnicastRemoteObject 
	implements RegistrationHome {

  public registration.RegistrationPK 
    create(String theuser, 
	String password, 
        String emailaddress, 
        String creditcard) 
	  throws registration.CreateException{
    // code to insert database record
  }

  public registration.Registration 
    findByPrimaryKey(registration.RegistrationPK pk) 
	throws registration.FinderException {
      if ((pk == null) || (pk.getUser() == null)) {
            throw new FinderException ();
      }
      return(refresh(pk));
  }

  private Registration refresh(RegistrationPK pk)
	throws FinderException {

    if(pk == null) {
      throw new FinderException ();
    }

    Connection con = null;
    PreparedStatement ps = null;
    try{
      con=getConnection();
      ps=con.prepareStatement("select password, 
	emailaddress, 
	creditcard, 
	balance from registration where theuser = ?");
      ps.setString(1, pk.getUser());
      ps.executeQuery();
      ResultSet rs = ps.getResultSet();
      if(rs.next()) {
        RegistrationImpl reg=null;
        try{
          reg= new RegistrationImpl();
        }catch (RemoteException e) {}
          reg.theuser = pk.getUser();
          reg.password = rs.getString(1);
          reg.emailaddress = rs.getString(2);
          reg.creditcard = rs.getString(3);
          reg.balance = rs.getDouble(4);
          return reg;
      }else{
          throw new FinderException ();
      }
    }catch (SQLException sqe) {
           throw new FinderException();
    }finally {
      try{
        ps.close();
        con.close();
      }catch (Exception ignore) {}
      }
  }

  public void findLowCreditAccounts(
                final ReturnResults client)  
	throws FinderException {
    Runnable bgthread = new Runnable() {
      public void run() {
        Connection con = null;
        ResultSet rs = null;
        PreparedStatement ps = null;
        ArrayList ar = new ArrayList();

        try{
          con=getConnection();
          ps=con.prepareStatement("select theuser, 
	     balance from registration 
	     where balance < ?");
          ps.setDouble(1, 3.00);
          ps.executeQuery();
          rs = ps.getResultSet();
          RegistrationImpl reg=null;
          while (rs.next()) {
            try{
              reg= new RegistrationImpl();
            }catch (RemoteException e) {}
              reg.theuser = rs.getString(1);
              reg.balance = rs.getDouble(2);
              ar.add(reg);
            }
          rs.close();
          client.updateResults(ar);
          }catch (Exception e) {
            System.out.println("findLowCreditAccounts: "+e);
            return;
          }
          finally {
            try{
              if(rs != null) {
                rs.close();
              }
              if(ps != null) {
                ps.close();
                       }
              if(con != null) {
                con.close();
              }
	  }catch (Exception ignore) {}
        }
      } //run
    };
    Thread t = new Thread(bgthread);
    t.start();
  }
}

El método main carga el driver JDBC. Esta versión usa la base de datos Postgres, instala el RMISecurityManager, y contacta con el registro RMI para unir el objeto remoto RegistrationHome al nombre registration2. No necesita unir el interface remoto, Registration porque la clase es cargada cuando es referenciada por RegistrationHome.

Por defecto, el servidor de nombres usa el puerto 1099. Si queremos usar un número de puerto diferente, podemos añadirlo con dos puntos de esta forma: kq6py:4321. Si cambiamos aquí el número de puerto, debemos arrancar el RMI Registry con el mismo número de puerto.

El método main también instala un RMIFailureHandler. Si el servidor falla al crear el socket servidor, el manejador de fallos devuelve true que instruye al servidor RMI para que reintente la operación.

  public static void main(String[] args){
     try {
        new pool.JDCConnectionDriver(
                "postgresql.Driver", 
		"jdbc:postgresql:ejbdemo",
		"postgres", "pass");
        } catch (Exception e){ 
           System.out.println(
             "error in loading JDBC driver");
           System.exit(1);
        }
        try {
           Properties env=System.getProperties();
           env.put("java.rmi.server.codebase", 
             "http://phoenix.eng.sun.com/registration");
           RegistrationServer rs= 
             new RegistrationServer();
           if (System.getSecurityManager() == null )  {
               System.setSecurityManager(
		new RMISecurityManager());
           }
           RMISocketFactory.setFailureHandler(
		new RMIFailureHandlerImpl());

           Naming.rebind("
		//phoenix.eng.sun.com/registration2",rs);
        }catch (Exception e) {
           System.out.println("Exception thrown "+e);
        }
   }
}

class RMIFailureHandlerImpl 
        implements RMIFailureHandler {
   public boolean failure(Exception ex ){
      System.out.println("exception "+ex+" caught");
      return true;
   }
}

. Interface Registration

El interface Registration declara los métodos implementados por RegistrationImpl en el fichero fuente RegistrationServer.java.

package registration;

import java.rmi.*;
import java.util.*;

public interface Registration extends Remote {
   boolean verifyPassword(String password) 
		throws RemoteException;
   String getEmailAddress() throws RemoteException;
   String getUser() throws RemoteException;
   int adjustAccount(double amount) 
         throws RemoteException;
   double getBalance() throws RemoteException;
}

. Interface RegistrationHome

El interface RegistrationHome declara los métodos implementados por la clase RegistrationServer. Estos métodos reflejan el interface Home definido en el ejemplo JavaBeans de Enterprise. El método findLowCreditAccounts toma un interface remoto como su único parámetro.

package registration;

import java.rmi.*;
import java.util.*;

public interface RegistrationHome extends Remote {
  RegistrationPK create(String theuser, 
	String password, 
	String emailaddress, 
	String creditcard) 
		throws CreateException, 
		RemoteException;

  Registration findByPrimaryKey(RegistrationPK theuser) 
		throws FinderException, RemoteException;

  public void findLowCreditAccounts(ReturnResults rr) 
		throws FinderException, RemoteException;
}

. Interface ReturnResults

El interface ReturnResults declara el método implementado por la clase SellerBean. El método updateResults es llamado desde RegistrationServer.

package registration;

import java.rmi.*;
import java.util.*;

public interface ReturnResults extends Remote {
    public void updateResults(ArrayList results) 
	throws FinderException, RemoteException;
}

. La Clase SellerBean

La clase SellerBean incluye la implementación del método callback y llama al objeto RegistrationServer usando RMI. El método updateAccounts se hace accesible mediante una llamada a UnicastRemoteObject.exportObject(this);. El método auditAccounts espera un objeto method Boolean.

El método updateAccounts envía una notificación a todos los métodos que esperan el objeto Boolean cuando ha sido llamado desde el servidor y recibe los resultados.

package seller;

import java.rmi.RemoteException;
import java.rmi.*;
import javax.ejb.*;
import java.util.*;
import java.text.NumberFormat;
import java.io.Serializable;
import javax.naming.*;
import auction.*;
import registration.*;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;


public class SellerBean 
	implements SessionBean, ReturnResults {

    protected SessionContext ctx;
    javax.naming.Context ectx;
    Hashtable env = new Hashtable();
    AuctionServlet callee=null;
    Boolean ready=new Boolean("false");
    ArrayList returned;

    public int insertItem(String seller, 
	String password, 
	String description, 
	int auctiondays, 
	double startprice, 
	String summary) 
		throws RemoteException { 

      try{
        RegistrationHome regRef = (
		RegistrationHome)Naming.lookup(
		"//phoenix.eng.sun.com/registration2");
        RegistrationPK rpk= new RegistrationPK();
        rpk.setUser(seller);
        Registration newseller = (
	      Registration)regRef.findByPrimaryKey(rpk);
	if((newseller == null) || 
              (!newseller.verifyPassword(password))) {
            return(Auction.INVALjdcbook_USER);	
  	}

        AuctionItemHome home = (
		AuctionItemHome) ectx.lookup(
		                   "auctionitems");
        AuctionItem ai= home.create(seller, 
		description, 
		auctiondays, 
		startprice, 
		summary);
	if(ai == null) {
          return Auction.INVALjdcbook_ITEM;
	}else{
          return(ai.getId()); 
	}
        }catch(Exception e){ 
          System.out.println("insert problem="+e);
          return Auction.INVALjdcbook_ITEM;
        }
    }

    public void updateResults(java.util.ArrayList ar) 
		throws RemoteException {
       returned=ar; 
       synchronized(ready) {
          ready.notifyAll();
       }
    }

    public ArrayList auditAccounts() {
        this.callee=callee;
        try {
           RegistrationHome regRef = (
		RegistrationHome)Naming.lookup(
		"//phoenix.eng.sun.com/registration2");
           regRef.findLowCreditAccounts(this);
           synchronized(ready) {
              try {
                 ready.wait();
              } catch (InterruptedException e){}
           }
        return (returned);
       }catch (Exception e) {
          System.out.println("error in creditAudit "+e);
       }
      return null;
    }

    public void ejbCreate() 
		throws javax.ejb.CreateException, 
		RemoteException {
      env.put(
        javax.naming.Context.INITIAL_CONTEXT_FACTORY, 
        "weblogic.jndi.TengahInitialContextFactory");
      try{
        ectx = new InitialContext(env);
      } catch (NamingException e) {
        System.out.println(
          "problem contacting EJB server");
        throw new javax.ejb.CreateException();
      }
        Properties env=System.getProperties();
        env.put("java.rmi.server.codebase", 
	  "http://phoenix.eng.sun.com/registration");
        env.put("java.security.policy","java.policy");
        UnicastRemoteObject.exportObject(this);
    }

    public void setSessionContext(SessionContext ctx) 
		throws RemoteException {
      this.ctx = ctx;
    }

    public void unsetSessionContext() 
		throws RemoteException {   
        ctx = null;  
    } 

    public void ejbRemove() {}
    public void ejbActivate() throws RemoteException {
	System.out.println("activating seller bean");
    }
    public void ejbPassivate() throws RemoteException { 
	System.out.println("passivating seller bean");
    }
}

. Common Object Request Broker Architecture (CORBA)

Las implementaciones de RMI y de JavaBeans Enterprise de la aplicación de subasta usan el lenguaje Java para implementar los distintos servicios de la subasta. Sin embargo, podríamos necesitar intergrarlo con aplicaciones escritas en C, C++ u otros lenguajes y ejecutarlo en un millar de sistemas operativos y máquinas distintas.

Una forma de integración con otras aplicciones es transmitir datos en un formato común como caracteres de 8 bits sobre sockets TCP/IP. La desventaja es tener que gastar mucho tiempo en derivar un mensaje de protocolo y mapeado de varias estructuras de datos hacia y desde el formato de transmisión común para que los datos puedan ser enviados y recibidos sobre la conexión TCP/IP.

Aquí es donde pueden ayudar el "Common Object Request Broker Architecture" (CORBA) y su "Interface Definition Language" (IDL). IDL proporciona un formato común para representar un objeto que puede ser distribuido a otras aplicaciones. Las otras aplicaciones podrían incluso no entender de objetos, pero mientras puedan proporcionar un mapeado entre el formato común IDL y sus propia representación de datos, la aplicación podrá compartir los datos.

Este capítulo describe el esquema de mapeo de IDL a lenguaje Java, y cómo reemplazar el original RegistrationBean basado en contenedor controlador por su equivalente servidor CORBA. Los programas SellerBean.java y AuctionServlet.java también se modifican para interoperar con el programa CORBA RegistrationServer.

. Esquema de Mapeo IDL

Muchos lenguajes de programación proporcionan un mapeo entre sus tipos de datos y el formato común denominado IDL, y el lenguaje Java no es una excepción. El lenguaje Java puede enviar objetos definidos por IDL a otras aplicaciones distribuidas CORBA y recibir objetos definidos mediante IDL desde otras aplicaciones distribuidas CORBA.

Esta sección descrbie el esquema de mapedo de Java a IDL y, cuando sea necesario, presenta problemas que debemos tener en consideración.

. Referencia Rápida

Aquí tenemos una tabla de referencia rápida de los tipos de datos del lenguaje Java y los de IDL CORBA, y las excepciones de tiempo de ejecución que se pueden lanzar cuando la conversión falla. Los tipos de datos de esta tabla que necesitan explicación se cubren más abajo.

Tipo de Dato Java Formato IDL Exception
byte octet

 

boolean boolean

 

char char DATA_CONVERSION
char wchar

 

double double

 

float float

 

int long

 

int unsigned long

 

long long long

 

long unsigned long long

 

short short

 

short unsigned short

 

java.lang.String string DATA_CONVERSION
java.lang.String wstring MARSHAL

Valores sin Signo: Los tipos de datos Java: byte, short, int, y long están representados por entereros de complemento a dos de 8, 16, 32 y 64 bits. Esto significa que un valor short Java representa un rango desde -215 hasta 215 - 1 ó desde -32768 hasta 32767 inclusives. El tipo con signo equivalente IDL para un short, short, es igual en el rango, pero el tipo short IDL sin signo usa el rango desde 0 hata 2 -15 ó desde 0 hasta 65535.

Esto significa que en el caso de short, si un valor short sin signo mayor de 32767 es pasado a un programa escrito en Java, el valor short será representado como un número negativo. Esto puede causar confusión en los límites de test para valores mayores que 32767 o menores que 0.

Tipos char IDL: El lenguaje Java usa un unicode de 16 Bits, pero los tipos char y string de IDL son carcateres de 8 bits. Podemos mapear un char Java a un char IDL de 8 bits para transmitir caracteres multi-byte si usamos un array para hacerlo. Sin embargo, el tipo de caracter ancho de IDL wchar está especialmente diseñado para lenguajes con caracteres multi-bytes y aloja el número fijo de bytes que sea necesario para contener el conjunto del lenguaje para cada una de las letras.

Cuando se mapea desde el tipo char de Java al tipo char de IDL, se puede lanzar la excepción DATA_CONVERSION si el caracter no entra en los 8 bits.

Tipos string IDL: El tipo string IDL puede ser lanzado como una secuencia de tipos char IDL, también lanza la excepción DATA_CONVERSION. El tipo wstring IDL es equivalente a una secuencua de wchars terminada por un wchar NULL.

Un tipo string y un tipo wstring de IDL pueden tener un tamaño fijo o sin máximo definido. Si intentamos mapear un java.lang.String a un string IDL de tamaño fijo y el java.lang.String es demasidado largo, se lanzará una excepción MARSHAL.

. Configurar el Mapeo IDL

El mapeo del lenguaje Java a IDL se sitúa en un fichero con extensión .idl. El fichero es compilado para que pueda ser accedido por los programas CORBA que necesitan enviar y recibir datos. Esta sección explica cómo construir los mapeos para las sentencias de paquete y los tipos de datos Java. La siguiente sección en Implementación CORBA de RegistrationServer describe cómo usar esta información para configurar el fichero de mapeo IDL para el servidor Registration CORBA.

Paquetes e Interfaces Java: Las sentencias de paquete Java son equivalentes al tipo module de IDL. Este tipo puede ser anidado, lo que resulta en que las clases Java generadas se crean en subdirectorios anidados.

Por ejemplo, si un programa CORBA contiene esta sentencia de paquete:

  package registration;

el fichero de mapeo debería tener este mapeo a módulo IDL para ella:

  module registration {
  };

Si un programa CORBA contiene una herencia de paquete como esta:

  package registration.corba;

su mapeo IDL de módulo será este:

  module registration {
    module corba {
    };
  };

Las clases distribuidas están definidas como interfaces Java y se mapean al tipo interface de IDL. IDL no define accesos como public o private que podríamos encontrar en el lenguaje Java, pero permite descender desde otros interfaces.

Este ejemplo añade el interface Java Registration a un registration module IDL.

  module registration {
     interface Registration {
     };
  }

Este ejemplo añade el interface Java Registration a un registration module IDL, e indica que el interface Registration desciende del interface User.

  module registration {
     interface Registration: User {
     };
  }

Métodos Java: Los métodos Java se mapean a operaciones IDL. Las operaciones IDL son similares a los métodos Java excepto en que no hay el concepto de control de acceso. También tenemos que ayudar al compilador IDL especificando qué parámetros son de entrada in, de entrada/salida inout o de salida out, definidos de esta forma:

  • in - El parámetro se pasa dentro del método pero no se modifica.
  • inout - El parámetro se pasa al método y se podría devolver modificado.
  • out - El parámetro se podría devolver modificado.

Este mapeo IDL incluye los métodos de los interfaces Registration y RegistrationHome a operaciones IDL usando un tipo módulo IDL.

module registration {

  interface Registration {
    boolean verifyPassword(in string password);
    string getEmailAddress();
    string getUser();
    long adjustAccount(in double amount);
    double getBalance();
  };

  interface RegistrationHome {
    Registration findByPrimaryKey(
		   in RegistrationPK theuser) 
		   raises (FinderException);
  }
}

Arrays Java: Los Arrays Java son mapeados a los tipos array o sequence IDL usando una definición de tipo.

Este ejemplo mapea el array Java double balances[10] a un tipo array IDL del mismo tamaño.

typedef double balances[10];

Estos ejemplos mapean el array Java double balances[10] a un tipo sequence IDL. El primer typedef sequence es un ejemplo de secuencia sin límite, y el segundo tiene el mismo tamaño que el array.

typedef sequence<double> balances;
typedef sequence<double,10> balances;

Excepciones Java: Las excepciones Java son mapeadas a excepciones IDL. Las operaciones usan exceptions IDL especificándolas como del tipo raises.

Este ejemplo mapea la CreateException desde la aplicación subastas al tipo exception IDL, y le añade el tipo raises a la operación. Las excepciones IDL siguen las sintaxis C++, por eso en lugar de lanzar una excepción (como se haría en lenguaje Java), la operación alcanza (raise) una excepción.

exception CreateException {
};

interface RegistrationHome {
  RegistrationPK create(
	           in string theuser, 
	           in string password, 
	           in string emailaddress, 
	           in string creditcard) 
	           raises (CreateException);
}

. Otros Tipos IDL

Estos otros tipos básicos IDL no tienen un equivalente exacto en el lenguaje Java. Muchos de estos deberían sernos familiares si hemos usado C ó C++. El lenguaje Java proporciona mapeo para estos tipos porque los programas escritos en Java pueden recibir datos desde programas escritos en C ó C++.

  • attribute IDL
  • enum IDL
  • struct IDL
  • union IDL
  • Any IDL
  • Principal IDL
  • Object IDL

atributo IDL: El tipo attribute IDL es similiar a los métodos get y set usados para acceder a los campos en el software de JavaBeans.

En el caso de un valor declarado como un atributo IDL, el compilador IDL genera dos métodos con el mismo nombre que el atributo IDL. Un método devuelve el campo y otro lo selecciona. Por ejemplo, este tipo attribute:

interface RegistrationPK {
  attribute string theuser;
};

define estos métodos:

//return user
  String theuser(); 
//set user
  void theuser(String arg); 

enum IDL: El lenguaje Java tiene una clase Enumeration para representar una colección de datos. El tipo enum IDL es diferente porque es declarado como un tipo de dato y no una colección de datos.

El tipo enum IDL es una lista de valores que pueden se referenciados por un nombre en vez de por su posición en la lista. En el ejemplo, podemos ver que referirnos al código de estado de un enum IDL por un nombre es mucho más legible que hacerlo por su número. Esta línea mapea los valores static final int de la clase final LoginError. Podemos referirnos a estos valores como lo haríamos con un campo estático:LoginError.INVALjdcbook_USER.

enum LoginError {
  INVALjdcbook_USER, WRONG_PASSWORD, TIMEOUT};

Aquí hay una versión del tipo enum que incluye un subrayado anterior para que pueda ser usado en sentencias switch:

switch (problem) {
  case LoginError._INVALjdcbook_USER:
    System.out.println("please login again");
    break;
}

struct IDL: Un tipo struct IDL puede ser comparado con una clase Java que sólo tiene campos, que es cómo lo mapea el compilador IDL.

Este ejemplo declara una struct IDL. Observamos que los tipos IDL pueden referenciar otros tipos IDL. En este ejemplo LoginError viene del tipo enum declarado arriba.

struct ErrorHandler {
  LoginError errortype;
  short retries;
};

union IDL: Una union IDL puede representar un tipo de una lista de tipos definidos para esa unión. La union mapea a una clase Java del mismo nombre con un método discriminator usado para determinar el tipo de esa unión.

Este ejemplo mapea la unión GlobalErrors a una clase Java con el nombre GlobalErrors. Se podría añadir un case por defecto case: DEFAULT para manejar cualquier elemento que podría estar en el tipo LoginErrors enum, y no está especificado con una sentencia case aquí.

  union GlobalErrors switch (LoginErrors) {
     case: INVALjdcbook_USER: string message;
     case: WRONG_PASSWORD: long attempts;
     case: TIMEOUT: long timeout;
  };

En un programa escrito en lenguaje Java, la clase unión GlobalErrors se crea de esta forma:

  GlobalErrors ge = new GlobalErrors();
  ge.message("please login again");

El valor INVALjdcbook_USER se recupera de esta forma:

  switch (ge.discriminator().value()) {
    case: LoginError._INVALjdcbook_USER:
      System.out.println(ge.message);
      break;
  }

Tipo Any: si no sabemos que tipo está siento pasado o devuelto desde una operación, podemos usar el tipo Any, que representa cualquier tipo IDL. La siguiente operación retorna y pasa un tipo desconocido:

  interface RegistrationHome {
    Any customSearch(Any searchField, out count);
  };

Para crear un tipo Any, se pide el tipo al "Object Request Broker" (ORB). Para seleccionar un valor de un tipo Any, usamos un método insert_<type>. Para recuperar un valor, usamos el método extract_<type>.

Este ejemplo pide un objeto del tipo Any, y usa el método insert_type para seleccionar un valor.

  Any sfield = orb.create_any();
  sfield.insert_long(34);

El tipo Any tiene un valor TypeCode asignado que puede consultarse usando type().kind().value() sobre el objeto. El siguiente ejemplo muestra una prueba del TypeCode double. Este ejemplo incluye una referencia al TypeCode IDL encontrado que contiene el objeto Any. El TypeCode se usa para todos los objetos. Podemos analizar el tipo de un objeto CORBA usando los métodos _type() o type().

public Any customSearch(Any searchField, IntHolder count){
  if(searchField.type().kind().value() == TCKind._tk_double){
// return number of balances greater than supplied amount 
    double findBalance=searchField.extract_double();

Principal: El tipo Principal identifica al propietario de un objeto CORBA, por ejemplo, un nombre de usuario. El valor puede consultarse desde el campo request_principal de la clase RequestHeader para hacer la identificación.

Object: El tipo Object es un objeto CORBA. Si necesitamos enviar objetos Java, tenemos que traducirlos a un tipo IDL o usar un mecanismo para serializarlos cuando sean transferidos.

. CORBA en la Aplicación de Subasta

El RegistrationBean controlado por contenedor de la aplicación subasta es totalmente reemplazado con un RegistrationServer solitario CORBA que implementa el servicio de registro. El RegistrationServer CORBA está construido creando y compilando ficheros de mapeo IDL para que los programas clientes se puedan comunicar con el servidor de registros.

Los ficheros SellerBean.java y AuctionServlet.java se han actualizado para que busquen el servidor de registro CORBA.

. Implementación del RegistrationServer CORBA

Esta sección describe el fichero Registration.idl, que mapea los interfaces remotos RegistrationHome y Registration desde la aplicación de subastas de JavaBeans de Enterprise a sus equivalentes IDL y muestra como compilar el fichero Registration.idl en las clases del servidor de registos CORBA.

El servidor de registros CORBA implementa los métodos create y findByPrimaryKey desdel el fichero RegistrationBean.java original, y lo amplía con los dos métodos siguientes para ilustrar las retrollamadas CORBA, y como usar el tipo Any.

  • findLowCreditAccounts(in ReturnResults rr), que usa una callback para devolver una lista de cuentas con bajo saldo.
  • any customSearch(in any searchfield, out long count), que devuelve un resultado de búsqueda diferente dependiendo del tipo de campo enviado.

. Fichero de Mapeos IDL

Aquí está el fichero Registration.idl que mapea los tipos de datos y métodos usados en los programas RegistrationHome y Registration a sus equivalentes IDL.

module registration {

interface Registration {
   boolean verifyPassword(in string password);
   string getEmailAddress();
   string getUser();
   long adjustAccount(in double amount);
   double getBalance();
};

interface RegistrationPK {
   attribute string theuser;
};

enum LoginError {INVALIDUSER, WRONGPASSWORD, TIMEOUT};

exception CreateException {
};

exception FinderException {
};

typedef sequence<Registration> IDLArrayList;

interface ReturnResults  {
  void updateResults(in IDLArrayList results) 
	raises (FinderException);
};

interface RegistrationHome {
  RegistrationPK create(in string theuser, 
			in string password, 
                   	in string emailaddress, 
			in string creditcard) 
                           raises (CreateException);

  Registration findByPrimaryKey(
                 in RegistrationPK theuser) 
		 raises (FinderException);
  void findLowCreditAccounts(in ReturnResults rr) 
		 raises (FinderException);
  any customSearch(in any searchfield, out long count);
};
};

. Compilar el Fichero de Mapeos IDL

El fichero IDL tiene que ser convertido en clases Java que puedan ser usadas en una red distribuida CORBA. La plataforma Java 2 compila los ficheros .idl usando el programa idltojava. Este programa será reemplazado eventualmente con el comando idlj.

Los argumentos -fno-cpp indican que no hay compilador C++ instalado.

  idltojava -fno-cpp Registration.idl

Otros compiladores Java IDL también deberían funcionar, por ejemplo, jidl de ORBacus puede generar clases que pueden ser usadas por el ORB de Java 2.

. Stubs y Skeletons

Corba y RMI son similares en que la compilación genera un fichero stub para el cliente y un fichero skeleton para el servidor. El stub (o proxy), y el skeleton (o sirviente) se usan para envolver o desenvolver datos entre el cliente y el servidor. El skeleton (o sirviente) está implementado mediante el servidor. En este ejemplo, el interface RegistrationHome genera una clase _RegistrationHomeImplBase (la clase skeleton o sirviente) que extiende la clase RegistrationServer generada.

Cuando se solicita un objeto CORBA remoto o se llama a un método remoto, la llamada del cliente pasa a través de la clase stub antes de alcanzar el servidor. Este clase proxy invoca la petición CORBA para el programa cliente. El siguiente ejemplo es el código generado automáticamente por la clase RegistrationHomeStub.java.

  org.omg.CORBA.Request r = _request("create");
  r.set_return_type(
        registration.RegistrationPKHelper.type());
  org.omg.CORBA.Any _theuser = r.add_in_arg();

. Object Request Broker

El centro de una red distribuida CORBA es el "Object Request Broker" o ORB. El ORB se encarga de empaquetar y desempaquetar los objetos entre el cliente y el servidor. Otros servicios como Servicios de Nombres y Servicios de Eventos funcionan con el ORB.

La plataforma Java 2 incluye un ORB en la distribución llamado el IDL ORB. Este ORB es diferente de otros muchos ORBs porque no incluye un distintivo de "Basic Object Adapter" (BOA) o "Portable Object Adapter" (POA).

Una adaptador de objetos maneja la creacción y ciclo de vida de los objetos en un espacio distribuido CORBA. Esto puede ser comparado con el contenedor del servidor de JavaBeans Enterprise que maneja el ciclo de vida de los beans de entidad y de sesión.

Los programas AuctionServlet y SellerBean crean e inicializan un ORB de Java 2 de esta forma:

  ORB orb = ORB.init(args, null);

En el programa RegistrationServer, el objeto servidor puede ser distribuido en unión con el ORB usando el método connect:

  RegistrationServer rs = new RegistrationServer();
  orb.connect(rs);

Un objeto conectado a un ORB puede ser eleminado con el método disconnect:

  orb.disconnect(rs);

Una vez conectado a un objeto servidor CORBA, el ORB Java2 mantiene vivo el servidor y espera peticiones del cliente para el servidor CORBA.

  java.lang.Object sync = new java.lang.Object();
  synchronized(sync) {
    sync.wait();
  }

. Poner Disponible el Servidor CORBA

Aunque este objeto está ahora siendo mapeado por el ORB, los clientes todavía no tienen el mecanismo para encontrar el objeto remoto. Esto puede resolverse uniendo el objeto servidor CORBA a un servicio de nombres.

El servicio de nombres Java 2 llamado tnameserv, por defecto usa el puerto 900; sin embargo, este valor puede modificarse seleccionado el argumento -ORBInitialPort portnumber cuando se arranca tnameserv o seleccionando la propiedad org.omg.CORBA.ORBInitialPort cuando arrancamos los procesos cliente y servidor.

Las siguientes secciones describen el método main de la clase RegistrationServer.

  java.util.Properties props=System.getProperties();
  props.put("org.omg.CORBA.ORBInitialPort", "1050");
  System.setProperties(props);
  ORB orb = ORB.init(args, props);

Las siguientes líneas muestran que la referencia inicial de nombres es inicializada por la petición del servicio llamado NameService. El NamingContext es recuperado y el nombre construido y unido al servicio de nombres como elementos NameComponent. El nombre de este ejemplo tiene una raíz llamada auction que es este objeto que se está uniendo como RegistrationBean desde la raíz auction. El nombre podría ser comparado por una clase mediante el nombre de auction.RegistrationBean.

  org.omg.CORBA.Object nameServiceObj = 
	orb.resolve_initial_references("NameService") ;
  NamingContext nctx = 
	NamingContextHelper.narrow(nameServiceObj);
  NameComponent[] fullname = new NameComponent[2];
  fullname[0] = new NameComponent("auction", "");
  fullname[1] = new NameComponent(
                      "RegistrationBean", "");

  NameComponent[] tempComponent = new NameComponent[1];
  for(int i=0; i < fullname.length-1; i++ ) {
      tempComponent[0]= fullname[i];
      try{
       nctx=nctx.bind_new_context(tempComponent);
      }catch (Exception e){ 
        System.out.println("bind new"+e);}
  }
  tempComponent[0]=fullname[fullname.length-1];
  try{
   nctx.rebind(tempComponent, rs);
  }catch (Exception e){
   System.out.println("rebind"+e);
  }

. Conectar un nuevo ORB

El ORB IDL de Java 2 realmente no incluye ninguno de los servicios disponibles en muchos otros ORBs comerciales como los servicios de seguridad o eventos (notificación). Podemos usar otro ORB en el runtime de Java 2 configurando dos propiedades e incluyendo cualquier codigo de objeto adaptador que sea necesario.

Usar un nuevo ORB en el servidor de registros requiere que las propiedades org.omg.CORBA.ORBClass y org.omg.CORBA.ORBSingletonClass apunten a las clases ORB apropiadas. En este ejemplo se usa el ORB ORBacus en lugar del ORB IDL de Java 2. Para usar otro ORB, el código de abajo debería conectarse dentro del método RegistrationServer.main.

En el código de ejemplo, se usa un ORB SingletonClass. Este ORB no es un ORB completo, y su uso primario es como factoría para TypeCodes. La llamada a ORB.init() en la última línea crea el ORB Singleton.

  Properties props= System.getProperties();
  props.put("org.omg.CORBA.ORBClass", 
            "com.ooc.CORBA.ORB");
  props.put("org.omg.CORBA.ORBSingletonClass",
	"com.ooc.CORBA.ORBSingleton");
  System.setProperties(props);
  ORB orb = ORB.init(args, props) ;

En el IDL de Java 2, no hay un objeto adaptador distinto. Como se muestra en el segmento de código inferior, usar el "Basic Object Adapter" desde ORBacus requiere un conversión explícita al ORB ORBacus, El "Broker Object Architecture" (BOA) es notificado de que el objeto ya está distribuido llamando al método impl_is_ready(null).

  BOA boa = ((com.ooc.CORBA.ORB)orb).BOA_init(
                                       args, props);
 ...
  boa.impl_is_ready(null);

Aunque los dos ORBs ORBSingletonClass y ORBClass construyen el nombre del objeto usando NameComponent, tenemos que usar un diferente servicio de nombres ORBacus. El servicio CosNaming.Server se arranca de la siguiente forma, donde el parámetro -OAhost es opcional:

  java com.ooc.CosNaming.Server -OAhost localhost -OAport 1060

Una vez arrancado el servicio de nombres, los programas cliente y servidor pueden encontrar el servicio de nombres usando el protocolo IIOP hacia el host y el puerto nombrados cuando se arrancó el servicio de nombrado:

  java registration.RegistrationServer 
	-ORBservice NameService 
	iiop://localhost:1060/DefaultNamingContext

. Acceso al Servicio de Nombres por los Clientes CORBA

Los cliente CORBA acceden al servicio de nombres de una forma similar a como lo hace el servidor, excepto que en lugar de unir un nombre, el cliente resuelve el nombre construido desde el NameComponents.

Las clases AuctionServlet y SellerBean usan el siguiente código para buscar el servidor CORBA:

  NameComponent[] fullname = new NameComponent[2];
  fullname[0] = new NameComponent("auction", "");
  fullname[1] = new NameComponent(
                      "RegistrationBean", "");

  RegistrationHome regRef = 
	RegistrationHomeHelper.narrow(
	                  nctx.resolve(fullname));

En el caso del ORB ORBacus, los clientes también necesitan un "Basic Object Adapter" si se usan retrollamadas en el método SellerBean.auditAccounts. El contexto de nombrado también se configura de forma diferente para el servidor ORBacus arrancado anteriormente:

  Object obj = 
	((com.ooc.CORBA.ORB)orb).get_inet_object (
		"localhost", 
		1060, 
		"DefaultNamingContext"); 
  NamingContext nctx = NamingContextHelper.narrow(obj);

. Clases Helper y Holder

Las referencias a objetos remotos en CORBA usan una clase Helper para recuperar un valor desde ese objeto. Un método usado comunmente es el método Helper, que asegura que el objeto está encastado correctamente.

Las clases Holder contienen valores devueltos cuando se usan parámetros inout o out en un método. El llamador primero ejemplariza la clase Holder apropiada para ese tipo y recupera el valor desde la clase cuando la llamada retorna. En el siguiente ejemplo, el valor del contador customSearch se configura y recupera después de que se haya llamado a customSearch. En el lado del servidor el valor del contador se selecciona llamando a count.value=newvalue.

  IntHolder count= new IntHolder();
  sfield=regRef.customSearch(sfield,count);
  System.out.println("count now set to "+count.value);

. Recolección de Basura

Al contrario que RMI, CORBA no tiene un mecanismo de recolección de basura distribuido. Las referencias a un objeto son locales al proxy del cliente y al sirviente del servidor. Esto significa que cada Máquina Virtual Java (JVM) es libre de reclamar un objeto y recoger la basura si no tiene más referencias sobre él. Si un objeto no es necesario en el servidor, necesitamos llamar a orb.disconnect(object) para permitir que el objeto sea recolectado.

. Retrollamadas (Callbacks) CORBA

El nuevo método findLowCreditAccounts es llamado desde el AuctionServlet usando la URL http://localhost:7001/AuctionServlet?action=auditAccounts.

El método AuctionServlet.auditAccounts llama al método SellerBean.auditAccounts, que devuelve un ArrayList de registros de Registration.

//AuctionServlet.java
  private void auditAccounts(ServletOutputStream out,
    HttpServletRequest request) throws IOException{

//   ...

   SellerHome home = (SellerHome) ctx.lookup("seller");
   Seller si= home.create();

   if(si != null) {
     ArrayList ar=si.auditAccounts();
     for(Iterator i=ar.iterator(); i.hasNext();) {
       Registration user=(Registration)(i.next());
       addLine("<TD>"+user.getUser() +
	 "<TD><TD>"+user.getBalance()+
	 "<TD><TR>", out);
     }
          addLine("<TABLE>", out);
    }

El objeto SellerBean llama al método CORBA RegistrationHome.findLowCreditAccounts implementado en el fichero RegistrationServer.java, y se pasa una referencia a sí mismo. La referencia es pasada siempre que la clase SellerBean implemente el interface ReturnResults declarado en el Registration.idl.

//SellerBean.java
  public ArrayList auditAccounts() {
    try{
      NameComponent[] fullname = new NameComponent[2];
      fullname[0] = new NameComponent("auction", "");
      fullname[1] = new NameComponent(
                          "RegistrationBean", "");

      RegistrationHome regRef = 
	  RegistrationHomeHelper.narrow(
	                     nctx.resolve(fullname));
        regRef.findLowCreditAccounts(this);
        synchronized(ready) {
          try{
           ready.wait();
          }catch (InterruptedException e){}
        }
        return (returned);
      }catch (Exception e) {
       System.out.println("error in auditAccounts "+e);
      }
      return null;
  }

El método RegistrationServer.findLowCreditAccounts recupera los registros de usuario desde la tabla Registration de la base de datos que tengan un valor de crédito menor de tres. Entonces devuelve la lista de registros Registration en un ArrayList llamando al método SellerBean.updateResults que tiene una referencia a ella.

//RegistrationServer.java
  public void findLowCreditAccounts(
                final ReturnResults client)  
		throws Finder Exception {
    Runnable bgthread = new Runnable() {
      public void run() {
        Connection con = null;
        ResultSet rs = null;
        PreparedStatement ps = null;
        ArrayList ar = new ArrayList();

        try{
          con=getConnection();
          ps=con.prepareStatement(
		"select theuser, 
		balance from registration 
		where balance < ?");
          ps.setDouble(1, 3.00);
          ps.executeQuery();
          rs = ps.getResultSet();
          RegistrationImpl reg=null;
          while (rs.next()) {
            try{
             reg= new RegistrationImpl();
            }catch (Exception e) {
             System.out.println("creating reg"+e);
            }
            reg.theuser = rs.getString(1);
            reg.balance = rs.getDouble(2);
            ar.add(reg);
          }
          rs.close();

          RegistrationImpl[] regarray = 
		(RegistrationImpl [])ar.toArray(
		new RegistrationImpl[0]);
          client.updateResults(regarray);
        }catch (Exception e) {
         System.out.println(
                      "findLowCreditAccounts: "+e);
          return;
        }
        finally {
        try{
            if(rs != null) {
              rs.close();
            }
            if(ps != null) {
             ps.close();
            }
            if(con != null) {
              con.close();
            }
          }catch (Exception ignore) {}
        }
      }//run
    };
  Thread t = new Thread(bgthread);
  t.start();
  }

El método SellerBean.updateResults actualiza el ArrayList global de registros de Registration devuelto por el objeto RegistrationServer y notifica al método SellerBean/auditAccounts que puede devolver este ArrayList de registros Registration al AuctionServlet.

  public void updateResults(Registration[] ar) 
	throws registration.FinderException {
    if(ar == null) {
          throw new registration.FinderException();
    }
    try{
      for(int i=0; i< ar.length; i++) {
        returned.add(ar[i]);
      }
    }catch (Exception e) {
     System.out.println("updateResults="+e);
     throw new registration.FinderException();
    }
     synchronized(ready) {
     ready.notifyAll();
    }
  }

. Usar el Tipo Any

El método RegistrationServer.customSearch usa el tipo Any de IDL para pasar y devolver resultados. El customSearch es llamado por el AuctionServlet de esta forma:

  http://localhost.eng.sun.com:7001/
     AuctionServlet?action=customSearch&searchfield=2

El parámetro searchfield puede ser seleccionado como un número o un string. El método AuctionServlet.customFind pasa el campo de búsqueda directamente al método SellerBean.customFind que recupera un String que luego es mostrado al usuario:

  private void customSearch(ServletOutputStream out,
               HttpServletRequest request) 
               throws IOException{

    String text = "Custom Search";
    String searchField=request.getParameter(
                                 "searchfield");

    setTitle(out, "Custom Search");
    if(searchField == null ) {
      addLine("Error: SearchField was empty", out);
      out.flush();
      return;
    }
    try{
      addLine("<BR>"+text, out);
      SellerHome home = (SellerHome) 
                           ctx.lookup("seller");
      Seller si= home.create();
      if(si != null) {
        String displayMessage=si.customFind(
                                   searchField);
        if(displayMessage != null ) {
          addLine(displayMessage+"<BR>", out);
        }
      }
    }catch (Exception e) {
     addLine("AuctionServlet customFind error",out);
     System.out.println("AuctionServlet " + 
                        "<customFind>:"+e);
    }
    out.flush();
  }

El método SellerBean.customFind llama al objeto RegistrationHome implementado en la clase RegistrationServer.java, y dependiendo de si el searchField puede ser convertido a un double o a un string, inserta este valor dentro de un objeto del tipo Any. El objeto Any se crea mediante una llamada al ORB, orb.create_any();

El método customFind también usa un parámetro out, count, del tipo int que devuelve el número de registros encontrados. El valor de count se recupera usando count.value cuando la llamada retorna:

//SellerBean.java
  public String customFind(String searchField) 
	   throws javax.ejb.FinderException, 
	   RemoteException{

  int total=-1;
  IntHolder count= new IntHolder();

  try{
      NameComponent[] fullname = new NameComponent[2];
      fullname[0] = new NameComponent("auction", "");
      fullname[1] = new NameComponent(
                          "RegistrationBean", "");

      RegistrationHome regRef = 
        RegistrationHomeHelper.narrow(
                            nctx.resolve(fullname));
      if(regRef == null ) {
        System.out.println(
                     "cannot contact RegistrationHome");
        throw new javax.ejb.FinderException();
      }
      Any sfield=orb.create_any();
      Double balance;
      try{
        balance=Double.valueOf(searchField);
        try {
            sfield.insert_double(balance.doubleValue());
        }catch (Exception e) {
         return("Problem with search value"+balance);
        }
       sfield=regRef.customSearch(sfield,count);
       if(sfield != null ) {
         total=sfield.extract_long();
       }
       return(total+" 
	accounts are below optimal level from" +
	count.value+" records");
     }catch (NumberFormatException e) {
      sfield.insert_string(searchField);
      Registration reg;
      if((reg=RegistrationHelper.extract(
	  	regRef.customSearch(
	  	         sfield,count))) 
	  	         != null ) {
        return("Found user "+reg.getUser() +" 
		who has email address "+
		reg.getEmailAddress());
      }else {
       return("No users found who have email address " +
		searchField);
      }
     }
    }catch(Exception e){
        System.out.println("customFind problem="+e);
        throw new javax.ejb.FinderException();
    }
  }

El valor devuelto desde la llamada a customFind se extrae dentro de un objeto del tipo Any y se construye un String con la salida mostrada al usuario. Para los tipos sencillos se puede usar el método extract_<type> de Any. Sin embargo, para el tipo Registration, se usa la clase RegistrationHelper.

  Registration reg =
    RegistrationHelper.extract(
                 regRef.customSearch(sfield,count))

El método RegistrationServer.customSearch determina el tipo del objeto que está siendo pasado en el parámetro searchField chequeando el .type().kind().value() del objeto Any.

 if(searchField.type().kind().value() == 
                         TCKind._tk_double)

Finalmente, como el método customSearch devuelve un objeto del tipo Any, se requiere una llamada a orb.create_any(). Para tipos sencillos como double, se usa el método insert_<type>. Para el tipo Registration , se usa la clase RegistrationHelper: RegistrationHelper.insert(returnResults, regarray[0]).

//RegistrationServer.java
  public Any customSearch(Any searchField, 
                          IntHolder count){
    Any returnResults= orb.create_any();

    int tmpcount=count.value;
    if(searchField.type().kind().value() == 
                          TCKind._tk_double){
// return number of balances greater 
// than supplied amount
      double findBalance=searchField.extract_double();
      Connection con = null;
      ResultSet rs = null;
      PreparedStatement ps = null;
      try{
        con=getConnection();
        ps=con.prepareStatement("select count(*) from 
                      registration where balance < ?");
        ps.setDouble(1, findBalance);
        ps.executeQuery();
        rs = ps.getResultSet();
        if(rs.next()) {
          tmpcount = rs.getInt(1);
        }
        count.value=tmpcount;
        rs.close();
       }catch (Exception e) {
                 System.out.println("custom search: "+e);
                 returnResults.insert_long(-1);
                 return(returnResults);
       }
       finally {
         try{
           if(rs != null) { rs.close(); }
           if(ps != null) { ps.close(); }
           if(con != null) { con.close(); }
         } catch (Exception ignore) {}
       }
         returnResults.insert_long(tmpcount);
         return(returnResults);
     }else if(searchField.type().kind().value() == 
		TCKind._tk_string) {
      // return email addresses that match supplied address
      String findEmail=searchField.extract_string();
      Connection con = null;
      ResultSet rs = null;
      PreparedStatement ps = null;
      ArrayList ar = new ArrayList();
      RegistrationImpl reg=null;
      try{
        con=getConnection();
        ps=con.prepareStatement("select theuser, 
           emailaddress from registration 
	   where emailaddress like ?");
        ps.setString(1, findEmail);
        ps.executeQuery();
        rs = ps.getResultSet();
        while (rs.next()) {
          reg= new RegistrationImpl();
          reg.theuser = rs.getString(1);
          reg.emailaddress = rs.getString(2);
          ar.add(reg);
        }
        rs.close();

        RegistrationImpl[] regarray = 
             (RegistrationImpl [])ar.toArray(
	     new RegistrationImpl[0]);
          RegistrationHelper.insert(
                               returnResults, 
                               regarray[0]);
          return(returnResults);
       }catch (Exception e) {
        System.out.println("custom search: "+e);
        return(returnResults);
       }
       finally {
        try{
          if(rs != null) { rs.close(); }
          if(ps != null) { ps.close(); }
          if(con != null) { con.close(); }
        } catch (Exception ignore) {}
      }
    }
    return(returnResults);
  }

. Conclusión

Como hemos podido ver, convertir una aplicación para que use RMI o CORBA requiere muy pocos cambios en el corazón del programa. La principal diferencia ha sido la inicialización y el servicio de nombres. Mediante la abstracción de estas dos áreas en nuestra aplicación fuera de la lógica del negocio podemos migrar fácilmente entre diferentes arquitecturas de objetos distribuidos.

. JDBC

La aplicación de subasta con JavaBeans Enterpise y con sus dos variantes de "Remote Method Invocation" (RMI) y "Common Object Request Broker" (CORBA) han usado llamadas sencillas de JDBC JDBC para actualizar y consultar información desde una base de datps usando una conexión JDBC. Por defecto, el acceso a bases de datos JDBC implica abrir una conexión con la base de datos, ejecutar comandos SQL en un sentencia, procesar los datos devueltos y cerrar la conexión con la base de datos.

En conjunto, la aproximación por defecto funciona bien para bajos volúmenes de acceso a la base de datos, pero ¿cómo podemos manejar un gran número de peticiones que actualizan muchas tablas relacionadas a la vez y aún así asegurar la integridad de los datos? Esta sección explica cómo hacerlo con los siguientes tópicos:

. Drivers JDBC

La conexión con la base de datos está manejada por la clase Driver JDBC. El SDK de Java contiene sólo un driver JDBC, un puente jdbc-odbc que comunica con un driver "Open DataBase Conectivity" (ODBC) existente. Otras bases de datos necesitan un driver JDBC espécifico para esa base de datos.

Para obtener un idea general de lo que hacer un driver JDBC, podemos examinar el fichero JDCConnectionDriver.java. La clase JDCConnectionDriver implemta la clase java.sql.Driver y actúa como un driver "pass-through" re-enviando peticiones JDBC al driver JDBC real de la base de datos. La clase driver JDBC se carga con un llamada a Class.forName(drivername).

Estas líneas de código muestran cómo cargar tres clases diferentes de drivers JDBC:

  Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
  Class.forName("postgresql.Driver"); 
  Class.forName("oracle.jdbc.driver.OracleDriver");

Cada driver JDBC está configurado para entender una URL específica, por eso se pueden cargar varios drivers JDBC a la vez. Cuando especificamos una URL en el momento de la conexión, se selecciona el primer driver JDBC que corresponda.

El puente jdbc-odbc acepta URLS que empiecen con jdbc:odbc: y usa el siguiente campo de esa URL para especificar el nombre de la fuente de los datos. Este nombre identifica el esquema de la base de datos particular a la que queremos acceder. La URL también puede incluir más detalles sobre cómo contactyar con la base de datos e introducir la cuenta.

//access the ejbdemo tables
  String url = "jdbc:odbc:ejbdemo";

El siguiente ejemplo contiene la información de Oracle SQL*net sobre una base de datos particular llamada ejbdemo en la máquina dbmachine:

  String url = "jdbc:oracle:thin:user/password@(
	description=(address_list=(
	               address=(protocol=tcp)
	(host=dbmachine)(port=1521)))(source_route=yes)
	(connect_data=(sid=ejbdemo)))";

Este siguiente ejemplo usa mysql para conectar con la base de datos ejbdemo en la máquina local. También se incluyen los detalles del nombre de usuario y la password para el login.

  String url = 
   "jdbc:mysql://localhost/ejbdemo?user=user;
      password=pass";

Los drivers JDBC se dividen en cuatro tipos. También se pueden categorizar como puro java o drivers pequeños para indicar si son usados por aplicaciones clientes (drivers puro java) o por applets (drivers pequeños).

. Drivers del Tipo 1

Los drivers JDBC del tipo 1 son drivers puente como el puente jdbc.odbc. Estos drivers utilizan un intermediario como el ODBC para transferir las llamadas SQL a la base de datos. Los drivers puente cuentan con código nativo, aunque la librería de código nativo del puente jdbc-odbc forma parte de la Máquina Virtual Java 2.

. Drivers del Tipo 2

Los drivers del tipo 2 usan el API existente de la base de datos para comunicarla con el cliente. Aunque los drivers del tipo 2 son más rápidos que los del tipo 1, los del tipo 2 usan código nativo y requieren permisos adicionales para funcionar en un applet.

Un driver del tipo 2 podría necesitar código de base de datos en el lado del cliente para conectar a través de la red.

. Drivers del Tipo 3

Los Drivers del tipo 3 llaman al API de la base de datos en el servidor. Las peticiones JDBC desde el cliente son primero comprobadas por el Driver JDBC en el servidor para ejecutarse. Los drivers del tipo 3 y 4 pueden usarse en clientes applets ya que no necesitan código nativo.

. Drivers del Tipo 4

El nivel más alto de drivers reimplementa el API de red para base de datos en el lenguaje Java. Los Drivers del tipo 4 también pueden usarse en clientes applets porque no necesitan código nativo.

. Conexiones a Bases de Datos

Una conexión con una base de datso puede establecerese con un llamada al método DriverManager.getConnection. La llamada toma una URL que identifica la base de datos, y opcionalmente el nombre de usuario y la password para la base de datos.

  Connection con = DriverManager.getConnection(url);
  Connection con = DriverManager.getConnection(url, 
			"user", "password");

Después de establecer la conexión, se puede ejecutar una sentencia contra la base de datos. Los resultados de la sentencias pueden recuperarse y cerrarse la conexión.

Una características útil de la clase DriverManager es el método setLogStream. Podemos usar este método para generar información de seguimiento para ayudarnos a dignosticar problemas de conexión que normalmente no serían visibles. Para generar la información de seguimiento, sólo tenemos que llamar al método de esta forma:

  DriverManager.setLogStream(System.out);

La sección Connection Pooling en el capítulo 8 muestra cómo podemos mejorar las conexión JDBC sin cerrrar la conexión una vez completada la sentencia. Cada conexión JDBC a una base de datos provoca una sobrecarga al abrir un nuevo socket y usar el nombre de usuario y la password para login en la base de datos. La reutilización de las conexiones reduce la sobrecarga. Las colas de Conexiones mantienen una lista de conexiones abiertas y limpia cualquier conexión que no pueda ser reutilizada.

. Sentencias

Hay tres tipos básicos de sentencias SQL usadas en el API JDBC: CallabelStatement, Statement, y PreparedStatement. Cuando se envía una sentencias Statement o PreparedStatement a la base de datos, el driver la traduce a un formato que la base de datos pueda reconocer.

. Sentencias Callable

Una vez que hemos establecido una conexión con una base de datos, podemos usar el método Connection.prepareCall para crear una sentencia callable. Estas sentencias nos permite ejecutar prodecimientos almacenados SQL.

El siguiente ejemplo crea un objeto CallableStatement con tres parámetros para almacenar información de la cuenta de login:

  CallableStatement cs =
        con.prepareCall("{call accountlogin(?,?,?)}");
  cs.setString(1,theuser);
  cs.setString(2,password);
  cs.registerOutParameter(3,Types.DATE);

  cs.executeQuery();
  Date lastLogin = cs.getDate(3);

. Statements

El interface Statement nos permite ejecutar una simple sentencias SQL sin parámetros. Las instrucciones SQL son insertadas dentro del objeto Statement cuando se llama al método Statement.executeXXX method.

Sentencias Query: Este segmento de código crea un objeto Statement y llama al método Statement.executeQuery para seleccionar texto desde la base de datos