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:
CartBean
)CartHome
)Cart
)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.
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:
SessionBean
.public
.abstract
ni final
.ejbCreate
.public
sin parámetros.finalize
.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) {} }
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()
.
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
.
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:
public
.void
.static
ni final.
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.
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:
ejbCreate
o ejbActivate
.public
.static
ni final
.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.
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:
ejbCreate
asociado.ejbCreate
,
sin embargo, debe devolver void
. Es el contenedor EJB el que
se encarga de crear el bean, llamar al método ejbCreate
y devolver la referencia remota.throws
del método create
debe incluir las excepciones java.rmi.RemoteException
y java.ejb.CreateException
.
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:
throws
debe incluir la excepción
java.rmi.RemoteException
.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>
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; } }
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
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); } }
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.
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()); } }
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");
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()); } ...