Remote Method Invocation: paso de objetos no remotos

Ramiro Lago (Febrero 2006, modificado en Septiembre 2008)

Introducción

En un capítulo anterior hemos visto una introducción a RMI. A continuación vamos a preguntarnos lo que ocurre cuando en la llamada al objeto remoto se pasan objetos en los argumentos y se devuelven objetos que no son remotos.

En Java ya sabemos que depende de la naturaleza de los parámetros:

Con RMI esta lógica cambia: cuando un cliente RMI hace una llamada remota y pasa como argumento un objeto no remoto, en realidad lo que le pasa es una copia del objeto. Lo mismo puede decirse de los objetos que se devuelven en una llamada remota: lo que en realidad se devuelve es una copia.

La buena noticia es que esta copia de objeto, puesto que hablamos de objetos no remotos, no tienen que tener un stub, ni tener un interfaz Remote, ni debe hacerse bin() o rebind(), no deben ser exportadas. No son servidores RMI, sino simplemente objetos de los que transportamos copias de una JVM a otra. No son objetos remotos.

Para transportar un objeto no remoto es necesario que su clase este declarada en el cliente (que la envía como argumento o la recibe en un return) y en el servidor (que la recibe como parámetro y la envía en un return).

Una segunda cosa necesaria es que sean Serializables.

Un ejemplo

Estructura del proyecto:

Supongamos que el objeto remoto es un especialista (GestionBDImpl y GestionBD) en acceso a una base de datos: realiza la carga del driver JDBC, la conexión, las consultas y actualizaciones, etc. Supongamos que una de las cosas que hace es una consulta de todos los clientes de nuestra tabla 'cliente'. En vez de devolver un chorro de bytes o caracteres lo más sensato es que devuelva un objeto del tipo Vector (objeto no remoto), que incluirá entre sus elementos objetos del tipo Cliente (que tampoco es un objeto remoto). Puesto que vamos a transportar un objeto (Vector) necesitamos que sea Serializable, y así es en efecto. El tipo Cliente puede observar que es un reflejo de lo que tenemos en la tabla (y también es serializable).

Para aprender sobre el acceso a base de datos con Java ver capítulo sobre JDBC

Para aprender el uso de log ver capítulo sobre log4j. Nuestro archivo de configuración de log4j (log\log4j.properties) es:


log4j.rootCategory=ALL, Default, MiConsola
log4j.appender.Default=org.apache.log4j.FileAppender
log4j.appender.Default.Threshold=INFO
log4j.appender.Default.ImmediateFlush=true
log4j.appender.Default.file=/doc/Java_eclipse/rmi02/log/server.log
log4j.appender.Default.layout=org.apache.log4j.PatternLayout
log4j.appender.Default.layout.ConversionPattern=%d %-5p %C.%M(%L)===> %m%n
log4j.appender.Default.append=false
log4j.appender.MiConsola=org.apache.log4j.ConsoleAppender
log4j.appender.MiConsola.Threshold=INFO
log4j.appender.MiConsola.ImmediateFlush=true
log4j.appender.MiConsola.layout=org.apache.log4j.PatternLayout
log4j.appender.MiConsola.layout.ConversionPattern=%d{ABSOLUTE} %-5p %C.%M(%L):==> %m%n

El cliente (Serializable):


package rmi02.beans;

public class Cliente implements java.io.Serializable {
	 private String codigo;
	 private String nombre;
	 private String ape1;
	 private String ape2;
	 private int edad;
	
	 public Cliente( String codigo, String nombre, String ape1, String ape2, int edad) {
		this.codigo = codigo;
		this.nombre = nombre;
		this.ape1 = ape1;
		this.ape2 = ape2;
		this.edad = edad;
	 }
	
	public String getCodigo() { return codigo; }
	public String getNombre(){ return nombre; }
	public String getApe1(){ return ape1; }
	public String getApe2() { return ape2; }
	public int getEdad() { return edad; }

	public void setCodigo( String codigo )	{ this.codigo = codigo; }
	public void setNombre( String nombre)	{ this.nombre = nombre; }
	public void setApe1( String ape1 )		{ this.ape1 = ape1; }
	public void setApe2( String ape2 ) 		{ this.ape2 = ape2; }
	public void setEdad( int edad ) 		{ this.edad = edad; }

}

Ahora pasemos a nuestro objeto remoto, el especialista en acceso a base de datos:


package rmi02.server;

import java.sql.DriverManager;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import java.sql.SQLException;
import java.util.Vector;
import java.rmi.server.UnicastRemoteObject;
import java.rmi.RemoteException;

import rmi02.server.GestionBD;
import rmi02.beans.Cliente;
import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;

/********************************************************************************
 * Clase remota especializada en el acceso a la base de datos de clientes
 * Es un ejemplo de como el cliente y este objeto remoto se pueden pasar
 * objetos no remotos del tipo Cliente. Puesto que son objetos no remotos el
 * paso se hace por COPIA.
 * El atributo ultimoCliente simplemente almacena el cliente devuelto por la
 * última llamada a getCliente(String).
 * Usa jdbc y log4j. log4j escribe en log/server.log y en consola de servidor
 ********************************************************************************/
public class GestionBDImpl extends UnicastRemoteObject implements GestionBD {
	private Cliente ultimoGetCliente = null;           // Ultimo cliente para el que se usa getCliente()
	private Logger logger = Logger.getLogger( this.getClass() );

	/*******************************************************************************
	 * Constructor, carga driver y configura logger
	 ******************************************************************************/
	public GestionBDImpl()  throws RemoteException, ClassNotFoundException {
		// log4j muestra resultados en archivo y en consola del servidor
		PropertyConfigurator.configure( "d:/DOC/Java_Eclipse/rmi02/log/log4j.properties");

		Class.forName("com.mysql.jdbc.Driver");
	}

	/*******************************************************************************
	 * Realiza la consulta de todos los clientes.
	 * Devuelve Vector de elementos de tipo 'Cliente'
	 ******************************************************************************/
	public Vector getClientes() throws RemoteException, SQLException {
		Connection con = abrirConexion();

		Vector vec = new Vector();
		//// Definir sentencia y ejecutarla
		Statement sentencia = con.createStatement();
		ResultSet rs = sentencia.executeQuery( "SELECT c.codigo,c.nombre,c.ape1,c.ape2,c.edad "+
		    										"FROM cliente c" );
		while (rs.next()){
				Cliente c = new Cliente( rs.getString("codigo"), rs.getString("nombre"),
								rs.getString("ape1"), rs.getString("ape2"), rs.getInt("edad") );
				vec.add( c );
		}
		cerrarConexion(con);
		return vec;         // Devuelvo el vector
	}
	/*******************************************************************************
	 * Realiza la consulta de un cliente cuyo codigo es el parámetro. Si no lo encuentra
	 * devuelve null.
	 ******************************************************************************/
	public Cliente getCliente( String codigo)  throws SQLException, RemoteException {
		Connection con = abrirConexion();

		//// Definir sentencia y ejecutarla
		Statement sentencia = con.createStatement();
		ResultSet rs = sentencia.executeQuery( "SELECT c.codigo,c.nombre,c.ape1,c.ape2,c.edad "+
		    										"FROM cliente c WHERE codigo='" + codigo +"'");
		rs.next();

		try {
			ultimoGetCliente = new Cliente( rs.getString("codigo"), rs.getString("nombre"),
								rs.getString("ape1"), rs.getString("ape2"), rs.getInt("edad") );
		} catch (Exception e) {
			cerrarConexion(con);
			return null;	// Si no ha encontrado cliente, devuelve null;
		}
		cerrarConexion(con);

		return ultimoGetCliente;           // Devuelvo el cliente
	}

	/*******************************************************************************
	 * Realiza UPDATE de un cliente que se pasa como parámetro.
	 * Devuelve nº de registros actualizados
	 ******************************************************************************/
	public int setCliente( Cliente cliente ) throws SQLException, RemoteException {
		Connection con = abrirConexion();

		//// Definir sentencia y ejecutarla
		Statement sentencia = con.createStatement();
		int actualizaciones = sentencia.executeUpdate( "UPDATE cliente SET nombre = '" + cliente.getNombre() +
		    								"', ape1='" + cliente.getApe1() + "', ape2='" + cliente.getApe2() +
		    								"', edad='" + cliente.getEdad() + "' " +
		    								"FROM cliente WHERE codigo='" + cliente.getCodigo() +"'");
		cerrarConexion(con);

		return actualizaciones;
	}

	/********************************************************************
	 Me aseguro de que se cierra la conexión
	 ********************************************************************/
	public void cerrarConexion(Connection con) throws SQLException, RemoteException {
		if (con != null) {
			if (!con.isClosed()) { // Si no está cerrada, la cierro
				con.close();
				con = null;
		        logger.info("Cerrada conexión");
			}
		}
	}
	/********************************************************************
	 Abrir conexión
	 ********************************************************************/
	public Connection abrirConexion() throws SQLException {
		Connection con = DriverManager.getConnection( "jdbc:mysql://localhost:3306/proactiv_prueba","proactiv_alumnos", "nebrija" );
		logger.info("Establecida conexión");
		return con;
	}

	/********************************************************************
	 Devuelve el último cliente que se ha devuelto por getCLiente()
	 ********************************************************************/
	public Cliente getUltimoGetCliente() {
		return ultimoGetCliente;
	}

}
	

El servidor es:


package rmi02.server;
import java.rmi.*;
import java.rmi.registry.*;

/***********************************************************************************
 * Servidor RMI. Exporta objeto (un Objeto de Acceso a Base de Datos MySQL) y muestra
 * los vínculos (bindings).
 * VER ARRANCARSERVIDOR.BAT
 ***********************************************************************************/
public class ServidorRMI {
   public static void main(String[] args) {
      try {
    	  System.out.println( "Servidor iniciado" );
    	  GestionBDImpl gestorBD = new GestionBDImpl();
    	  Registry r = LocateRegistry.getRegistry();
    	  r.rebind( "gestorBD1", gestorBD );
    	  mostrarBindings( "" );
    	  System.out.println( "Servidor activo y con objeto remoto a la espera de llamadas" );

      }
      catch (Exception e) {
    	  e.printStackTrace();
      }
   }

   /********************************************************************************
    * Muestra los vínculos (bindings) de un registro cuyo nombre es 'nombre_registro'
    *******************************************************************************/
   public static void mostrarBindings( String nombre_registro ) {
      try {
    	  String[] bindings = Naming.list( nombre_registro );
    	  System.out.println( "Vinculos disponibles:");
    	  for ( int i = 0; i < bindings.length; i++ )
    		  System.out.println( bindings[i] );
      }
      catch (Exception e) {
    	  e.printStackTrace();
      }

   }
}

El archivo arrancarServidor.bat es:


echo off
echo COMPILO SERVIDOR Y BEANS
D:
cd \DOC\Java_eclipse\rmi02
javac -d bin src\rmi02\beans\*.java src\rmi02\server\*.java
cd bin
echo CREO STUB CON RMIC
rmic rmi02.server.GestionBDImpl
echo ARRANCO RMIREGISTRY Y SERVIDOR:
start rmiregistry
rem Si no tuviesemos el conector JDBC y log4j en el CLASSPATH, tendriamos que usar la opcion -classpath:
rem java -classpath .;D:\DOC\Java_eclipse\rmi02\lib\mysql-connector-java-3.1.6-bin.jar;D:\DOC\Java_eclipse\rmi02\lib\log4j-1.2.14.jar rmi02.server.ServidorRMI
echo on
java rmi02.server.ServidorRMI
pause

Nuestro cliente obtiene una referencia al stub de GestionBDImpl (como siempre, la referencia es del tipo del interfaz) y logra mandar un mensaje por el que se devuelve un Vector con elementos del tipo Cliente:


		....
    	  GestionBD gestor1 = (GestionBD) Naming.lookup( url + "gestorBD1");
    	  Vector vecClientes = gestor1.getClientes();
	

Este mensaje (getClientes()), puesto que es accesible desde el cliente, debe estar en el interfaz remoto (GestionBD):


package rmi02.server;

import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.Vector;
import java.sql.SQLException;
import rmi02.beans.Cliente;

public interface GestionBD extends Remote {
	public Vector getClientes() throws RemoteException, SQLException;
	....
}
	

Paso de objetos no remotos: una copia

GestionBDImpl tiene un atributo que es una referencia a objeto NO REMOTO. No es remoto, ya que no se exporta (ni falta que hace en este ejemplo, como veremos más adelante):


	private Cliente ultimoGetCliente = null; // Ultimo cliente para el que se usa getCliente()
	

Este atributo es una referencia a un objeto que se crea y devuelve en getCliente(String) de GestionBDImpl:

Lo que se hace en este método es una consulta a la tabla 'cliente', buscando un cliente cuyo código sea igual al parámetro. Si lo encuentra, crea un objeto (ultimoGetCliente) de la clase Cliente y lo devuelve (return). La pregunta es sencilla, ¿Lo que recibirá el cliente es una copia o una referencia a dicho objeto?.

El cliente:

  1. Pide el host por teclado, si se pulsa Enter asume que es localhot
  2. Obtiene objeto remoto (del tipo interface GestionBD)
  3. Llama al objeto remoto (GestionBDImpl.getClientes()) para obtener y mostrar vector de clientes
  4. Llama al objeto remoto (GestionBDImpl.getCliente(String)) para obtener un cliente cuyo código es el argumento, lo denomica cli. GestionBDImpl almacena dicho objeto devuelto en el atributo ultimoGetCliente
  5. Cambiamos la edad de cli. ¿Hemos cambiado el objeto original (ultimoGetCliente)?
  6. Llama a GestionBDImpl.getUltimoGetCliente(), que devuelve el atributo ultimoGetCliente, y muestra el objeto devuelto. ¿Ha cambiado ultimoGetCliente?

package rmi02.cliente;

import java.rmi.*;
import java.util.Vector;
import java.io.*;

import rmi02.server.GestionBD;
import rmi02.beans.*;

/***************************************************************************************
 * Cliente RMI. El servidor exporta el objeto remoto que accede a la base de datos de clientes.
 * Es un ejemplo de como el objeto remoto devuelve objetos no remotos (Cliente, Vector, etc),
 * por tanto la llamada es por copia, es decir, recibimos una copia de los objetos no remotos.
 ***************************************************************************************/
public class ClienteRMI {
   public static void main(String[] args) {

      //// Leemos desde teclado la url del host. Si pulsa Enter es localhost
      String url;
      try {
    	  System.out.println("\nIntroduzca server (ENTER='rmi://localhost/'), ojo, debe terminar con /:");
    	  BufferedReader entrada = new BufferedReader(new InputStreamReader(System.in));
    	  String str = entrada.readLine();
    	  if (str.length() == 0)
    		  url = "rmi://localhost/";
    	  else
    		  url = str;
      }
      catch (IOException e) {
    	  url = "rmi://localhost/";
      }

      try {

    	  //// Obtenemos objeto remoto
    	  GestionBD gestor1 = (GestionBD) Naming.lookup( url + "gestorBD1");

    	  //// Obtenemos vector de clientes y lo mostramos
    	  Vector vecClientes = gestor1.getClientes();
    	  System.out.println("Listado de todos los clientes:");
    	  for ( int i = 0; i < vecClientes.size(); i++ ) {
    		  Cliente c = (Cliente) vecClientes.get( i );
        	  System.out.println( c.getCodigo() + ", " + c.getNombre() + " " + c.getApe1() + ", " + c.getEdad());
    	  }

    	  //// Obtenemos un cliente a partir del codigo introducido por teclado
    	  System.out.println("\nIntroduzca un codigo de cliente:");
    	  BufferedReader entrada = new BufferedReader(new InputStreamReader(System.in));
    	  String codigo = entrada.readLine();
    	  Cliente cli = (Cliente) gestor1.getCliente( codigo ); // DEVUELVE COPIA DE CLIENTE

    	  //// Si no se ha encontrado, avisamos y salimos
    	  if ( cli == null ) {
        	  System.out.println("\nCliente no encontrado.");
        	  return;
    	  }

    	  //// Cambio la edad del objeto Cliente recibido
    	  System.out.println("\nHe cambiado el ultimo objeto Cliente recibido, edad=99:");
    	  cli.setEdad( 99 );
    	  System.out.println( cli.getCodigo() + ", " + cli.getNombre() + " " + cli.getApe1() + ", " + cli.getEdad());

    	  //// Sin embargo ese cliente lo recupero del objeto remoto y no ha cambiado
    	  //// La explicación: el objeto recibido es una COPIA
    	  System.out.println("\nPero el objeto en el servidor no ha cambiado:");
    	  Cliente cli2 = (Cliente) gestor1.getUltimoGetCliente();
    	  System.out.println( cli2.getCodigo() + ", " + cli2.getNombre() + " " + cli2.getApe1() + ", " + cli2.getEdad());

      }
      catch(Exception e) {
    	  e.printStackTrace();
      }
   }
}

Resumiendo, los objetos remotos se pasan a través de la red como stubs, mientras que los que no lo son (en nuestro ejemplo 'Vector' o 'Cliente') se pasan como COPIAS, todo esto es automático y sin intervención por parte del programador.




Volver al índice