- What Does a Session Bean Do?
- A "Hello World" Session Bean
- More Details About Session Beans
- Creating a Stateless Session Bean
- Troubleshooting
Creating a Session Bean That Does Some Work
The main purpose of the HelloWorldSession example was to get you familiar with the overall structure of a session bean. Writing three different Java files for a single component seems overwhelming at first, but after you're used to it, it doesn't seem like such a big deal.
Now that you're familiar with the structure of a session bean, you can write a bean that does some work. Specifically, you can write a bean that retrieves data from a database. For this example, assume that you have an SQL table containing product codes and prices, created using the following SQL statement:
create table price (product_code varchar(10) not null primary key, price decimal(10,2) not null)
The Pricing session bean gives you a list of all valid product codes and can return the price for a specific code, as specified by the Remote interface shown in Listing 6.9.
Listing 6.9 Source Code for Pricing.java
package usingj2ee.pricing; import java.rmi.*; import javax.ejb.*; /** Defines the methods you can call on a Pricing session */ public interface Pricing extends EJBObject { /** Returns all the available product codes */ public String[] getProductCodes() throws RemoteException; /** Returns the price for a specific product code */ public double getPrice(String productCode) throws RemoteException, InvalidProductCodeException; }
The Pricing session bean doesn't need to remember anything about a particular client, so it can be implemented as a stateless session bean. Thus, the PricingHome interface, shown in Listing 6.10, only needs a single create method.
Listing 6.10 Source Code for PricingHome.java
package usingj2ee.pricing; import java.rmi.*; import javax.ejb.*; /** Defines the methods for creating a Pricing session */ public interface PricingHome extends EJBHome { /** Creates a Pricing session bean */ public Pricing create() throws RemoteException, CreateException; }
When a session bean needs to access a database connection, it usually allocates the connection in the setSessionContext method and releases the connection in the ejbRemove method. Of course, if you are holding onto a database connection, you must also be prepared to close it if the container calls ejbPassivate and make the connection again when the container calls ejbActivate.
You'll find that most EJB developers create a method to return a connection; that way you can change the way you get connections without affecting the various places where you need to create one. You should also use a DataSource object to create your connections. A DataSource makes it easy to change database drivers and to use a connection pool when necessary.
Listing 6.11 shows the PricingImpl implementation class for the Pricing session bean.
Listing 6.11 Source Code for PricingImpl.java
package usingj2ee.pricing; import java.rmi.*; import java.util.*; import javax.ejb.*; import java.sql.*; import javax.sql.*; import javax.naming.*; /** The implementation class for the Pricing bean */ public class PricingImpl implements SessionBean { /** The session context provided by the EJB container. A session bean must hold on to the context it is given. */ private SessionContext context; /** The database connection used by this session */ private Connection conn; /** An EJB must have a public, parameterless constructor */ public PricingImpl() { } /** Called by the EJB container to set this session's context */ public void setSessionContext(SessionContext aContext) { context = aContext; } /** Called by the EJB container when a client calls the create() method in the Home interface */ public void ejbCreate() throws CreateException { try { // Allocate a database connection conn = getConnection(); } catch (Exception exc) { throw new CreateException( "Unable to access database: "+exc.toString()); } } /** Called by the EJB container to tell this session bean that it is being suspended from use (it's being put to sleep). */ public void ejbPassivate() throws EJBException { try { // Shut down the current database connection conn.close(); conn = null; } catch (Exception exc) { throw new EJBException("Unable to close database connection: "+ exc.toString()); } } /** Called by the EJB container to wake this session bean up after it has been put to sleep with the ejbPassivate method. */ public void ejbActivate() throws EJBException { try { // When the bean wakes back up, get a database connection again conn = getConnection(); } catch (Exception exc) { throw new EJBException( "Unable to access database: "+exc.toString()); } } /** Called by the EJB container to tell this session bean that it has been removed, either because the client invoked the remove() method or the container has timed the session out. */ public void ejbRemove() throws EJBException { try { // Shut down the current database connection conn.close(); conn = null; } catch (Exception exc) { throw new EJBException("Unable to close database connection: "+ exc.toString()); } } /** Returns a list of the available product codes */ public String[] getProductCodes() throws EJBException { Statement s = null; try { s = conn.createStatement(); ResultSet results = s.executeQuery( "select product_code from price"); Vector v = new Vector(); // Copy the results into a temporary vector while (results.next()) { v.addElement(results.getString("product_code")); } // Copy the vector into a string array String[] productCodes = new String[v.size()]; v.copyInto(productCodes); return productCodes; } catch (Exception exc) { throw new EJBException("Unable to get product codes: "+ exc.toString()); } finally { // Close down the statement in a finally block to guarantee that it gets // closed, whether an exception occurred or not try { s.close(); } catch (Exception ignore) { } } } /** Gets the price for a particular product code */ public double getPrice(String productCode) throws EJBException, InvalidProductCodeException { PreparedStatement ps = null; try { // It's always better to use a prepared statement than to try to insert // a string directly into the query string. This way you don't have to // worry if there's a quote in the product code ps = conn.prepareStatement( "select price from price where product_code = ?"); // Store the product code in the prepared statement ps.setString(1, productCode); ResultSet results = ps.executeQuery(); // If there are any results, get the first one (there should only be one) if (results.next()) { return results.getDouble("price"); } else { // Otherwise, if there were no results, this product code doesn't exist throw new InvalidProductCodeException(productCode); } } catch (SQLException exc) { throw new EJBException("Unable to get price: "+ exc.toString()); } finally { // Close down the statement in a finally block to guarantee that it gets // closed, whether an exception occurred or not try { ps.close(); } catch (Exception ignore) { } } } protected Connection getConnection() throws SQLException, NamingException { // Get a reference to the naming service InitialContext context = new InitialContext(); // Get the data source for the pricing database DataSource ds = (DataSource) context.lookup( "java:comp/env/jdbc/PriceDB"); // Ask the data source to allocate a database connection return ds.getConnection(); } }
The getConnection method in PricingImpl deserves some special attention. Notice that it uses JNDI (the naming service) to locate a data source named java:comp/env/jdbc/PriceDB. The prefix java:comp/env refers to the JNDI naming context for your session bean. When the session bean is deployed in an EJB container, the container sets up a naming context for the bean with various entries that are set up when you deploy the bean. The java:comp/env naming context lets you associate logical names with various resources. The idea is that when you write the bean, you don't need to know the exact name of a data source or Home interface to use it. When you deploy the bean into a container, you set up associations that link the names used by the bean to the actual resource name. This substantially improves the portability of the bean because it isn't tied to specific resource names.
When you deploy the Pricing bean, you must specify an alias name for jdbc/PriceDB. If you're using the Cloudscape database that comes with the J2EE SDK, this alias must be jdbc/Cloudscape. Otherwise, you must set up a data source in the EJB server that points to the database you want to work with. When you deploy the Pricing bean, you specify the name of the data source that jdbc/PriceDB refers to. Again, jdbc/PriceDB is a logical name. You can use the Pricing bean with many different databases just by changing the naming association when you deploy the bean.
If you are using a different data source, you can change it at deployment time. You can also set up a default.properties file containing information about various drivers and databases you want to use. For example, you can use the following default.properties file for an Oracle database:
jdbc.drivers=oracle.jdbc.driver.OracleDriver jdbc.datasources=jdbc/Oracle|jdbc:oracle:thin:@localhost:1521:orcl
After you set up this alternate data source, which is called jdbc/Oracle, you can change the association for jdbc/PriceDB to make it use the Oracle data source. Once again, you don't change the Pricing bean, just its deployment properties.
Assuming you're running the J2EE SDK deploy tool, you set up the jdbc/PriceDB naming entry in the Resource References section of the deploy tool, as shown here in Figure 6.8.
Figure 6.8. The Resource References dialog box lets you configure a session bean's naming context.
The only other difference between the deployment of the Pricing bean and the HelloWorldSession bean is that you must specify the alias for the jdbc/PriceDB in the JNDI Names tab panel, as shown in Figure 6.9.
Figure 6.9. The JNDI names panel lets you set up JNDI aliases for various names your bean uses.
Writing a client to test the Pricing bean is simple and the program looks similar to the other client programs you have seen. Listing 6.12 shows the pricing test client.
Listing 6.12 Source Code for TestPricing.java
package usingj2ee.pricing; import java.util.*; import javax.naming.*; import javax.rmi.*; public class TestPricing { public static void main(String[] args) { try { /** Creates a JNDI naming context for location objects */ Context context = new InitialContext(); /** Asks the context to locate an object named "Pricing" and expects the object to implement the PricingHome interface */ PricingHome home = (PricingHome) PortableRemoteObject.narrow( context.lookup("Pricing"), PricingHome.class); /** Asks the Home interface to create a new session bean */ Pricing session = (Pricing) home.create(); /** Get a list of valid product codes */ String[] codes = session.getProductCodes(); for (int i=0; i < codes.length; i++) { System.out.println(codes[i]+": "+ session.getPrice(codes[i])); } try { session.getPrice("f00b4r"); } catch (InvalidProductCodeException exc) { System.out.println("Got invalid product code exception: "+ exc.toString()); } /** Destroy this session */ session.remove(); } catch (Exception exc) { exc.printStackTrace(); } } }
Finally, Figure 6.10 shows the output from the pricing test client. Notice that nowhere in the source code or the output does the client have any idea that the bean is getting its data from a database.
Figure 6.10. The client doesn't know that the session bean gets its data from a database.
Note
The makeprices.sql script included on the CD-ROM contains insert commands to populate the pricing database.
INSERT INTO price (product_code, price) VALUES ('A1', 1.59);
Now that you've started with session beans, Chapter 7, "Creating an Entity Bean," introduces you to the other important EJB: the entity bean. Chapter 8 then shows you how transactions fit in to the EJB world.