2 La arquitectura EJB

Los contenidos que vamos a ver en el tema son:

2.1 Repaso de RMI

Stubs, skeletons y paso de argumentos

En este apartado vamos a repasar algunos conceptos fundamentales para entender el funcionamiento de la arquitectura EJB: los stubs y skeletons, los objetos remotos y el paso de parámetros y devolución de resultados en las llamads remotas.

RMI (Remote Method Invocation) define la forma de comunicación remota entre objetos Java situados en máquinas virtuales distintas. Supongamos que un objeto cliente quiere hacer una petición a un objeto remoto situado en otra JVM (Máquina Virtual Java, Java Virtual Machine). RMI pretende hacer trasparente la presencia de la red, de forma que cuando escribas el código de los objetos clientes y remotos no tengas que tratar con la complicación de gestionar la comunicación física por la red.

Para ello, RMI proporciona al cliente un objeto proxy (llamado stub) que recibe la petición del objeto cliente y la transforma en algo que se puede enviar por la red hasta el objeto remoto. Este stub se hace cargo de todos los aspectos de bajo nivel (streams y sockets). En el lado del servidor, un objeto similar (llamado skeleton) recibe la comunicación, la desempaqueta y lanza el método correspondiente del objeto remoto al que sirve. Al igual que la petición, se deben empaquetar los argumentos de la llamada. El programador sólo debe definir el código del método en el objeto remoto. Los objetos stub y skeleton los construye el compilador de RMI de forma automática.

La siguiente figura muestra un ejemplo con el objeto remoto SaludoImpl:

El objeto remoto SaludoImpl está sombreado para indicar que en él se encuentra la implementación de los métodos remotos.

Una vez realizada la llamada, el stub queda en espera (y el objeto cliente que ha llamado al método correspondiente del stub) hasta recibir la respuesta del skeleton (respuesta que debe proprocionar el método invocado en el objeto remoto). Si la respuesta no se recibe, el stub lanza una excepción que debe capturar el objeto cliente.

Es posible que en el servidor no exista un objeto skeleton por cada objeto remoto sino que, para hacer la arquitectura más eficiente, se pueda definir un objeto genérico que distribuya las peticiones a los objetos remotos con algún mecanismo de identificación de la petición y de caché de objetos remotos. Por eso en las siguientes imágenes que mostremos no aparecerá este objeto.

Concretando más, para implementar y usar una clase remota con RMI debemos cumplir las siguientes condiciones

La siguiente imagen muestra la estructura de clases e interfaces que se usan para definir una sencilla clase remota llamada Saludo. Se define la interfaz Saludo que extiende la interfaz Remote. La clase SaludoImpl la escribe el programador e implementa la interfaz Saludo. La clase SaludoStub también implementa la interfaz Saludo y la construye el compilador de RMI. Por último, el cliente se comunica con un objeto instancia de la clase SaludoStub que implementa la interfaz Saludo. Esta interfaz es la única que ve el cliente.

 

Al implementar la interfaz Saludo, los objetos de la clase SaludoImp son también objetos Remote, ya que la interfaz Saludo hereda de la interfaz Remote.

Muy importante: a clase stub se debe instalar en la JVM del objeto cliente para que éste pueda usarla.

Pregunta:

Paso de argumentos

En cuanto a los argumentos y los valores devueltos por las llamadas remotas, deben ser de uno de los siguientes tipos:

Un caso muy interesante, por la frecuencia con la que sucede en la arquitectura EJB, es el de un objeto remoto que es devuelto en una llamada a otro objeto también remoto. El siguiente ejemplo proporciona una representación de lo que ocurre en este caso.

Supongamos que tenemos un conjunto de objetos remotos de la clase Estudiante, cada uno con un identificador determinado. Supongamos también un objeto remoto llamado estudianteFactory que puede localizar al objeto Estudiante con un identificador determinado.

  1. El cliente invoca el método getEstudiante("Id#2334") del stub de estudianteFactory. El stub trasmite la llamada al skeleton.
  2. El skeleton desempaqueta la llamada e invoca el método getEstudiante("Id#2334") en el objeto remoto estudianteFactory (objeto EJBHome). El objeto remoto localiza el objeto estudiante que se solicita y devuelve su referencia al Skeleton. Al ser estudiante un objeto remoto, estará disponible su stub en la JVM.
  3. El objeto skeleton estudianteFactory serializa el stub de estudiante y se lo pasa al skeleton, el cual lo trasmite al stub. El stub lo desempaqueta y crea una copia local en la JVM del cliente. Por último, el stub devuelve al objeto cliente una referencia local al stub de estudiante.

Pregunta:

Es muy importante recordar que cuando pasas un objeto serializado de una JVM a otra, la JVM que lo recibe debe tener disponible el fichero class correspondiente al tipo del objeto que pasas. Esto sucede así incluso cuando pasas un stub. Si la JVM a la que pasas el stub no tiene su definición, obtendrás un error.

2.2 EJB y RMI

¿Cómo usa la arquitectura EJB la tecnología RMI? Comencemos por presentar la siguiente figura, que muestra la diferencia fundamental entre EJB y RMI.

En el lado del cliente todo es igual. El cliente sigue comunicándose con un stub que implementa la interfaz Saludo. Sin embargo, los cambios se encuentran en el lado del servidor. El objeto remoto (Saludo EJBObject) no tiene la implementación realizada por el programador, sino que ésta se encuentra en el llamado objeto Bean (SaludoBean). El objeto remoto hace de "cortafuegos" que separa el bean de los clientes y permite intercalar las llamadas al contenedor en las peticiones de los clientes. Así es posible incorporar los servicios de transacciones, seguridad, etc., proporcionados por el servidor.

Además del objeto remoto que implementa la interfaz del bean (llamada interfaz componente), la arquitectura EJB proporciona otro objeto remoto llamado objeto home que hace el papel de factoría del bean. Este objeto home (SaludoHome, en el ejemplo) proprociona al cliente métodos remotos para crear y localizar instancias de beans. La siguiente figura proporciona una imagen completa de ambas características:

Veamos con detalle las clases que se deben implementar para defir un enterprise bean, así como las interfaces que extienden:

A su vez, las interfaces EJBObject y EJBHome heredan de la interfaz RMI Remote. Esto hace que las interfaces Saludo y SaludoHome sean también remotas. Sin embargo, ni la clase SaludoBean ni la clase SessionBean implementan la interfaz Remote. Esto es muy importante para entender la arquitectura de EJB: la clase bean no es remota. Recuerda que el objetivo principal de la estructura de clases e interfaces de EJB es evitar que el cliente llame directamente al bean. Existe siempre un objeto intermedio, el EJBObject, que intercepta las llamadas de los clientes y realiza todos los servicios que proporciona el servidor de aplicaciones.

La siguiente figura representa la estructura de clases del ejemplo que hemos visto.

¿Quién crea las clases que implementan todas estas interfaces de las que estamos hablando? Evidentemente, como ya hemos dicho, esa es tarea del contenedor EJB. La siguiente tabla resume quién escribe cada una de las clases de un bean.

Programador Contenedor de aplicaciones
La interfaz componente
que hereda de javax.ejb.EJBObject. (Saludo.java)
La clase EJBObject que implementa la interfaz componente. (Objeto remoto SaludoEJB.java)
La clase stub EJBObject que implementa la interfaz componente y sirve para comunicarse por la red con la clase EJBObject anterior. (SaludoStub.java)

La interfaz home que hereda de javax.ejb.EJBHome.
(SaludoHome.java)

La clase EJBHome que implementa la interfaz home. (SaludoHome.java)
La clase stub EJBHome que implementa la interfaz home y sirve para comunicarse por la red con la clase EJBHome anterior. (SaludoHomeStub.java)
La clase bean que implementa javax.ejb.SessionBean o javax.ejb.EntityBean.
(SaludoBean.java)
 

 

2.3 Interfaces locales y remotas

Hasta ahora hemos considerado siempre que el cliente y el bean se encuentran en distintas máquinas virtuales. Por eso es necesario RMI para conectar el cliente y el EJBObject. Pero ¿por qué introducir RMI cuando el cliente y el EJBObject se encuentran en la misma JVM? La introducción de RMI en la arquitectura la dota de flexibilidad, pero también le añade penalización en el rendimiento, debido sobre todo a la necesidad de serializar todos los argumentos y llamadas. Esta penalización no está justificada cuando el objeto cliente del bean es, por ejemplo, un servlet o una página JSP que reside en la misma JVM que el bean. Tampoco está justificado el uso de RMI cuando se están comunicando dos beans que residen en la misma JVM.

La especificación 2.0 de EJB propone el uso de las interfaces locales EJBLocalHome y EJBLocalObject como una solución a estas situaciones. A la hora de programar el bean lo único que cambia es que las interfaces home y componente deben heredar de EJBLocalHome y EJBLocalObject. Estas interfaces ya no son remotas, por lo que los métodos no van a tener que declarar la excepción RemoteException. En el nombre de las interfaces suelen añadirse la palabra local, para indicar que se tratan de objetos que van a llamarse sin usar RMI. En el caso del bean Saludo, llamaríamos SaludoLocal a la interfaz componente y SaludoLocalHome a la interfaz home.

package especialista;

import javax.ejb.*;

public interface SaludoLocal extends EJBLocalObject {
public String saluda();
}
package especialista;
import javax.ejb.*;

public interface SaludoHomeLocal extends EJBLocalHome {
public SaludoLocal create() throws CreateException;
}

En la arquitectura EJB, la única diferencia es que las llamadas entre el cliente y el EJBObject serían llamadas normales entre objetos Java y no tendrían que ser serializadas. Pero el objeto EJBObject seguiría haciendo las mismas funciones de proteger a los beans del exterior y de incorporar los servicios del contenedor EJB.

Por último, en el lado del cliente existen dos detalles que hay que modificar cuando se están utilizando interfaces locales. En primer lugar, las interfaces locales no declaran la excepción RemoteException, por lo que no es necesario que el cliente capture estas excepciones con un try/catch. El segundo aspecto tiene que ver con el casting del objeto home que obtenemos de JNDI. En el caso de que el objeto home sea remoto, vimos en la sesión anterior que antes de hacer el casting había que convertirlo en un objeto Java con la llamada al método PotableRemoteObject.narrow(). En el caso de estar trabajando con interfaces locales no es necesario hacer esta conversión con lo que para hacer el casting bastaría con hacer:

Object ref = jndiContext.lookup("SaludoEJB");
SaludoHome home = (SaludoHome) ref;

Terminamos con unas breves consideraciones que nos pueden ayudar a determinar si conviene usar interfaces locales o remotas:

2.3 Funcionamiento de la clase home

La clase home de un bean define los métodos de creación de instancias del bean. Cuando se despliega un bean en el contenedor EJB, se crea automáticamente una única instancia de la clase home del bean desplegado. Esta instancia reside en el contenedor EJB y queda a la espera de recibir peticiones de los clientes para crear instancias del bean.

El funcionamiento completo de la clase home se podría resumir con la siguiente figura y los siguientes pasos (para simplificar vamos a usar como ejemplo el bean de sesión sin estado SaludoBean que vimos en la sesión anterior).

  1. Desplegamos en el contenedor EJB el bean SaludoBean. Inmendiatamente, el contenedor crea una instancia de SaludoHome y la registra en JNDI con el nombre "SaludoEJB". Ésta va a ser la única instancia de objeto Home del bean SaludoBean.
  2. El cliente realiza un lookup en JNDI buscando el objeto home con el nombre "SaludoEJB".
  3. JNDI devuelve el stub del objeto Home y el cliente lo instala en su JVM. Este stub define la interfaz home del bean que el cliente puede usar para crear nuevas instancias del bean.
  4. El cliente llama al método create() del stub para que el objeto Home realice una instanciación del bean y devuelva un stub de la instancia del bean SaludoEJB recién creada. Este stub será un stub del EJBObject que intercepta las llamadas al bean.
  5. El objeto Home recibe la petición y le pide al contenedor EJB que inicialize un nuevo bean. Se crea un objeto de la clase SaludoBean y se crea su correspondiente EJBObject que intercepta las llamadas de los clientes. Por último, se devuelve el stub del EJBObject al cliente.
  6. Ahora ya por fin el cliente puede realizar una petición al bean Saludo. Ya puede usar la interfaz componente del bean, porque el stub devuelto por el objeto home implementa esta interfaz. Por ejemplo, puede ejecutar el método Saluda() del stub.
  7. Se realiza una llamada a un método de la interfaz componente. Esto es, el stub transfiere por la red todos los datos al EJBObject, éste realiza todas las comprobaciones necesarias y, por último, transfiere la petición al objeto bean que está representando.

Esta descripción del funcionamiento de la clase home, como hemos dicho, contempla únicamente el caso en de los beans de sesión sin estado. El funcionamiento es distinto según el tipo de bean. Cuando veamos el ciclo de vida de cada uno de los tipos de bean explicaremos cómo funciona la clase home en el resto de casos.

2.4 La arquitectura de los distintos tipos de beans

Hasta ahora hemos simplificado la descripción de la arquitectura EJB suponiendo que teníamos un único cliente y centrando la discusión en el acceso de los clientes a los beans.

Vamos a ampliar en este punto la explicación de la arquitectura EJB, centrándonos en la gestión de la concurrencia de acceso de los clientes y en el proceso de creación de los beans. Vamos a ver que la gestión de la concurrencia es distinta según tengamos un bean de sesión con estado, un bean de sesión sin estado, un bean de entidad o un bean dirigido por mensajes. Lo mismo sucede con el proceso de creación de los beans y su ciclo de vida en el contenedor EJB.

Todas estas consideraciones hacen que cada tipo de bean tenga unas características propias, en cuanto a eficiencia, escalabilidad, tiempo de respuesta, etc.. Es muy importante conocer estas características, ya que van a incidir directamente en el rendimiento de la aplicación que estés diseñando. Cuando veamos el ciclo de vida de cada uno de los tipos de beans haremos más consideraciones que también afectarán al rendimiento de cada tipo de bean.

Beans de sesión sin estado

Comencemos por recordar que los beans de sesión sin estado (por ejemplo, el bean SaludoEJB) proporcionan al cliente un conjunto de métodos remotos que representan tareas que el cliente puede solicitar. No existe ningún estado que deba mantener el bean: el cliente realiza la petición al EJBObject, el EJBObject pasa la petición al bean, el bean realiza la operación y devuelve la respuesta al cliente.

La simplicidad del funcionamiento de este tipo de beans hace posible que sean muy escalables y que tengan un rendimiento muy bueno. El contenedor puede utlizar bastantes técnicas para optimizar su rendimiento, como son el mantener una reserva (pool) de beans, el reutilizar una instancia de un bean para distintos clientes o el usar un único EJBObject para varios clientes.

La siguiente figura muestra un ejemplo en el que más de un cliente está solicitando servicios de un bean de sesión sin estado.

¿Cuándo crea el contenedor nuevas instancias de beans? Cuando lo considera necesario para mantener el rendimiento del servicio a los clientes. Puede ser que en un momento dado haya una avalancha de peticiones concurrentes y sea necesario aumentar el número de beans del pool de beans. Eso sí, como ya ha quedado claro, el método create() no hace que se cree un objeto nuevo. Todos sabemos que en Java las operaciones más costosas tienen que ver con la creación y desaparición de objetos y con la posible puesta en marcha del recolector de basura que ello conlleva.

Vamos a detallar el ciclo de vida del contenedor EJB durante el proceso de creación y uso de los beans de sesión sin estado.

  1. El cliente obtiene una referencia al objeto home.
  2. El cliente usa el método create() de la interfaz home para crear una instancia de un objeto EJBObject cuya referencia (su stub) se pasa al cliente.
  3. El cliente invoca un método de negocio en el objeto EJBObject.
  4. El contenedor reusa una instancia del bean existente en la reserva, si existe, o crea una nueva instancia del bean e inicializa su contexto llamando a setSessionContext().
  5. Si la instancia del bean ha sido creada en el paso anterior, el contenedor invoca ejbCreate() en la instancia del bean.
  6. El contenedor comienza una transacción, si hay que hacerlo.
  7. El contenedor invoca el método de negocio solicitado en la instancia del bean, y el bean realiza la operación solicitada.
  8. El contenedor confirma la transacción, si hay que hacerlo.
  9. Los resultados de la llamada al método de negocio se devuelven al cliente.
  10. El cliente puede invocar métodos adicionales en el objeto EJBObject. El contenedor puede realizar invocar los métodos adicionales en instancias distintas del bean.
  11. El cliente llama a remove() en el objeto EJBObject cuando ha terminado de usarlo, como una forma de comunicarle al contenedor EJB que ya no va a necesitar más el bean.
  12. En algún momento, el contenedor decide reducir el tamaño de la reserva de beans de sesión e invoca el método ejbRemove() en la instancia del bean que va a eliminar.

Beans de sesión con estado

Los beans de sesión con estado mantienen un estado distinto para cada cliente. Por ello, ya no es posible usar alguna de las optimizaciones que hemos comentado para los beans de sesión sin estado. Siempre hay una relación uno a uno entre clientes y objetos EJBObjects e instancias del bean.

El proceso de creación de un bean de sesión con estado es idéntico al de un bean de sesión sin estado, con la salvedad de que, al mantenerse en el bean un estado propio de la conexión con el cliente, la instancia del bean asignada al cliente es la que debe ejecutar todas las llamadas. No es posible, como se hacía en el punto 4 del ciclo de vida anterior, invocar a distintas instancias del bean.

Beans de entidad

En los beans de entidad, cada instancia de bean representa un objeto de negocio concreto. Por ejemplo, el estudiante ID213 o el aula ID101. Cada instancia de bean de entidad representa una fila de una o varias tablas (en el caso en que las tablas de la base de datos estén normalizadas y que la información de un objeto esté repartida en más de tabla).

En este caso, es posible que más de un cliente esté accediendo al mismo bean, por lo que el contenedor debe tener esto en cuenta y gestionar la concurrencia. Más adelante veremos qué tipo de estrategias existen para gestionar estos accesos concurrentes de los clientes a los beans de entidad.

En cuanto al ciclo de vida del bean se puede resumir en los siguientes pasos:

  1. El cliente obtiene una referencia al bean de entidad de alguna forma, como usando un método finder o recorriendo una relación de un bean.
  2. El cliente invoca un método de negocio en el objeto EJBObject. Asumimos que no hay ninguna transacción activa en ese momento.
  3. El contenedor comienza una transacción, si hay que hacerlo.
  4. El contenedor reutiliza una instancia existente en la reserva, si hay alguna disponible, o instancia una nueva instancia del bean e inicializa su contexto llamando a setEntityContext().
  5. El contenedor carga el estado persistente del bean del almacén persistente, o bién automáticamente o bién llamando a ejbLoad().
  6. El contenedor invoca el método de negocio deseado por el cliente y el bean realiza la operación.
  7. El contenedor guarda el estado del bean en el almacen persistente, o bién automáticamente o bién llamando a ejbStore().
  8. El contenedor confirma la transacción, si hay que hacerlo.
  9. Los resultados de la llamada al método de negocio se devuelven al cliente.

Esta es una descripción muy simplificada en la que no se contemplan muchas de las técnicas de optimización y caché que se utilizan para mejorar el rendimiento reduciendo el trabajo en cada paso o eliminándolo completamente.

Beans dirigidos por mensajes

  1. El cliente envía un mensaje al un servicio de mensajería JMS
  2. El servicio de mensajería envía el mensaje al contenedor
  3. El contenedor obtiene un bean dirigido por mensajes de la reserva de beans
  4. El contenedor envía el mensaje al bean mediante una llamada al método onMessage().

Despliegue de aplicaciones y beans

El proceso de despliegue de un bean es algo complejo y es muy común que se produzcan errores en su desarrollo. Para complicar más las cosas, es un proceso que no está estandarizado por J2EE y que depende de cada servidor de aplicaciones.

Vamos a intentar explicar los elementos fundamentales que sí contempla J2EE. Se trata de la especificación del contenido que de los distintos tipos de ficheros que intervienen en el despliegue. Son los siguientes:

A continuación mostramos el contenido de estos ficheros para el ejemplo de apliación enterprise en la que se usa el bean SaludoEjb de forma local (ver los ejercicios de la sesión 2).

Fichero EJB JAR (podría llamarse saludoLocalEjb.jar).
/saludoEjb
/saludoEjb/especialista
/saludoEjb/especialista/SaludoBean.class
/saludoEjb/especialista/SaludoLocal.class
/saludoEjb/especialista/SaludoLocalHome.class
/saludoEjb/META-INF
/saludoEjb/META-INF/ejb-jar.xml
/saludoEjb/META-INF/weblogic-ejb-jar.xml

El directorio especialista es necesario porque todas las clases SaludoBean, Saludo y SaludoHome se incluyen el paquete Java especialista (para evitar solapamiento de nombres con otros posibles beans desarrollados por terceros).

El fichero weblogic-ejb-jar.xml es el descriptor de despliegue específico del servidor de aplicaciones WebLogic. Es muy importante saber que en él se define el nombre JNDI del bean o los beans del fichero EJB JAR. 

Fichero EAR (podría llamarse saludoEar.jar):

/saludoEar
/saludoEar/saludoLocalEjb.jar
/saludoEar/saludoWar.jar
/saludoEar/META-INF
/saludoEar/META-INF/application.xml
/saludoEar/META-INF/weblogic-application.xml

El fichero weblogic-application.xml es el descriptor de despliegue de la aplicación enterprise específico del servidor de aplicaciones WebLogic.

El contenido de los descriptores de despliegue se muestra a continuación:

 ejb-jar.xml:

<!DOCTYPE ejb-jar PUBLIC '-//Sun Microsystems, Inc.//DTD Enterprise
JavaBeans 2.0//EN' 'http://java.sun.com/dtd/ejb-jar_2_0.dtd'>

<!-- Generated XML! -->

<ejb-jar>
  <enterprise-beans>
    <session>
      <ejb-name>SaludoLocalBean</ejb-name>
      <local-home>especialista.SaludoLocalHome</local-home>
      <local>especialista.SaludoLocal</local>
      <ejb-class>especialista.SaludoBean</ejb-class>
      <session-type>Stateless</session-type>
      <transaction-type>Container</transaction-type>
    </session>
  </enterprise-beans>

  <assembly-descriptor>
    <container-transaction>
      <method>
        <ejb-name>SaludoLocalBean</ejb-name>
        <method-name>*</method-name>
      </method>
      <trans-attribute>Required</trans-attribute>
    </container-transaction>
  </assembly-descriptor>

</ejb-jar>

weblogic-ejb-jar.xml:

<!DOCTYPE weblogic-ejb-jar PUBLIC '-//BEA Systems, Inc.//DTD WebLogic 8.1.0 EJB//EN' 
'http://www.bea.com/servers/wls810/dtd/weblogic-ejb-jar.dtd'>

<!-- Generated XML! -->

<weblogic-ejb-jar>
  <weblogic-enterprise-bean>
    <ejb-name>SaludoLocalBean</ejb-name>
    <stateless-session-descriptor>
      <pool>
      </pool>

      <stateless-clustering>
      </stateless-clustering>

    </stateless-session-descriptor>

    <transaction-descriptor>
    </transaction-descriptor>

    <local-jndi-name>SaludoLocalBean</local-jndi-name>
  </weblogic-enterprise-bean>
</weblogic-ejb-jar>

application.xml:

<!DOCTYPE application PUBLIC '-//Sun Microsystems, Inc.//DTD J2EE
Application 1.3//EN' 'http://java.sun.com/dtd/application_1_3.dtd'>
<application>
  <display-name></display-name>
  <module>
    <ejb>saludoLocalEjb</ejb>
  </module>
  <module>
    <web>
<web-uri>saludoWebApp</web-uri>
<context-root>saludoWebApp</context-root>
    </web>
  </module>
</application>