Beans de sesión

(Enero de 2006)

Introducción

Ya sabemos que el bean de sesión se mantiene a lo largo de la sesión del cliente con la finalidad de dar representar flujo de trabajo, estado de aplicación, lógica y utilidades de empresa. Un bean de sesión con estado puede mantener datos entre las diferentes peticiones de un cliente; pero no ocurre así con un bean de sesión sin estado.


Con o sin estado

La limitación inherente a los beans sin estado nos puede hacer creer que sean de escasa utilidad. Para una primera aproximación a estos bean hagamos una diferencia:

Veámoslo desde el punto de vista del código. En la implamentación del bean tenemos el método ejbCreate():


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

Además en el cliente creamos dos EJBs:


    try {

        //// Creo un contexto
    	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);
        
        /// Creo una referencia a objeto remoto
        Object objRef = ctx.lookup("ejb/Calculador");
        CalculadorHome home = (CalculadorHome)javax.rmi.PortableRemoteObject.narrow(objRef, CalculadorHome.class);
        Calculador cal = home.create();

        //// ... Uso el EJB
        
        /**************************************************************************
         * Liberamos el objeto: la instancia se devuelve al pool de bean de sesión.
         * IMPORTANTE: la persistencia del objeto en memoría viene determinada por 
         * el servidor EJB.
         **************************************************************************/
        cal.remove();
        
        /**************************************************************************
         * Creamos una segunda referencia
         **************************************************************************/
        Calculador cal2 = home.create();

        //// ... Uso el EJB
        
        cal2.remove();
        ctx.close();
        
    } catch (Exception e){
    	e.printStackTrace();
    }
	

La pregunta es: ¿Qué diferencias existen en función de si el bean mantiene o no el estado?. Para la respuesta veamos el comportamiento de cada caso:

Dicho de forma abstracta, estos tipos de beans tienen diferentes tipos de ciclos de vida.



Sin estado, pero ¿qué ocurre con los atributos?

El hecho de que no tenga estado no significa que no pueda tener variables atributos. Pero hay un aspecto que no debemos olvidar: es el servidor EJB el que se interpone y controla la vida del bean sin estado. Por tanto, no debería asumir nada sobre los valores de esas variables atributos. Como se ha podido ver en el ejemplo anterior, el servidor mantiene el primer bean en reserva para activarlo en la segunda petición del cliente, esto implica que se mantienen los valores de cualquier variable atributo. Por tanto, estas variables se comportan como variables globales (lo que puede llevar a efectos laterales indeseados)



Con estado, pero ¿qué ocurre con el rendimiento?

En el ejemplo anterior se puede deducir que los bean de sesión con estado pueden resultar un mecanismo excesivamente costoso desde el punto de vista del rendimiento. Afortunadamente los servidores EJB tienen mecanismos de optimización. El más conocido es semejante al funcionamiento del swapping de memoria. Para los que lo hayan olvidado y por decirlo brevemente, el swapping funciona así: si una aplicación (o parte de ella) no se está usando, se sitúa en un espacio secundario de almacenamiento temporal (disco). En el momento que el procesador la necesita se restaura (activa) desde este espacio temporal.

De manera semejante, el servidor EJB puede almacenar en un espacio temporal los beans de sesión con estado. A este mecanismo se le llama pasivación. La inversa (restaurar en memoria de trabajo el bean desde el almacenamiento temporal) se conoce como activación. El algoritmo que controla ambos mecanismos no pertenece al standar J2EE, es específico al fabricante. El servidor invocará el correspondiente método del bean como una forma de aviso del cambio de estado (la implementación es obligada pero pueden estar vacios):


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

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

El contenedor EJB no activa ni pasiviza beans de sesión sin estado.



Elegir

Llegado a este punto uno puede seguir preguntándose cuando utilizar beans de sesión con estado o sin estado, más aún, cuando usar beans de entidad o beans de sesión. No hay normas inquebrantables, aunque si hay orientaciones generales:



Ejemplo de bean de sesión sin estado. Un decisor de rentabilidad aceptable

Supongamos un sencillo ejemplo, necesitamos un EJB que es informado del precio y coste de una transacción económica (una venta) y nos dice las ventas que implican una rentabilidad aceptable. La implementación del bean (CalculadorBean.java) sería:


package calculador;
import javax.ejb.*;

/**********************************************************************************
 * Calcula si una venta es financieramente aceptable. 
 * EJB de sesión sin estado.
 * Debe definir un ejbCreate() coincidente (en args) por cada create() 
 *********************************************************************************/
public class CalculadorBean implements javax.ejb.SessionBean {

	/******************************************************************************
	 * Devuelve true si el precio supera en un 50% al coste. Si no, devuelve false.     
	 *****************************************************************************/
	public boolean esAceptable( double precio, double coste) {
		if ( precio > coste * 1.5 )
			return true;
		return false;
	}
	
	public void ejbCreate() {
		System.out.println("--->Llamada a ejbCreate() de " + getClass().getName());
	}

	/******************************************************************************
	* Métodos obligatorios (están en el interface SessionBean), pero pueden estar vacios.
	*****************************************************************************/
	public void ejbRemove() {
		System.out.println("--->Llamada a ejbRemove() de " + getClass().getName());
	}

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

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

	public void setSessionContext(SessionContext ctx){
		System.out.println("--->Llamada a setSessionContext() de " + getClass().getName());
		System.out.println("--->"+ctx.getEJBObject().getClass().getName());
	}
}
	

Respecto a los métodos ejbXXXXX(), conviene decir que son invocados por el servidor y están definidos en el interface SessionBean (salvo ejbCreate()):

Una vez que el contenedor precisa de un nuevo bean el orden de llamadas es:

  1. setSessionContext
  2. ejbCreate
  3. A partir de este momento el bean se situa en reserva y dispuesto para ser utilizado por el cliente.

El interfaz local:


	package calculador;
	public interface CalculadorHome extends javax.ejb.EJBHome {
		Calculador create() throws java.rmi.RemoteException, javax.ejb.CreateException;
	}
	

El interfaz remoto:


	package calculador;
	public interface Calculador extends javax.ejb.EJBObject {
		boolean esAceptable( double precio, double coste) throws java.rmi.RemoteException;
	}
	

El descriptor ejb-jar.xml:


<!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>Calculador</ejb-name>
      <home>calculador.CalculadorHome</home>
      <remote>calculador.Calculador</remote>
      <ejb-class>calculador.CalculadorBean</ejb-class>
      <session-type>Stateless</session-type>
      <transaction-type>Container</transaction-type>
   </session>
 </enterprise-beans>

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

El descriptor jboss.xml:


<?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>Calculador</ejb-name>
			<jndi-name>ejb/Calculador</jndi-name>
		</session>
	</enterprise-beans>
</jboss>
	

El cliente:


package cliente;

import calculador.*;
import java.util.Properties;
import javax.naming.Context;
import javax.naming.InitialContext;

/**************************************************************************************
 * Este cliente EJB requiere (si usa JBOSS): jbossall-client.jar 
 *************************************************************************************/
public class Cliente {
  public static void main(String[] args) {
    try {

        //// Creo un contexto
    	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);
        
        //// Creo una referencia a objeto remoto
        Object objRef = ctx.lookup("ejb/Calculador");
        CalculadorHome home = (CalculadorHome)javax.rmi.PortableRemoteObject.narrow(objRef, CalculadorHome.class);
        Calculador cal = home.create();

        //// Datos de entrada para el EJB, cada venta tiene su precio y coste)
        double precio = 33, coste = 29;
      	System.out.println( "\nVenta. Precio: " + precio + " Coste: " + coste);
      	System.out.println( "Venta aceptable: " + cal.esAceptable( precio, coste));
        
        
        /**************************************************************************
         * Liberamos el objeto: la instancia se devuelve al pool de bean de sesión.
         * IMPORTANTE: la persistencia del objeto en meoría viene determinada por 
         * el servidor EJB.
         **************************************************************************/
        cal.remove();
        
        ctx.close();
        
    } catch (Exception e){
    	e.printStackTrace();
    }
  }
}

	


Bean de sesión con estado

Partimos del ejemplo anterior (CalculadorBean.java). Ya que vamos a hacer que tenga estado, extendemos su funcionalidad: tendremos un atributo que nos dirá cuál es la venta con mayor precio a lo largo de la sesión con el cliente. Otro atributo es el código de cliente:


package calculador;
import javax.ejb.*;

/**********************************************************************************
 * Calcula si una venta es financieramente aceptable y el precio máximo durante la sesión. 
 * EJB de sesión con estado.
 *********************************************************************************/
public class CalculadorBean implements javax.ejb.SessionBean {

	private double maximo;  // Precio máximo durante la sesión
	private String codigo;	// Código del cliente
	
	/******************************************************************************
	 * Devuelve true si el precio supera en un 50% al coste. Si no, devuelve false.     
	 *****************************************************************************/
	public boolean esAceptable( double precio, double coste) {
		if ( precio > coste * 1.5 )
			return true;
		return false;
	}
	
	/******************************************************************************
	 * Devuelve true si el argumento es mayor que el máximo; si no, devuelve false.     
	 *****************************************************************************/
	public boolean esMayorQueMaximo( double precio ) {
		if ( precio > maximo )
			return true;			
		return false;
	}

	/******************************************************************************
	 * Si el argumento es mayor que el máximo, actualiza máximo.     
	 *****************************************************************************/
	public void setMaximo( double precio ) {
		if ( precio > maximo )
			maximo = precio;
	}

	public double getMaximo() {
		return maximo;
	}
	public String getCodigo() {
		return codigo;
	}

	public void ejbCreate( String codigo ) {
		System.out.println("--->Llamada a ejbCreate() de " + getClass().getName() + " con código " + codigo);
		this.codigo = codigo;
		maximo = 0;
	}

	/******************************************************************************
	 * Métodos obligatorios (están en el interface SessionBean), pero pueden estar vacios.
	 *****************************************************************************/
	public void ejbRemove() {
		System.out.println("--->Llamada a ejbRemove() de " + getClass().getName());
	}

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

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

	public void setSessionContext(SessionContext ctx){
		System.out.println("--->Llamada a setSessionContext() de " + getClass().getName());
		System.out.println("--->"+ctx.getEJBObject().getClass().getName());
	}
}
	

Como estamos ante un bean con estado podemos utilizar un ejbCreate() con parámetros. Además debemos hacer que create() del interfaz local sea coincidente con ejbCreate(). El interfaz local:


package calculador;

/*********************************************************************************
 * 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.
 * Sin estado: debe definir al un create() sin parámetros.
 * Con estado: puede haber uno o varios create() con parámetros. 
 *********************************************************************************/
public interface CalculadorHome extends javax.ejb.EJBHome {
	Calculador create( String codigo ) throws java.rmi.RemoteException, javax.ejb.CreateException;
}
	

En el interfaz remoto deben aparecer los nuevos métodos:


package calculador;

public interface Calculador extends javax.ejb.EJBObject {
	boolean esAceptable( double precio, double coste) throws java.rmi.RemoteException;
	boolean esMayorQueMaximo( double precio ) throws java.rmi.RemoteException;
	void setMaximo( double precio ) throws java.rmi.RemoteException;	
	double getMaximo() throws java.rmi.RemoteException;
	String getCodigo() throws java.rmi.RemoteException;
}
	

El descriptor jboss.xml no cambia. En ejb-jar.xml hemos cambiado tan sólo:


      <session-type>Stateful</session-type>
	

En el cliente hemos usado un bean para evaluar dos ventas:


package cliente;

import calculador.*;
import java.util.Properties;
import javax.naming.Context;
import javax.naming.InitialContext;

/**************************************************************************************
 * Este cliente EJB requiere (si usa JBOSS): jbossall-client.jar 
 *************************************************************************************/
public class Cliente {
	public static void main(String[] args) {
		try {
	
			//// Creo un contexto
			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);
	        
			//// Creo una referencia a objeto remoto
			Object objRef = ctx.lookup("ejb/Calculador");
			CalculadorHome home = (CalculadorHome)javax.rmi.PortableRemoteObject.narrow(objRef, CalculadorHome.class);
			Calculador cal = home.create( "Pedro" );
	
			gestionVenta( cal, 33, 29 ); // Primera venta
			gestionVenta( cal, 29, 12 ); // Segunda venta
	        
			cal.remove();
			ctx.close();
	        
		} catch (Exception e){
			e.printStackTrace();
		}
	}
  
	static void gestionVenta( Calculador cal, double precio, double coste) {
		try {
			System.out.println( "\nVenta al cliente " + cal.getCodigo() + ". Precio: " + precio + " Coste: " + coste);
			System.out.println( "Venta aceptable: " + cal.esAceptable( precio, coste));
			System.out.println( "El precio maximo es: " + cal.getMaximo() );
			System.out.println( "El precio " + precio + " es mayor que el maximo: " + cal.esMayorQueMaximo(precio));
			cal.setMaximo(precio);
			System.out.println( "El precio maximo es: " + cal.getMaximo() );
		} catch (Exception e){
			e.printStackTrace();
		}  
	}
}
	

El resultado de la ejecución es:


Venta al cliente Pedro. Precio: 33.0 Coste: 29.0
Venta aceptable: false
El precio maximo es: 0.0
El precio 33.0 es mayor que el maximo: true
El precio maximo es: 33.0

Venta al cliente Pedro. Precio: 29.0 Coste: 12.0
Venta aceptable: true
El precio maximo es: 33.0
El precio 29.0 es mayor que el maximo: false
El precio maximo es: 33.0
	



Volver al índice