Ejemplo básico de Enterprise JavaBeans (EJB)

(Enero de 2005)

Introducción

Los EJBs requieren un contenedor específico. Hay un capítulo para informarse de la instalación y configuración de un contenedor EJB como JBoss.

A continuación se puede ver la estructura del proyecto:


Crear el Bean

Vamos a empezar pensando en la implementación del bean, que debe usar (implements) el interfaz javax.ejb.SessionBean. Los primeros cuatro métodos son específicos a la implementación "de negocio". Los siguientes métodos no son necesarios, pero se sobreescriben con la finalidad de observar las salidas que va a producir el servidor:


package orderMgmt;

import javax.ejb.SessionContext;
import java.util.Date;

/**********************************************************************************
 * EJB de sesión sin estado.
 * Debe definir un ejbCreate() coincidente (en args) por cada create() del interfaz
 EJBHome
 *********************************************************************************/
public class OrderManagementBean implements javax.ejb.SessionBean {

  public void placeOrder(String custName, String prodName, int quantity) {
    System.out.println("Pedido de  " + quantity + " copias de " + 
                       prodName + " para el cliente " + custName);
  }
  
  public String getOrderDate() {
	  return (new Date()).toString();
  }

  public void cancelOrder(String custName, String prodName) {
    System.out.println("Orden cancelada");
  }

  public boolean isShipped(String custName, String prodName) {
    System.out.println("Pedido en transito");
    return true;
  }

  public void ejbCreate() {
    System.out.println("--->Llamada a ejbCreate()");
  }

  public void ejbRemove() {
    System.out.println("--->Llamada a ejbRemove()");
  }

  public void ejbActivate() {
    System.out.println("--->Llamada a ejbActivate()");
  }

  public void ejbPassivate() {
    System.out.println("--->Llamada a ejbPassivate()");
  }

  public void setSessionContext(SessionContext ctx){
    System.out.println("--->Llamada a setSessionContext()");
  }
}
	

El método ejbCreate() del bean se corresponde con el método create() del interfaz local o básica.


Crear el interfaz local

A continuación se puede ver el interfaz EJBHome (interfaz local o básica), que tiene sólo la referencia o firma del método create(). Es importante porque se usa para crear el objeto bean y devuelve una referencia del tipo interfaz remoto, de esta forma el cliente puede acceder al bean remoto:


package orderMgmt;

/*********************************************************************************
 * Interfaz EJBHome
 * El interfaz local se utiliza para crear y conseguir acceso al interface remoto,
 * por medio del método create() que devuelve una referecia del tipo interfaz remota.
 * Debe definir al menos un create() sin parámetros.
 *********************************************************************************/
public interface OrderManagementHome extends javax.ejb.EJBHome {
	OrderManagement create() throws java.rmi.RemoteException, javax.ejb.CreateException;
}
	

Crear el interfaz remoto

El interfaz remoto hereda de javax.ejb.EJBObject y debe tener las referencias o firmas de los métodos que son accesibles de forma remota. La interfaz remota es implementada por el contenedor EJB. Se recomienda al programador que no implemente esta interfaz para evitar accesos directos a los objetos remotos, ya que los accesos deben realizarse a través del proxy generado por el servidor EJB:


package orderMgmt;

/**********************************************************************************
 * Interfaz remota
 *********************************************************************************/
public interface OrderManagement extends javax.ejb.EJBObject {
  void placeOrder(String custName, String prodName, int quantity) throws java.rmi.RemoteException;
  String getOrderDate() throws java.rmi.RemoteException; 
  void cancelOrder(String custName, String prodName) throws java.rmi.RemoteException;
  boolean isShipped(String custName, String prodName) throws java.rmi.RemoteException;
}
	

Un bean de sesión sin estado, como en este ejemplo, debe definir un método create() sin argumentos. Un bean de sesión con estado tendrá uno o más métodos create().


Archivos de despliegue

La especificación EJB exige la existencia de un descriptor de despliegue, que se llamará ejb-jar.xml y se sitúa en el directorio META-INF. La estructura del proyecto dentro de nuestro IDE es:

Como se puede ver el descriptor sirve para señalar los tres archivos class que componen el EJB y caracterizar al bean, entre otras cosas como un componente sin estado (Stateless):


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

<ejb-jar>
 <enterprise-beans>
   <session>
      <ejb-name>OrderManagement</ejb-name>
      <home>orderMgmt.OrderManagementHome</home>
      <remote>orderMgmt.OrderManagement</remote>
      <ejb-class>orderMgmt.OrderManagementBean</ejb-class>
      <session-type>Stateless</session-type>
      <transaction-type>Container</transaction-type>
   </session>
 </enterprise-beans>

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

Además casi todos los servidores de contenedores EJB precisan un descriptor de recursos. En nuestro caso se llama jboss.xml y lo dejamos en META-INF. Se puede observar que asigna al bean una referencia JNDI:


<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE jboss PUBLIC "-//JBoss//DTD JBOSS//EN"
	"http://www.jboss.org/j2ee/dtd/jboss.dtd">
<jboss>
	<enterprise-beans>
		<session>
			<ejb-name>OrderManagement</ejb-name>
			<jndi-name>ejb/OrderManagement</jndi-name>
		</session>
	</enterprise-beans>
</jboss>
	

Con ello la estructura de archivos hasta ahora es la siguiente (derivados de bin):

Desplegar el EJB en el servidor

La especificación J2EE señala que la instalación en el servidor se realiza por medio de un archivo jar, que contiene la estructura de archivos y directorios antes mencionada:


C:\DOC\Java_eclipse\ejb_OrderManagement\bin>jar cvf OrderManagement.jar META-INF/ orderMgmt/*.class
manifest agregado
ignorando entrada META-INF/
agregando: META-INF/ejb-jar.xml(entrada = 809) (salida= 343)(desinflado 57%)
agregando: META-INF/jboss.xml(entrada = 313) (salida= 201)(desinflado 35%)
agregando: orderMgmt/OrderManagement.class(entrada = 467) (salida= 249)(desinflado 46%)
agregando: orderMgmt/OrderManagementBean.class(entrada = 2069) (salida= 925)(desinflado 55%)
agregando: orderMgmt/OrderManagementHome.class(entrada = 289) (salida= 198)(desinflado 31%)
	

Puede comprobar el contenido del archivo jar mediante:


C:\DOC\Java_eclipse\ejb_OrderManagement\bin>jar tvf OrderManagement.jar
	

Observar que se hace desde el directorio "anterior" (bin en nuestro caso). El lugar donde se coloca este archivo jar es asunto dependiente de cada fabricante del servidor, en nuestro caso es muy sencillo: situamos el archivo jar en el directorio 'deploy' que corresponde con el tipo de servidor ('default' en nuestro caso) que hayamos elegido. En resumen lo situamos en: JBOSS_HOME\server\default\deploy.

Buena noticia, si tenemos activo el servidor JBoss, este capta el archivo nuevo (modificado), descarga la vieja versión del bean (si la hubiese) y despliega la nueva versión. En nuestro ejemplo:

10:22:14,164 INFO  [EJBDeployer] Undeploying: file:/C:/Desarrollo/jboss-3.2.8/server/default/deploy/OrderManagement.jar
10:22:14,305 INFO  [STDOUT] --->Llamada a ejbRemove()
10:22:14,335 INFO  [EjbModule] Undeployed: OrderManagement
10:22:16,017 INFO  [EjbModule] Deploying OrderManagement
10:22:16,498 INFO  [EJBDeployer] Deployed: file:/C:/Desarrollo/jboss-3.2.8/server/default/deploy/OrderManagement.jar
	

Lo siguiente es definir y ejecutar el cliente.


El cliente

No olvidar que cada cliente debe tener en el CLASSPATH la librería jbossall-client.jar del directorio JBOSS_HOME/client.

Hay que usar JNDI, por ello no debe sorprender que sea necesario conseguir un contexto. Las propiedades del contexto son dependientes del fabricante del servidor EJB. En nuestro caso:


    	Properties props = new Properties();
        props.put(Context.INITIAL_CONTEXT_FACTORY,"org.jnp.interfaces.NamingContextFactory");
        props.put(Context.PROVIDER_URL, "jnp://localhost:1099");
        Context ctx = new InitialContext(props);
	

Evidentemente el componente "org.jnp.interfaces.NamingContextFactory", debe estar en el CLASSPATH (en nuestro caso es así, ya que se encuentra en jbossall-client.jar. El puerto (1099) depende de la configuración de servidor. A continuación obtenemos una referencia del tipo interfaz EJBHome al bean (el nombre "ejb/OrderManagement" es un típico nombre JNDI, definido en jboss.xml):


        Object objRef = ctx.lookup("ejb/OrderManagement");
        OrderManagementHome home = (OrderManagementHome)javax.rmi.PortableRemoteObject.narrow(
                		  objRef, OrderManagementHome.class);
	

Con esta referencia local (tipo interfaz EJBHome) se consigue la referencia remota (interfaz remoto OrderManagement) que nos va a permitir llamar al objeto remoto, recordar que desde al cliente se accede al objeto remoto sólo por medio de su interfaz remoto (como en RMI):


        OrderManagement orderManagement = home.create();
        orderManagement.placeOrder("Juan","J2EE Server Programming", 1000);
        System.out.println( "Fecha del pedido: " + orderManagement.getOrderDate() );
	

Al final invocamos al método remove() del interfaz

El código fuente completo del cliente es:


package cliente;

import orderMgmt.*;
import java.util.Properties;
import javax.naming.Context;
import javax.naming.InitialContext;
/**************************************************************************************
 * Este cliente EJB requiere (si usa JBOSS): jbossall-client.jar 
 *************************************************************************************/
public class Client {
  public static void main(String[] args) {
    try {
    	
    	Properties props = new Properties();
        props.put(Context.INITIAL_CONTEXT_FACTORY,"org.jnp.interfaces.NamingContextFactory");
        props.put(Context.PROVIDER_URL, "jnp://localhost:1099");

        Context ctx = new InitialContext(props);
        Object objRef = ctx.lookup("ejb/OrderManagement");
        
        OrderManagementHome home = (OrderManagementHome)javax.rmi.PortableRemoteObject.narrow(
                		  objRef, OrderManagementHome.class);

        OrderManagement orderManagement = home.create();
        orderManagement.placeOrder("Juan","J2EE Server Programming", 1000);
        System.out.println( "Fecha del pedido: " + orderManagement.getOrderDate() );
        orderManagement.remove();
        ctx.close();
    } catch (Exception e){
      e.printStackTrace();
    }
  }
}
	

La ejecución produce dos resultados. Primero aparece en la consola del servidor el siguiente mensaje (del método placeOrder()):


10:55:43,614 INFO  [STDOUT] --->Llamada a setSessionContext()
10:55:43,614 INFO  [STDOUT] --->Llamada a ejbCreate()
10:55:43,614 INFO  [STDOUT] Pedido de  1000 copias de J2EE Server Programming para el cliente Juan
	

A continuación se ejecuta getOrderDate() en la consola del cliente:


Fecha del pedido: Mon Feb 13 10:55:43 CET
	

El contenedor se interpone

Se puede observar que ejbCreate() es invocado por el servidor EJB cuando precisa la creación efectiva de un nuevo objeto. No se debe dar por supuesto que un create() en el interfaz local genere de forma automática un ejbCreate() en el Bean, es decir, no ocurre siempre que la orden create() que da el cliente genera necesariamente un objeto en el contenedor. Expliquemos esto:

Diferencias:

Sobre remove(): la instancia se devuelve al pool de bean de sesión QUE GESTIONA EL SERVIDOR EJB. IMPORTANTE: la persistencia del objeto en memoria viene determinada por el servidor EJB. Por ello, la llamada a remove() no implica de forma inmediata un ejbRemove() (el borrado efectivo del objeto), ya que este borrado depende del contenedor EJB. Algo parecigo pasa con el Garbagge Collection: es la JVM la que determina cuando y como se libera la memoria del objeto, no el programador.


Algunos de los errores más comunes

¿Qué ocurre si nos equivocamos en el puerto de conexión? Tendremos una respuesta de este tipo:


javax.naming.CommunicationException: Could not obtain connection to any of these urls: 
	localhost:1199 and discovery failed with error: javax.naming.CommunicationException: 
	Receive timed out [Root exception is java.net.SocketTimeoutException: Receive timed out] 
	[Root exception is javax.naming.CommunicationException: Failed to connect to server localhost:1199 
	at org.jnp.interfaces.NamingContext.checkRef(NamingContext.java:1414)
	at org.jnp.interfaces.NamingContext.lookup(NamingContext.java:594)
	at org.jnp.interfaces.NamingContext.lookup(NamingContext.java:587)
	at javax.naming.InitialContext.lookup(Unknown Source)
	at cliente.Client.main(Client.java:18)	

¿Qué ocurre si nos olvidamos de jbossall-client.jar? Tendremos una respuesta de este tipo:


javax.naming.NoInitialContextException: Cannot instantiate class: org.jnp.interfaces.NamingContextFactory 
	[Root exception is java.lang.ClassNotFoundException: org.jnp.interfaces.NamingContextFactory]
	at javax.naming.spi.NamingManager.getInitialContext(Unknown Source)
	at javax.naming.InitialContext.getDefaultInitCtx(Unknown Source)
	at javax.naming.InitialContext.init(Unknown Source)
	at javax.naming.InitialContext.(Unknown Source)
	at cliente.Client.main(Client.java:19)
	

La respuesta es sencilla, no puede encontrar el interfaz NamingContextFactory que viene en dicho jar.



Prohibiciones

La especificación J2EE desaconseja o prohibe el uso de ordenes de sincronización de hilos (para evitar bloqueos, ya que el servidor sitúa cada petición en un hilo y sincroniza para que no sea usado por otra petición hasta que la primera ha terminado), operaciones de archivo (java.io), carga de bibliotecas nativas, etc. En vez de archivos se aconseja el uso de JDBC.




Volver al índice