ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


O'Reilly Book Excerpts: Building Java Enterprise Applications, Vol I: Architecture

Business Logic, Part 3

Related Reading

Building Java Enterprise Applications
By Brett McLaughlin

by Brett McLaughlin

This is the final part of an extended excerpt from Chapter 8 of Building Java Enterprise Applications, Vol I: Architecture. This installment focuses on issues of statefulness and statelessness. The second article looked at the UserManager component. The first installment covered the facade pattern, a way to use session beans to access entity beans, and introduced the officeManager component.

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'll 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.

Also in this series:

Business Logic, Part 2
In Part 2 of our excerpt from Chapter 8 of Building Java Enterprise Applications, Vol I: Architecture, Brett McLaughlin builds a UserManager component, and illustrates why managers are a good thing.

Business Logic, Part 1
In this excerpt from Chapter 8 of Building Java Enterprise Applications, Vol I: Architecture, Brett McLaughlin discusses the fašade pattern, in which you use session beans to access entity beans. This access method is used instead of allowing direct access to entity beans, and is key to a sound strategy in building enterprise applications.

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'll keep the methods required for the bean simple. Example 8-7 shows the remote interface for this bean.



Example 8-7: The AccountManager Remote Interface

package com.forethought.ejb.account;
 
import java.rmi.RemoteException;
import java.util.List;
import javax.ejb.EJBObject;
 
// Account bean
import com.forethought.ejb.account.AccountInfo;
 
// AccountType bean
import com.forethought.ejb.accountType.UnknownAccountTypeException;
 
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. Example 8-8 shows this method in the manager's home interface.


Example 8-8: 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 Example 8-9 on the next page.


Example 8-9: 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 {
    
    /** Username related to this account */
    private String username;
    
    /** User bean for this account's user */
    private User user;
    
    /** <p> Required method for allowing bean lookups. </p> */
    public void ejbCreate(String username) 
        throws CreateException, RemoteException {
            
        this.username = username;
        
        try {
            // Get an InitialContext
            Context context = new 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) {
	
            // 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.findByPrimaryKey(new Integer(id));
            
            return account;
        } catch (Exception e) {
            // Any problems - just return null
            return null;
        }
    }
}

This is all basic EJB material, and shouldn't cause you any problems. You'll 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's 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 can't 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've 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's 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 {
        // Get an InitialContext
        Context context = new 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 {
        // Get an InitialContext
        Context context = new InitialContext(  );
        
        // Get the correct user
        User user = getUser(username);
 
        // 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. Example 8-10 shows this principle in action, detailing the AccountManagerHelper utility class.


Example 8-10: 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 {
    
    /** The username for this account's user */
    private String username;
    
    /** The <code>AccountManager</code> bean instance */
    private AccountManager manager;
 
    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 don't 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.

What's Next?

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. In the next chapter, though, I want to move beyond the basics into the less-used realm of the Java Message Service (and specifically, message-driven beans). Although it is still somewhat unusual to see these kinds of beans in action, you will find that JMS offers several attractive features. I'll detail these and how they can help in asynchronous tasks in the next chapter, which focuses specifically on messaging in enterprise applications.


Brett McLaughlin had developed enterprise Java applications for Nextel Communications and Allegiance Telecom. When that became fairly mundane, Brett took on application servers. He then got hooked on open source software, and helped found several cool programming tools, like Jakarta turbine and JDOM.


View catalog information for Building Java Enterprise Applications Vol I: Architecture

Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.