Tema 3: Un ejemplo completo de bean de sesión con estado

Vamos a construir en esta sesión un ejemplo completo de bean de sesión. En concreto, vamos a implementar un bean de sesión con estado que define un carrito de la compra de una librería.

El bean de sesión CartEJB representa un carrito de la compra de una librería on line. El cliente del bean podrá añadir un libro al carrito, eliminar un libro o consultar el contenido del carrito. Tal y como adelantamos en el tema 2, para construir el enterprise bean CartEJB, se necesitan los siguientes ficheros:

El estado del bean va a estar definido por sus variables de instancias. Estas variables de instancia deben declararse en la clase CartBean que implementa la lógica de negocio, y serán actualizadas por los métodos del bean, como consencuencia de la invocación remota del cliente.

Además de estas clases, vamos a necesitar dos clases de apoyo, la clase BookException y IdVerifier.

La siguiente tabla muestra cómo están relacionados los métodos en la interface home, la interfaz remota y la implementación.

  Método de la interfaz home Método de la interfaz remota Método de la clase de implementación
Creación Cart create(args) throws CreateException, RemoteException; No disponible public void ejbCreate(args)
Uso No disponible Métodos de negocio (deben arojar la excepción RemoteException) Métodos de negocio con la misma signatura (arrojar RemoteException es opcional)
Borrado public void remove(); public void remove() public void ejbRemove();

El código fuente de este ejemplo está en el fichero ejercicio3.zip en la página web del módulo de EJB.

3.1 La clase bean de sesión (SessionBean)

Siguiendo las indicaciones de nomenclatura que comentamos en la sesión pasada, la clase bean se llama CartBean. Esta clase implementa la lógica de negocio del bean, y debe cumplir los siguientes requisitos:

El código fuente sigue a continuación:


import java.util.*;
import javax.ejb.*;

public class CartBean implements SessionBean {

   String customerName;
   String customerId;
   Vector contents;


   public void ejbCreate(String person) 
      throws CreateException {

      if (person == null) {
         throw new CreateException("Null person not allowed.");
      }
      else {
         customerName = person;
      }

      customerId = "0";
      contents = new Vector();
   }

   public void ejbCreate(String person, String id) 
      throws CreateException {

      if (person == null) {
         throw new CreateException("Null person not allowed.");
      }
      else {
         customerName = person;
      }

      IdVerifier idChecker = new IdVerifier();
      if (idChecker.validate(id)) {
         customerId = id;
      }
      else {
         throw new CreateException("Invalid id: "+ id);
      }

      contents = new Vector();
   }

   public void addBook(String title) {
      contents.addElement(title);
   }

   public void removeBook(String title) throws BookException {

      boolean result = contents.removeElement(title);
      if (result == false) {
         throw new BookException(title + "not in cart.");
      }
   }

   public Vector getContents() {
      return contents;
   }

   public CartBean() {}
   public void ejbRemove() {}
   public void ejbActivate() {}
   public void ejbPassivate() {}
   public void setSessionContext(SessionContext sc) {}

} 

3.1.1 Las variables de instancia

Las variables de instancia definen el estado de una instancia de enterprise bean. En este caso se trata de customerName, customerId y contents. Las variables de instancia se inicializan al crearse una instancia del bean, copiándose en ellas los argumentos del método ejbCreate. Después son modificadas por las llamadas de los clientes a los métodos de negocio addBook() y removeBook().

3.1.2 La interfaz SessionBean

La interfaz SessionBean extiende la interfaz EnterpriseBean, que a su vez extiende el interfaz Serializable. La interfaz SessionBean declare los métodos ejbRemove, ejbActivate, ejbPassivate, y setSessionContext. Estos métodos se llaman desde el contenedor EJB cuando el bean va pasando de un estado a otro en su ciclo de vida. La clase CartBean no usa estos métodos, pero debe implementarlos porque están declarados en la interfaz SessionBean.

3.1.3 Los métodos ejbCreate

Un cliente no puede instanciar directamente un bean, ya que éste reside en el contenedor EJB. Sólo el contenedor EJB puede realizar la instanciación. En el ejemplo del carrito de la compra esto sucede de la siguiente forma:

1. El cliente invoca un método create en el objeto home:

Cart shoppingCart = home.create("Duke DeEarl","123");

2. El contenedor EJB instancia el enterprise bean.
3. El contenedor EJB invoca el método ejbCreate apropiado en CartBean:

    public void ejbCreate(String person, String id) 
      throws CreateException {

      if (person == null) {
         throw new CreateException("Null person not allowed.");
      }
      else {
         customerName = person;
      }

      IdVerifier idChecker = new IdVerifier();
      if (idChecker.validate(id)) {
         customerId = id;
      }
      else {
         throw new CreateException("Invalid id: "+ id);
      }

      contents = new Vector();
   }

Al tratarse de un bean de sesión con estado, el método ejbCreate inicializa este estado. El método ejbCreate anterior, por ejemplo, inicializa las variables customerName y customerId con los argumentos pasados por el método create.

Un enterprise bean debe tener uno o más métodos ejbCreate. Las signaturas de estos métodos deben cumplir los siguientes requisitos:

La clásula throws puede incluir la excepción javax.ejb.CreateException y cualquier otra excepción específica de la aplicación. El método ejbCreate arroja normalmente una excepción CreateException si un parámetro de entrada no es válido.

3.1.4 Métodos de negocio

El propósito fundamental de un bean de sesión es ejecutar tareas de negocio a petición de aplicaciones cliente. El cliente invoca métodos de negocio en la referencia remota del objeto que es devuelta por el método create. Desde la perspectiva del cliente, los métodos de negocio parecen ejecutarse de forma local, pero realmente lo que se ejecuta es un método del stub que envía al skeleton (recordatorio de RMI!!) la petición de invocación remota, junto con todo el marshaling de parámetros.

Un ejemplo del punto de vista del cliente:

Cart shoppingCart = home.create("Duke DeEarl", "123");
...
shoppingCart.addBook("The Martian Chronicles"); 
shoppingCart.removeBook("Alice In Wonderland");
bookList = shoppingCart.getContents();

La clase CartBean implementa los métodos de negocio en el siguiente código:

public void addBook(String title) {
   contents.addElement(new String(title));
}

public void removeBook(String title) throws BookException {
   boolean result = contents.removeElement(title);
   if (result == false) {
      throw new BookException(title + "not in cart.");
   }
}

public Vector getContents() {
   return contents;
}

La signatura de un método de negocio debe cumplir los siguientes requisitos:

La cláusula throws puede incluir excepciones que se definen en la aplicación. Por ejemplo, el método removeBook arroja la excepción BookException si el libro no está en el carrito.

Para indicar un problema de nivel de sistema, como la imposibilidad de conectar con una base de datos, un método de negocio debería arrojar una excepción javax.ejb.EJBException. Cuando un método de negocio arroja una excepción EJBException, el contenedor EJB la envuelve en una RemoteException, que es capturada por el cliente. El contenedor no envuelve excepciones de aplicación como BookException. Debido a que EJBException es una subclase de RuntimeException, no es necesario incluirla en la cláusula throws del método de negocio.

3.2 La interfaz home

La interfaz home extiende la interfaz javax.ejb.EJBHome. Para un bean de sesion, el propósito de la interfaz home es definir los métodos create() (puede haber más de uno, dependiendo de los argumentos) que el cliente remoto puede invocar para crear instancias del bean. El cliente CartCliente, por ejemplo, invoca este método create

(Cart shoppingCart = home.create("Duke DeEarl", "123");

Por cada método create en la interfaz home debe existir un método ejbCreate correspondiente en la clase de implementación del bean (clase CartBean). En nuestro ejemplo, definíamos dos signaturas de métodos ejbCreate:

public void ejbCreate(String person) throws CreateException   
... 
public void ejbCreate(String person, String id) 
   throws CreateException 

Y el código correspondiente en el fichero CartHome.java es el siguiente:

import java.io.Serializable;
import java.rmi.RemoteException;
import javax.ejb.CreateException;
import javax.ejb.EJBHome;

public interface CartHome extends EJBHome {
   Cart create(String person) throws 
                  RemoteException, CreateException;
   Cart create(String person, String id) throws 
                  RemoteException, CreateException; 
}

Las signaturas de ambos conjuntos de métodos son similares, pero difieren en aspectos importantes. Un resumen de las reglas que debemos aplicar al crear ambas signaturas es el siguiente:

3.3 Interfaz Remote

La interfaz remota siempre debe extender la interfaz javax.ejb.EJBObject. Esta interfaz define los métodos de negocio que el cliente remoto puede invocar. A continuación se muestra el código fuente del fichero Cart.java:

import java.util.*;
import javax.ejb.EJBObject;
import java.rmi.RemoteException;

public interface Cart extends EJBObject {
 
   public void addBook(String title) throws RemoteException;
   public void removeBook(String title) throws 
                     BookException, RemoteException;
   public Vector getContents() throws RemoteException;
}

La definición de los métodos en la interfaz remota debe seguir estas reglas:

3.4 El fichero descriptor del despliegue

El fichero ejb-jar.xml se muestra a continuación

<ejb-jar>
<display-name>CartJAR</display-name>
<enterprise-beans>
<session>
<display-name>CartEJB</display-name>
<ejb-name>CartEJB</ejb-name>
<home>CartHome</home>
<remote>Cart</remote>
<ejb-class>CartBean</ejb-class>
<session-type>Stateful</session-type>
<transaction-type>Container</transaction-type>
<security-identity>
<use-caller-identity/>
</security-identity> </session>
</enterprise-beans> ... </ejb-jar>

Y el fichero weblogic-ejb-jar.xml es el siguiente. Los únicos datos que se definen son el nombre del EJB y el nombre JNDI del bean.

  <weblogic-ejb-jar>
     <weblogic-enterprise-bean>
        <ejb-name>CartEJB</ejb-name>
        <stateful-session-descriptor><stateful-session-cache>
        </stateful-session-cache><stateful-session-clustering>
        </stateful-session-clustering></stateful-session-descriptor>
        <transaction-descriptor></transaction-descriptor>
        <jndi-name>CartEJB</jndi-name>
     </weblogic-enterprise-bean>
  </weblogic-ejb-jar>

3.5 Clases de apoyo

El bean de sesión CartEJB tiene dos clases de apoyo: BookException y IdVerifier. La excepción BookException es arrojada por el método removeBook y el IdVerifier valida el clienteId en uno de los métodos ejbCreate. Ambas clases deben residir en el mismo fichero JAR en el que se empaqueta todo el enterprise bean.

A continuación se muestran los códigos de ambas clases:

public class BookException extends Exception {

    public BookException() {
    }

    public BookException(String msg) {
        super(msg);
    }
}


public class IdVerifier {

    public IdVerifier() {
    }

    public boolean validate(String id) {

       boolean result = true;
       for (int i = 0; i < id.length(); i++) {
         if (Character.isDigit(id.charAt(i)) == false)
            result = false;
       }
       return result;
    }
}

3.6 El fichero EJB JAR

El fichero EJB JAR tiene la siguiente estructura, sencilla al no usar paquetes en la definición de las clases

extracted: META-INF/MANIFEST.MF
extracted: BookException.class
extracted: Cart.class
extracted: CartBean.class
extracted: CartHome.class
extracted: IdVerifier.class
  created: META-INF/
extracted: META-INF/ejb-jar.xml
extracted: META-INF/weblogic-ejb-jar.xml

3.7 La aplicación cliente

La aplicación cliente compra algunos libros y después elimina algún libro no existente en el carrito, lo que genera un error.

import java.util.*;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.rmi.PortableRemoteObject;

public class CartClient {

   public static void main(String[] args) {
       try {
           Context jndiContext = getInitialContext();
           Object objref = jndiContext.lookup("CartEJB");

           CartHome home = 
               (CartHome)PortableRemoteObject.narrow(objref, 
                                            CartHome.class);

           Cart shoppingCart = home.create("Duke DeEarl","123");

           shoppingCart.addBook("The Martian Chronicles");
           shoppingCart.addBook("2001 A Space Odyssey");
           shoppingCart.addBook("The Left Hand of Darkness");
           
           Vector bookList = new Vector();
           bookList = shoppingCart.getContents();

           Enumeration enumer = bookList.elements();
           while (enumer.hasMoreElements()) {
              String title = (String) enumer.nextElement();
              System.out.println(title);
           }

           shoppingCart.removeBook("Alice in Wonderland");
           shoppingCart.remove();

           System.exit(0);

       } catch (BookException ex) {
           System.err.println("Caught a BookException: " + ex.getMessage());
           System.exit(0);
       } catch (Exception ex) {
           System.err.println("Caught an unexpected exception!");
           ex.printStackTrace();
           System.exit(1);
       }
   } 

    public static Context getInitialContext() 
        throws javax.naming.NamingException {
           Properties p = new Properties();
	   p.put(Context.INITIAL_CONTEXT_FACTORY, "weblogic.jndi.WLInitialContextFactory");  
	   p.put(Context.PROVIDER_URL, "t3://localhost:7001");
           return new javax.naming.InitialContext(p);
    }
} 

3.8 Otras características de los enterprise beans

Los aspectos que siguen se aplican tanto a beans de sesión como a beans de entidad. Veremos en primer lugar cómo leer desde los beans entradas del entorno que se especifican de forma declarativa en la descripción de despliegue del bean. A continuación revisaremos cómo comparar enterprise beans. Terminaremos comentando cómo pasar al cliente referencias remotas de beans.

3.8.1 Cómo acceder a entradas del entorno

Una entrada del entorno (environment entry) es una pareja nombre-valor que se almacena en el fichero de descripción del despliegue y que permite modificar la lógica de negocio del bean sin cambiar su código fuente ni recompilarlo. El formato con el que se almacena en el fichero ejb-jar.xml es el siguiente

<env-entry>
<description>Discount Level</description>
<env-entry-name>DiscountLevel</env-entry-name>
<env-entry-type>java.lang.Double</env-entry-type>
<env-entry-value>300.0</env-entry-value>
</env-entry> <env-entry>
<description>Discount Percent</description>
<env-entry-name>Discount Percent</env-entry-name>
<env-entry-type>java.lang.Double</env-entry-type>
<env-entry-value>0.15</env-entry-value>
</env-entry>

En el ejemplo se han definido las entradas del entorno DiscountLevel y DiscountPercent.

Una vez desplegado el enterprise bean en el contenedor EJB, estas entradas se almancenan como objetos JNDI en el contexto java:comp/env. Para usar estas entradas en los métodos de negocio del enterprise bean hay que usar JNDI y obtener el contexto de nombres del entorno invocando a lookup del contexto inicial con el parámetro java:comp/env. Luego ya se puede invocar a loockup del contexto de nombres pasándole como parámetro el string que define el nombre de la entrada del entorno.

El siguiente código proporciona un ejemplo práctico, el método de negocio applyDiscount:

public double applyDiscount(double amount) {

   try {

      double discount;

      Context initial = new InitialContext();
         Context environment = 
            (Context)initial.lookup("java:comp/env");

      Double discountLevel = 
         (Double)environment.lookup("DiscountLevel");
            Double discountPercent = 
               (Double)environment.lookup("DiscountPercent");

      if (amount >= discountLevel.doubleValue()) {
         discount = discountPercent.doubleValue();
      }
      else {
         discount = 0.00;
      }

      return amount * (1.00 - discount);

   } catch (NamingException ex) {
      throw new EJBException("NamingException: "+
         ex.getMessage());
   }
}

3.8.2 Cómo comparar enterprise beans

Un cliente puede determinar si un bean de sesión con estado ya ha sido definido y está residiendo en el contenedor de aplicaciones usando el método isIdentical:

bookCart = home.create("Bill Shakespeare"); 
videoCart = home.create("Lefty Lee");
...
if (bookCart.isIdentical(bookCart)) { 
   // true ... }
if (bookCart.isIdentical(videoCart)) { 
   // false ... }

Debido a que los beans de sesión sin estado siempre tienen la misma identidad como objetos, el método isIdentical siempore devolverá true cuando se comparan.

Para determinar si dos beans de entidiad son identicos, el cliente puede usar el método isIdentical o puede obtener y comparar las claves primarias de los beans:

String key1 = (String)accta.getPrimaryKey();
String key2 = (String)acctb.getPrimaryKey();

if (key1.compareTo(key2) == 0)
   System.out.println("equal");

3.8.3 Cómo devolver una referencia a un enterprise bean

Supongamos que queremos pasar la referencia de un mismo bean a otro bean, o al cliente que invoca un método de negocio del bean. No podemos usar this, por que apunta a la instancia del bean, que está corriendo en el contenedor EJB.

Se obtiene llamando al método getEJBObject de la interfaz SessionContext. Un bean de entidad llamaría al método getEJBObject de la interfaz EntityContext. Estas interfaces proporcionan a los beans la posibilidad de acceder a contextos de instancias mantenidos por el contenedor EJB. Normalmente, el bean graba los contextos en el método setSessionContext. El siguiente código muestra un ejemplo de cómo usar estos métodos:

public class WagonBean implements SessionBean {
   
   SessionContext context;
   ...
   public void setSessionContext(SessionContext sc) { 
      this.context = sc; 
   }
   ...
   public void passItOn(Basket basket) {
   ...
      basket.copyItems(context.getEJBObject()); 
   } 
   ...