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.
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.
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.
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.
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
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";
};
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
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 sigueintes 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
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.
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.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
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.
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".
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.
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.
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;
}
}
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;
}
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;
}
package registration;
import java.rmi.*;
import java.util.*;
public interface ReturnResults extends Remote {
public void updateResults(ArrayList results)
throws FinderException, RemoteException;
}
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.INVALID_USER);
}
AuctionItemHome home = (
AuctionItemHome) ectx.lookup(
"auctionitems");
AuctionItem ai= home.create(seller,
description,
auctiondays,
startprice,
summary);
if(ai == null) {
return Auction.INVALID_ITEM;
}else{
return(ai.getId());
}
}catch(Exception e){
System.out.println("insert problem="+e);
return Auction.INVALID_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");
}
}
_______
1 Cuando se usan en toda esta site, los términos, "Java virtual machine" o "JVM" significan una máquina virtual de la plataforma Java.