EJB Tutor Page



  

State Design

Continuing on with the Forethought business logic,
I want to spend some time on the issue of stateful versus stateless beans.
I refer to it as an issue because it almost always manages to come up when working with session beans,
and can have a drastic effect on your application's performance.
Specifically, stateless session beans are much more efficient than stateful session beans.
For a more detailed discussion on the issue, you can check out Richard Monson-Haefel's Enterprise JavaBeans,
which spends a great deal of print on how a container handles these two kinds of beans.
However, I will briefly sum up the relevant portions here.
A stateless session bean is a very lightweight bean, as it needs to carry around only EJB-mandated variables, and not programmer-defined ones.
As a result, most containers have pools of stateless beans.
Because no single client needs to have access to a specific stateless bean instance (no state is being kept, remember),
a single instance can serve two, three, ten, or even a hundred clients.
This allows the bean pool to be kept small, and negates a frequent need to grow or shrink the pool, which would take valuable processor cycles.
A stateful bean is just the opposite: an instance is tied to the client that invoked its create( ) method.
This means that an instance must exist for every client accessing the stateful bean.
Therefore, the bean pools must be larger, or must frequently be grown as more requests come in.
The end result is longer process times, more beans, and fewer clients being served.
The moral of this technology tale is that if at all possible, you should use stateless session beans.
The following sections demonstrate these principles,
and specifically how a bean that appears to be a better stateful bean can easily be converted into a stateless one.
This should provide you with some good ideas about state design and some handy tips on how to convert your own stateful beans into stateless ones.
Starting Stateful
Take the case of an AccountManager bean that will handle a single user's accounts.
For this exercise, I will keep the methods required for the bean simple.
The AccountManager Remote Interface

package com.forethought.ejb.account;
 
import java.rmi.RemoteException;
import java.util.List;
import javax.ejb.EJBObject;
 
import com.forethought.ejb.account.AccountInfo;						// Account bean
import com.forethought.ejb.accountType.UnknownAccountTypeException;	// AccountType bean
	 
public interface AccountManager extends EJBObject 
{
    public AccountInfo add( String type, float balance ) throws RemoteException, UnknownAccountTypeException;
    public AccountInfo get( int accountId ) throws RemoteException;
    public List        getAll() throws RemoteException;
    public AccountInfo deposit(  AccountInfo accountInfo, float amount ) throws RemoteException;
    public AccountInfo withdraw( AccountInfo accountInfo, float amount ) throws RemoteException;
    public float       getBalance( int accountId ) throws RemoteException;
    public boolean     delete( int accountId ) throws RemoteException;  
}

As you can see, the manager operates upon a single account for a single user.
This allows a client to simply pass in the user's username one time (using the create( ) method),
and worry about details of the account independently of keeping up with a username. The AccountManager Home Interface shows this method in the manager's home interface.

The AccountManager Home Interface

package com.forethought.ejb.account;
 
import java.rmi.RemoteException;
import javax.ejb.CreateException;
import javax.ejb.EJBHome;
 
public interface AccountManagerHome extends EJBHome 
{
    public AccountManager create( String username ) throws CreateException, RemoteException;
}

This provides a means to create a new account or to find all existing accounts for a given username.
Because the bean is keeping up with the account's user and the account's ID, both required for entity bean interaction, the bean must be stateful.
That is, since each individual method uses the data, this information must be kept in the bean instance between requests.
For a better understanding of these details, review the bean's implementation code in The AccountManager Implementation Class

The AccountManager Implementation Class


package com.forethought.ejb.account;
 
import java.rmi.RemoteException;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import javax.ejb.CreateException;
import javax.ejb.FinderException;
import javax.ejb.EJBHome;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
 
import com.forethought.ejb.util.SessionAdapter;
 
// Account bean
import com.forethought.ejb.account.Account;
import com.forethought.ejb.account.AccountHome;
import com.forethought.ejb.account.AccountInfo;
 
// AccountType bean
import com.forethought.ejb.accountType.UnknownAccountTypeException;
 
// User bean
import com.forethought.ejb.user.User;
import com.forethought.ejb.user.UserHome;
 
// LDAPManager (for utility method)
import com.forethought.ldap.LDAPManager;
 
public class AccountManagerBean extends SessionAdapter 
{
    private String username;	    /** Username related to this account */
    private User user;				/** User bean for this account's user */
    
    /** Required method for allowing bean lookups. */
    public void ejbCreate(String username) throws CreateException, RemoteException 
	{
        this.username = username;
        
        try 
		{
            Context context = new InitialContext();	// Get an InitialContext
 
            // Look up the Account bean
            UserHome userHome = (UserHome ) context.lookup("java:comp/env/ejb/UserHome");
            this.user = userHome.findByUserDn(LDAPManager.getUserDN(username));
        } 
		catch (NamingException e) 
		{
            throw new CreateException("Could not load underlying User bean.");
        } 
		catch (FinderException e) 
		{
            throw new CreateException("Could not locate specified user.");
        }
    }
    
    public AccountInfo add(String type, float balance) throws UnknownAccountTypeException 
	{
        try 
		{
            // Get an InitialContext
            Context context = new InitialContext();
 
            // Look up the Account bean
            AccountHome accountHome = (AccountHome ) context.lookup("java:comp/env/ejb/AccountHome");
            Account account = accountHome.create(type, balance, user);
            return account.getInfo();
        } 
		catch (RemoteException e) 
		{
            return null;
        } 
		catch (CreateException e) 
		{
            return null;
        } 
		catch (NamingException e) 
		{
            return null;
        }
    }
 
    public AccountInfo get(int accountId) throws RemoteException 
	{
        return getAccount(accountId).getInfo();
    }
 
	public List getAll() 
	{
		List accounts = new LinkedList();

		try 
		{
			Integer userId = user.getId();

			// Get an InitialContext
			Context context = new InitialContext();

			// Look up the Account bean
			AccountHome accountHome = (AccountHome) 
			context.lookup("java:comp/env/ejb/AccountHome");
			Collection userAccounts = accountHome.findByUserId(userId);
			for (Iterator i = userAccounts.iterator(); i.hasNext(); ) 
			{
				Account account = (Account)i.next();
				accounts.add(account.getInfo());
			}
		} 
		catch (Exception e) 
		{
			// Let fall through to the return statement
		}
		return accounts;
	}
 
    public AccountInfo deposit(AccountInfo accountInfo, float amount) throws RemoteException 
	{
        // Look up bean, to ensure most current view of data
        Account     account = getAccount(accountInfo.getId());
        AccountInfo info    = account.getInfo();
        
        // Update balance
        info.setBalance(info.getBalance() + amount);
        try 
		{
            account.setInfo(info);
        } 
		catch (UnknownAccountTypeException neverHappens) 
		{ 
		}
        
		return info;
    }
 
    public AccountInfo withdraw(AccountInfo accountInfo, float amount) throws RemoteException 
	{
        // Look up bean, to ensure most current view of data
        Account     account = getAccount(accountInfo.getId());
        AccountInfo info    = account.getInfo();
        
        // Update balance
        info.setBalance(info.getBalance() - amount);
        try 
		{
            account.setInfo(info);
        } 
		catch (UnknownAccountTypeException neverHappens) 
		{ 
		}

        return info;
    }
    
    public boolean delete(int accountId) 
	{
        try 
		{
            Account account = getAccount(accountId);
            account.remove();
            return true;
        } 
		catch (Exception e) 
		{
            return false;
        }
    }
 
    public float getBalance(int accountId) throws RemoteException 
	{
        return getAccount(accountId).getBalance();
    }
 
    private Account getAccount(int id) 
	{
		try
		{
            Context context = new InitialContext();		// Get an InitialContext
 
            // Look up the Account bean
            AccountHome accountHome = (AccountHome) context.lookup("java:comp/env/ejb/AccountHome");
            Account account = accountHome.findByPrimaryKey(new Integer(id));
            
            return account;
        } 
		catch (Exception e) 
		{
            // Any problems - just return null
            return null;
        }
    }
}

This is all basic EJB material, and should not cause you any problems.
You will notice that this class also uses a new finder method on the Account bean:

public Collection findByUserId(Integer userId) throws FinderException, RemoteException;

The accompanying query element in the Account bean's entry in the ejb-jar.xml descriptor would look like this:

<query>
<query-method>
<method-name>findByUserId</method-name>
<method-params>
<method-param>java.lang.Integer</method-param>
</method-params>
</query-method>
<ejb-ql>
<![CDATA[WHERE userLocal.id = ?1]]>
</ejb-ql>
</query>

To deploy the AccountManager bean, you would use this (additional) XML entry in your ejb-jar.xml deployment descriptor:
<session>
<description>
This AccountManager bean allows administration of Forethought accounts.
</description>
<ejb-name>AccountManagerBean</ejb-name>
<home>com.forethought.ejb.account.AccountManagerHome</home>
<remote>com.forethought.ejb.account.AccountManager</remote>
<ejb-class>com.forethought.ejb.account.AccountManagerBean</ejb-class>
<session-type>Stateful</session-type>
<transaction-type>Container</transaction-type>
<ejb-ref>
<ejb-ref-name>ejb/AccountHome</ejb-ref-name>
<ejb-ref-type>Entity</ejb-ref-type>
<home>com.forethought.ejb.account.AccountHome</home>
<remote>com.forethought.ejb.account.Account</remote>
<ejb-link>AccountBean</ejb-link>
</ejb-ref>
</session>

Additions to your application server's vendor-specific descriptors should be equally simple.
With this bean in stateful form ready for use, it is time to see how it can be turned into a better-performing stateless session bean.
Going Stateless
To move this bean into stateless territory, you first need to change the home interface's create() signature.
Since stateless beans cannot maintain any information between method calls,
passing in a username (or any other data) to the create( ) method is useless.
Make the following change:

public AccountManager create() throws CreateException, RemoteException;

Once this is done, you will need to determine which methods advertised by the bean require a username for operation.
In other words, browse through your bean's implementation class and note any method that uses the username or user method variable.
Once you have determined the methods in this category, you will need to change the signature for those methods in the remote interface:

public interface AccountManager extends EJBObject 
{
    public AccountInfo add( String username, String type, float balance ) throws RemoteException, UnknownAccountTypeException;
    public AccountInfo get( int accountId ) throws RemoteException;
    public List        getAll( String username ) throws RemoteException;
    public AccountInfo deposit(  AccountInfo accountInfo, float amount ) throws RemoteException;
    public AccountInfo withdraw( AccountInfo accountInfo, float amount ) throws RemoteException;
    public float       getBalance( int accountId ) throws RemoteException;
    public boolean     delete( int accountId ) throws RemoteException;  
}

In this case, only two methods require this information, so it is not terribly inconvenient.
However, in many cases conversion from stateful to stateless requires a parameter to be added to ten, twenty, or more methods.
Even though this example is somewhat trivial, I want to continue the discussion assuming that it is a major issue to
have to keep the username around for these multiple method calls.
Before getting to the solution, though, you'll need to update your bean implementation class to operate without maintaining state.
First, add a utility method to the end of the class:

private User getUser(String username) throws RemoteException 
{
    try 
	{
        Context context = new InitialContext();		// Get an InitialContext
 
        // Look up the Account bean
        UserHome userHome = (UserHome) context.lookup("java:comp/env/ejb/UserHome");
        User user = userHome.findByUserDn(LDAPManager.getUserDN(username));
    
	    return user;
    } 
	catch (NamingException e) 
	{
        throw new RemoteException("Could not load underlying User bean.");
    } 
	catch (FinderException e) 
	{
        throw new RemoteException("Could not locate specified user.");
    }
}
Then remove the username and user member variables, and modify three methods (those affected by the change to stateless):
public void ejbCreate() throws CreateException 
{
    // Nothing to be done for stateless beans
}
 
public AccountInfo add(String username, String type, float balance) throws UnknownAccountTypeException 
{
    try 
	{
        Context context = new InitialContext();			// Get an InitialContext
        User user = getUser(username);					// Get the correct user
 
        // Look up the Account bean
        AccountHome accountHome = (AccountHome) context.lookup("java:comp/env/ejb/AccountHome");
        Account account = accountHome.create(type, balance, user);
        
		return account.getInfo();
    } 
	catch (RemoteException e) 
	{
        return null;
    } 
	catch (CreateException e) 
	{
        return null;
    } 
	catch (NamingException e) 
	{
        return null;
    }
}
 
public List getAll(String username) 
{
    List accounts = new LinkedList();
    
    try 
	{
        User user = getUser(username);
        Integer userId = user.getId();
        
        // Get an InitialContext
        Context context = new InitialContext();
 
        // Look up the Account bean
        AccountHome accountHome = (AccountHome) 
            context.lookup("java:comp/env/ejb/AccountHome");
        Collection userAccounts = accountHome.findByUserId(userId);
        for (Iterator i = userAccounts.iterator(); i.hasNext(); ) 
		{
            Account account = (Account)i.next();
            accounts.add(account.getInfo());
        }
    } 
	catch (Exception e) 
	{
        // Let fall through to the return statement
    }
    
	return accounts;
}
Finally, don't forget to change your deployment descriptor:
<session>
<description>
This AccountManager bean allows administration of Forethought accounts.
</description>
<ejb-name>AccountManagerBean</ejb-name>
<home>com.forethought.ejb.account.AccountManagerHome</home>
<remote>com.forethought.ejb.account.AccountManager</remote>
<ejb-class>com.forethought.ejb.account.AccountManagerBean</ejb-class>
<session-type>Stateless</session-type>
<transaction-type>Container</transaction-type>
<ejb-ref>
<ejb-ref-name>ejb/AccountHome</ejb-ref-name>
<ejb-ref-type>Entity</ejb-ref-type>
<home>com.forethought.ejb.account.AccountHome</home>
<remote>com.forethought.ejb.account.Account</remote>
<ejb-link>AccountBean</ejb-link>
</ejb-ref>
<ejb-ref>
<ejb-ref-name>ejb/UserHome</ejb-ref-name>
<ejb-ref-type>Entity</ejb-ref-type>
<home>com.forethought.ejb.user.UserHome</home>
<remote>com.forethought.ejb.user.User</remote>
<ejb-link>UserBean</ejb-link>
</ejb-ref>
</session>

All things considered, these are relatively simple changes to make, and have the net effect of making your bean faster, more efficient,
and only marginally harder to use.
However, as I mentioned, there are times when the changes to the bean's remote interface are more difficult.
Passing in a username or any other piece of data ten, twenty, or more times to a bean's methods can result in pain for the developer, and less-clear code.
In these cases, a simple helper class on the client can make a stateless session bean behave just as a stateful one did.
shows this principle in action, detailing the AccountManagerHelper utility class.

An AccountManager Helper Class

package com.forethought.client;
 
import java.rmi.RemoteException;
import java.util.List;
import javax.ejb.CreateException;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.rmi.PortableRemoteObject;
 
// Account bean
import com.forethought.ejb.account.AccountInfo;
import com.forethought.ejb.account.AccountManager;
import com.forethought.ejb.account.AccountManagerHome;
 
// AccountType bean
import com.forethought.ejb.accountType.UnknownAccountTypeException;
 
public class AccountManagerHelper 
{
    private String username;			/** The username for this account's user */
    private AccountManager manager;		/** The AccountManager bean instance */
 
    public AccountManagerHelper(String username) throws CreateException, NamingException, RemoteException 
	{
        this.username = username;
        
        Context context = new InitialContext();
        
        // Get the stateless bean instance
        Object ref = context.lookup("forethought.AccountManagerHome");
        AccountManagerHome accountManagerHome = (AccountManagerHome) PortableRemoteObject.narrow(ref, AccountManagerHome.class);
        this.manager = accountManagerHome.create();
    }
 
    public AccountInfo add(String type, float balance) throws RemoteException, UnknownAccountTypeException 
	{
        return manager.add(username, type, balance);
    }
 
    public AccountInfo get(int accountId) throws RemoteException 
	{
        return manager.get(accountId);
    }
 
    public List getAll() throws RemoteException 
	{
        return manager.getAll(username);
    }
 
    public AccountInfo deposit(AccountInfo accountInfo, float amount) throws RemoteException 
	{
        return manager.deposit(accountInfo, amount);
    }
 
    public AccountInfo withdraw(AccountInfo accountInfo, float amount) throws RemoteException 
	{
        return manager.withdraw(accountInfo, amount);
    }
 
    public boolean delete(int accountId) throws RemoteException 
	{
        return manager.delete(accountId);
    }
 
    public float getBalance(int accountId) throws RemoteException 
	{
        return manager.getBalance(accountId);
    }
}

Looking at the methods available on this helper class, you should realize pretty quickly that it mirrors the remote interface of the AccountManager session bean;
however, it looks like the stateful bean version, rather than the new stateless version.
The constructor for the class then takes the place of the old stateful bean's create() method from the home interface.
This class then maintains a bean instance, the username for the manager, and delegates to the session bean.
All of the same exceptions are passed through to the client, so the interface is very similar;
the only difference is that context lookups are handled within the helper class.
This makes the client code even simpler, as this code fragment shows:

// Look up the AccountManager bean
	System.out.println("Looking up the AccountManager bean.");
	AccountManagerHelper accountHelper = new AccountManagerHelper("gqg10012");
    
// Create an account
	AccountInfo everydayAccount = accountHelper.add("Everyday", 5000);
	if (everydayAccount == null) 
	{
		System.out.println("Failed to add account.\n");
		return;
	}
	System.out.println("Added account.\n");
 
// Get all accounts
	List accounts = accountHelper.getAll();
	for (Iterator  i = accounts.iterator(); i.hasNext(); ) 
	{
		AccountInfo accountInfo = (AccountInfo)i.next();
		System.out.println("Account ID: " + accountInfo.getId());
		System.out.println("Account Type: " + accountInfo.getType());
		System.out.println("Account Balance: " + accountInfo.getBalance() + "\n");
	}
 
// Deposit
	accountHelper.deposit(everydayAccount, 2700);
	System.out.println("New balance in everyday account: " + accountHelper.getBalance(everydayAccount.getId()) + "\n");
    
// Withdraw
	accountHelper.withdraw(everydayAccount, 500);
	System.out.println("New balance in everyday account: " + accountHelper.getBalance(everydayAccount.getId()) + "\n");
    
// Delete account
	accountHelper.delete(everydayAccount.getId());
	System.out.println("Deleted everyday account.");

You may find that helper classes like this can simplify your own client code, even if you do not need to provide stateful session bean masquerading,
where a stateless bean is made to look like a stateful one.
In any case, this approach provides the best of both session bean types: the performance of a stateless bean with the interface of a stateful one.
This technique will allow you to convert all of your application's stateful session beans into stateless ones,
which will yield some dramatic performance improvements.
You now have the tools to build the back-end of almost any enterprise application you may come across,
and apply your knowledge to most of the problems you will encounter in the enterprise Java space.



Glossary   Option Terms   Derivatives   CS Complex Swaps   CS Compound Options   CS Contingent Premium Options   CS Credit Derivatives   CS Derivative-linked securities   CS Digital options   CS Forwards   CS Multi-factor options   CS Vanilla Options   CS Vanilla Swaps   FX Forwards   Quick Java  EJB Tutor  Java Jar: Derivative Pricing