RMI depende enteramente de la capacidad para enviar objetos a y desde métodos en máquinas remotas. RMI asume una arquitectura orientada a objetos. Cualquier objeto que se envía como parámetro a un objeto remoto lo hace por valor. Esto significa que se pasa una copia del objeto, no el original o una referencia al original. La excepción a esta regla es un objeto que haya sido exportado como un objeto remoto (objeto que implementa métodos remotos). En este caso, al cliente se le envía un proxy o stub que representa un objeto que reside en el host, o máquina servidor. Cualquier interacción con el stub es enviada al original, que es quién realiza realmente cualquier operación que sea requerida por el stub.
Cuando se envían estructuras de datos desde una máquina a otra, dichos datos se deben codificar de cierta manera para que puedan ser enviados a través de la red. Esto normalmente implica implementar un protocolo de transmisión, incluyendo algún tipo de marcas para delimitar los campos y registros enviados. Las conexiones de red son, en su mayor parte, en serie, y los datos deben transformarse antes de que sean transmitidos a través de las conexiones serie.
El modelo RMI asume una plataforma homogénea, la plataforma Java. Pero incluso asumiendo un entorno homogéneo, se debe desarrollar un protocolo para manejar el desensamblado, transmisión y reconstrucción de los datos. La serialización de objetos hace referencia a los métodos usados para duplicar un objeto escribiendo los valores de sus campos en un flujo de datos y recreando una copia de dicho objeto a partir de dichos valores de campos.
Es importante destacar que la reconstrucción del objeto obtiene una
copia, no el objeto original. Los objetos serializados son siempre copias del
original. Cuando se necesite enviar una referencia a un objeto, en vez de una
copia, se debe usar un objeto remoto, es decir, un objeto exportado de forma
implícita extendiendo la clase java.rmi.server.UnicastRemoteObject
,
o explícitamente utilizando UnicastRemoteObject.exportObject(Remote)
,
y en cualquier caso, debe implementar una interfaz remota.
Para poder ejecutar un método remoto, RMI tiene que transmitir los argumentos del método desde el cliente al servidor, y transmitir los resultados en la dirección contraria. El proceso de codificar los argumentos y resultados para su transmisión se denomina marshalling; el proceso de decodificarlos cuando se reciben se denomina unmarshalling. RMI realiza los procesos de marshalling y unmarshalling via serialización Java.
Al proceso de convertir los componentes que conforman los datos en un flujo (stream) de bytes se denomina serialización. Se denomina de-serializazión a la acción de decodificar dicho flujo de bytes para reconstruir una copia del objeto original. El término serialización se usa también de forma informal para incluir ambos procesos.
Para que un objeto pueda ser marshalled y unmarshalled por RMI debe ser serializable. Para ello debe cumplir las siguientes reglas:
Serializable
, o su extensión Externalizable
static
o transient
serialVersionUID
para permitir características relacionadas
con el versioneado automático de clases
Si en una llamada RMI se encuentra una clase no serializable, se lanza la
excepción NotSerializableException
, la cual se manifiesta
en el cliente como MarshalException
o UnmarshalException
.
Un objeto es serializado de forma implícita por RMI cuando éste
es marshalled, o de forma explícita llamando a ObjectOutputStream.writeObject
.
Este método lanza una IOException
ante cualquier error de
entrada/salida, incluyendo NotSerializableException
si se encuentra
con un objeto no serializable.
Cuando un objeto está siendo serializado, se construye un grafo representando
a dicho objeto (como raiz) y las referencias que contenga a otros objetos (ver
Figura 3.1). Un grafo de objetos contiene un objeto raiz y todos los objetos
alcanzables desde la raiz. Cuando un objeto es serializado, se escribe en el
flujo de datos: su objeto base, si es serializable, y sus atributos (los atributos
declarados como static
o transient
son ignorados).
El proceso de serialización es recursivo: el mismo proceso se aplica
al objeto base (si es serializable), y a cualquier atributo que sea un objeto
(no tipos primitivos), a través del grafo de objetos del que el objeto
original es la raiz. Cualquier objeto en el grafo de objetos se serializa solamente
una vez, posteriores referencias a él se codifican usando un mecanismo
de referencias compartidas, que es decodificada durante el proceso de de-serialización.
Figura 3.1. Ejemplos de grafos de objetos.
Un objeto es de-serializado de forma implícita por RMI cuando éste
es unmarshalled, o de forma explícita llamando a ObjectIntputStream.readObject
.
Este método lanza una IOException
ante cualquier error de
entrada/salida, o ClassNotFoundException
si la clase correspondiente
a algún objeto en el grafo de objetos que está siendo de-serializado
no puede cargarse. Cuando un objeto es de-serializado, el grafo de objetos se
reconstruye con un aspecto idéntico al grafo utilizado para su serialización.
La serialización por defecto funciona bastante bien, pero quizá
es demasiado lenta. Una clase serializable puede implementar su propia serialización.
Para ello se deben declarar los métodos privados readObject
y writeObject
, que implementan cualquier tratamiento especial que
requiera dicha clase. Estos métodos son opcionales.
Como ejemplo de serializazión por defecto, supongamos que queremos enviar un objeto sencillo que representa un punto en dos dimenesiones:
class MyObject implements java.io.Serializable { int x; int y; MyObject (int x, int y) { this.x = x; this.y = y; } }
Debido a que implementa la interfaz Serializable
, dicho objeto
podría enviarse a un flujo de datos con el siguiente código:
MyObject point = new MyObject(10,20); ObjectOutputStream oos = new ObjectOutputStream(SomeOutputStream); oos.writeObject(point);
Dicho código crea un nuevo MyObject
, crea un ObjectOutputStream
(asumiendo que SomeOutputStream
es un objeto OutputStream
válido), y escribe los datos de MyObject
en el flujo de
datos.
El receptor puede reconstruir el objeto, asumiendo que tiene acceso local al
bytecode de MyObject
, con el siguiente código:
MyObject point = new MyObject(10,20); ObjectInputStream ois = new ObjectInputStream(SomeInputStream); point = (MyObject)ois.readObject();
A continuación mostramos el código para crear nuestros propios métodos para serializar los datos y reconstruir el objeto.
class MyObject implements java.io.Serializable { int x; int y; public MyObject() { //constructor sin argumentos } public MyObject (int x, int y) { this.x = x; this.y = y; } void writeObject(ObjectOutputStream s) throws java.io.IOException { s.writeInt(x); s.writeInt(y); } void readObject(ObjectInputStream s) throws java.io.IOException { x= s.readInt(); y= s.readInt(); } }
En este ejemplo trivial, el objeto reconstruido no es diferente del que se
obtiene con la serialización por defecto, pero podían haberse
incluido otras sentencias en los métodos readObject
y writeObject
para calcular nuevos valores de campos o manejar de forma especial los datos.
Cuando el objeto es serializado, el sistema chequea si el objeto define los
métodos readObject()
y writeObject()
. Si es
así, entonces chequea para ver si tiene un constructor sin argumentos.
En ese caso, el sistema llama al método writeObject()
con
un ObjectStream
como argumento para que el objeto pueda serializarse
él mismo. Entonces, en la parte del receptor, el objeto es instanciado
usando su constructor sin argumentos. A continuación se llama al método
readObject()
con un objeto ObjectInputStream
como
parámetro que es usado para reconstruir el objeto de datos.
En lugar de implementar la clase java.io.Serializable
, una clase
puede implementar la interfaz java.io.Externalizable
, para tener
un control completo sobre el protocolo usado para codificar las instancias de
esa clase. Por ejemplo, los métodos readObject y writeObject solamente
tienen en cuenta la lectura y escritura de atributos declarados en su clase,
pudiendo obviar campos de sus superclases. Además, la serialización
via Externalizable
probablemente funcionará más rápido
que la serialización por defecto via Serializable
.
La principal diferencia entre los objetos Externalizable
y Serializable
es que un objeto Externalizable debe definir métodos de lectura y escritura
(readExternal()
y writeExternal()
). No hay serialización
por defecto para un objeto Externalizable
.
public void writeExternal(ObjectOutput stream) throws IOException; public void readExternal(objectInput stream) throws IOException;
Típicamente, los métodos readExternal
y writeExternal
de una clase Externalizable
cuya clase base sea también
Externalizable
comenzarán llamando a super.readExternal
y super.writeExternal
respectivamente.