The Stateless Session Bean
The Life Cycle of a Stateless Session Bean
The Stateful Session Bean
The Life Cycle of a Stateful Session Bean
As you learned in Chapter 6, "Entity Beans", entity beans provide an object-oriented interface that makes it easier for developers to create, modify, and delete data from the database. Entity beans make developers more productive by encouraging reuse and by reducing development costs. A concept like a Ship can be reused throughout a business system without having to redefine, recode, or retest the business logic and data access defined by the Ship bean.
However, entity beans are not the entire story. We have also seen another kind of enterprise bean: the session bean. Session beans fill the gaps left by entity beans. They are useful for describing interactions between other beans (workflow) or for implementing particular tasks. Unlike entity beans, session beans don't represent shared data in the database, but they can access shared data. This means that we can use session beans to read, update, and insert data. For example, we might use a session bean to provide lists of information, such as a list of all available cabins. Sometimes we might generate the list by interacting with entity beans, like the cabin list we developed in the TravelAgent bean in Chapter 4, "Developing Your First Enterprise Beans". More frequently, session beans will generate lists by accessing the database directly.
So when do you use an entity bean and when do you use a session bean to directly access data? Good question! As a rule of thumb, an entity bean is developed to provide a safe and consistent interface to a set of shared data that defines a concept. This data may be updated frequently. Session beans access data that spans concepts, is not shared, or is usually read-only.
In addition to accessing data directly, session beans can represent workflow. Workflow describes all the steps required to accomplish a particular task, such as booking passage on a ship or renting a video. Session beans are part of the same business API as entity beans, but as workflow components, they serve a different purpose. Session beans can manage the interactions between entity beans, describing how they work together to accomplish a specific task. The relationship between session beans and entity beans is like the relationship between a script for a play and the actors that perform the play. Where entity beans are the actors and props, the session bean is the script. Actors and props without a script can each serve a function individually, but only in the context of a script can they tell a story. In terms of our example, it makes no sense to have a database full of cabins, ships, passengers, and other objects if we can't create interactions between them, like booking a passenger for a cruise.
Session beans are divided into two basic types: stateless and stateful. A stateless session bean is a collection of related services, each represented by a method; the bean maintains no state from one method invocation to the next. When you invoke a method on a stateless session bean, it executes the method and returns the result without knowing or caring what other requests have gone before or might follow. Think of a stateless session bean as a set of procedures or batch programs that execute a request based on some parameters and return a result. Stateless session beans tend to be general-purpose or reusable, such as a software service.
A stateful session bean is an extension of the client application. It performs tasks on behalf of the client and maintains state related to that client. This state is called conversational statebecause it represents a continuing conversation between the stateful session bean and the client. Methods invoked on a stateful session bean can write and read data to and from this conversational state, which is shared among all methods in the bean. Stateful session beans tend to be specific to one scenario. They represent logic that might have been captured in the client application of a two-tier system.
Session beans, whether they are stateful or stateless, are not persistent like entity beans. In other words, session beans are not saved to the database. Stateful session beans are dedicated to one client and may have a preset timeout period. In EJB 1.0, the timeout period, which is specified in the deployment descriptor, is defined in seconds and is applied between business method invocations by the client.Each time a business method is invoked, the timeout clock is reset.[1] In EJB 1.1, the bean deployer declares the timeout in a vendor-dependent manner. Timeouts are no longer included in the deployment descriptor.
[1] This was not clearly defined in the EJB 1.0 specification. As a result, some vendors incorrectly measure the timeout period from the creation of the bean by the client, and the timeout period is not reset between method invocations. Consult your vendor's documentation to determine which strategy your vendor employs.
If the client fails to use the stateful bean before it times out, the bean instance is destroyed and the remote reference is invalidated. This prevents the stateful session bean from lingering long after a client has shut down or otherwise finished using it. A client can also explicitly remove a stateful session bean by calling one of its remove methods.
Stateless session beans have longer lives because they don't retain any conversational state and are not dedicated to one client, but they still aren't saved in a database because they don't represent any data. Once a stateless session bean has finished a method invocation for a client, it can be reassigned to any other EJB object to service a new client. A client can maintain a connection to a stateless session bean's EJB object, but the bean instance itself is free to service requests from anywhere. Because it doesn't contain any state information, there's no difference between one client and the next. Stateless session beans may also have a timeout period and can be removed by the client, but the impact of these events is different than with a stateful session bean. With a stateless session bean, a timeout or remove operation simply invalidates the remote reference for that client; the bean instance is not destroyed and is free to service other client requests.
A stateless session bean is relatively easy to develop and also very efficient. Stateless session beans require few server resources because they are neither persistent nor dedicated to one client. Because they aren't dedicated to one client, many EJB objects can use just a few instances of a stateless bean. A stateless session bean does not maintain conversational state relative to the EJB object it is servicing, so it can be swapped freely between EJB objects. As soon as a stateless instance services a method invocation, it can be swapped to another EJB object immediately. Because there is no conversational state, a stateless session bean doesn't require passivation or activation, further reducing the overhead of swapping. In short, they are lightweight and fast!
Stateless session beans often perform services that are fairly generic and reusable. The services may be related, but they are not interdependent. This means that everything a stateless session bean method needs to know has to be passed via the method's parameters. The only exception to this rule is information obtainable from the SessionContext and in EJB 1.1, the JNDI ENC. This provides an interesting limitation. Stateless session beans can't remember anything from one method invocation to the next, which means that they have to take care of the entire task in one method invocation.
Stateless session beans are EJB's version of the traditional transaction processing applications, which are executed using a procedure call. The procedure executes from beginning to end and then returns the result. Once the procedure is done, nothing about the data that was manipulated or the details of the request are remembered. There is no state.
These restrictions don't mean that a stateless session bean can't have instance variables and therefore some kind of internal state. There's nothing that prevents you from keeping a variable that tracks the number of times a bean has been called or that tracks data for debugging. An instance variable can even hold a reference to a live resource like a URL connection for writing debugging data, verifying credit cards, or anything else that might be useful. However, it's important to remember that this state can never be visible to a client. A client can't assume that the same bean instance will service it every time. If these instance variables have different values in different bean instances, their values will appear to change randomly as stateless session beans are swapped from one client to another. Therefore, any resources that you reference in instance variables should be generic. For example, each bean instance might reasonably record debugging messages in a different file--that might be the only way to figure out what was happening on a large server with many bean instances. The client doesn't know or care where debugging output is going. However, it would be clearly inappropriate for a stateless bean to remember that it was in the process of making a reservation for Madame X--the next time it is called, it may be servicing another client entirely.
Stateless session beans can be used for report generation, batch processing, or some stateless services like validating a credit card. Another good application would be a StockQuote bean that returns a stock's current price. Any activity that can be accomplished in one method call is a good candidate for the high-performance stateless session bean.
Both the TravelAgent bean and the ProcessPayment beans, which we develop in this chapter, depend on other entity beans, some of which we developed earlier in this book and several that you can download from O'Reilly's web site. The Cabin was developed in Chapter 4, "Developing Your First Enterprise Beans", but we still need several other beans to develop this example. The other beans are the Cruise, Customer, and Reservation beans. The source code for these beans is available with the rest of the examples for this book at the O'Reilly download site. Instructions for downloading code are available in the preface of this book.
Before you can use these beans, you will need to create some new tables in your database. Here are the table definitions that the new entity beans will need. The Cruise bean maps to the CRUISE table:
CREATE TABLE CRUISE ( ID INT PRIMARY KEY, NAME CHAR(30), SHIP_ID INT )
The Customer bean maps to the CUSTOMER table:
CREATE TABLE CUSTOMER ( ID INT PRIMARY KEY, FIRST_NAME CHAR(30), LAST_NAME CHAR(30), MIDDLE_NAME CHAR(30) )
The Reservation bean maps to the RESERVATION table:
CREATE TABLE RESERVATION ( CUSTOMER_ID INT, CABIN_ID INT, CRUISE_ID INT, PRICE NUMERIC )
Once you have created the tables, deploy these beans as container-managed entities in your EJB server and test them to ensure that they are working properly.
Chapter 2, "Architectural Overview" and Chapter 3, "Resource Management and the Primary Services" discussed the TravelAgent bean, which had a business method called bookPassage(). This bean demonstrated how a session bean manages workflow. Here is the code for bookPassage():
public Ticket bookPassage(CreditCard card, double price) throws IncompleteConversationalState {// EJB 1.0: also throws RemoteException if (customer == null || cruise == null || cabin == null) { throw new IncompleteConversationalState(); } try { ReservationHome resHome = (ReservationHome) getHome("ReservationHome",ReservationHome.class); Reservation reservation = resHome.create(customer, cruise, cabin, price); ProcessPaymentHome ppHome = (ProcessPaymentHome) getHome("ProcessPaymentHome",ProcessPaymentHome.class); ProcessPayment process = ppHome.create(); process.byCredit(customer, card, price); Ticket ticket = new Ticket(customer, cruise, cabin, price); return ticket; } catch (Exception e) { // EJB 1.0: throw new RemoteException("",e); throw new EJBException(e); } }
In the next section, we will develop a complete definition of the TravelAgent bean, including the logic of the bookPassage() method. At this point, however, we are interested in the ProcessPayment bean, which is a stateless bean used by the TravelAgent bean. The TravelAgent bean uses the ProcessPayment bean to charge the customer for the price of the cruise.
The process of charging customers is a common activity in Titan's business systems. Not only does the reservation system need to charge customers, but so do Titan's gift shops, boutiques, and other related businesses. The process of charging a customer for services is common to many systems, so it has been encapsulated in its own bean.
Payments are recorded in a special database table called PAYMENT. The PAYMENT data is batch processed for accounting purposes and is not normally used outside of accounting. In other words, the data is only inserted by Titan's system; it's not read, updated, or deleted. Because the process of making a charge can be completed in one method, and because the data is not updated frequently or shared, a stateless session bean has been chosen for processing payments. Several different forms of payment can be used: credit card, check, or cash. We will model these payment forms in our stateless ProcessPayment bean.
The ProcessPayment bean accesses an existing table in Titan's system called the PAYMENT table. Create a table in your database called PAYMENT with this definition:
CREATE TABLE PAYMENT ( customer_id NUMERIC, amount DECIMAL(8,2), type CHAR(10), check_bar_code CHAR(50), check_number INTEGER, credit_number NUMERIC, credit_exp_date DATE )
A stateless session bean, like any other bean, needs a remote interface. We obviously need a byCredit() method because the TravelAgent bean uses it. We can also identify two other methods that we'll need: byCash() for customers paying cash and byCheck() for customers paying with a personal check.
Here is a complete definition of the remote interface for the ProcessPayment bean:
package com.titan.processpayment; import java.rmi.RemoteException; import java.util.Date; import com.titan.customer.Customer; public interface ProcessPayment extends javax.ejb.EJBObject { public boolean byCheck(Customer customer, Check check, double amount) throws RemoteException,PaymentException; public boolean byCash(Customer customer, double amount) throws RemoteException,PaymentException; public boolean byCredit(Customer customer, CreditCard card, double amount) throws RemoteException,PaymentException; }
Remote interfaces in session beans follow the same rules as the entity beans. Here we have defined the three business methods, byCheck(), byCash(), and byCredit(), which take information relevant to the form of payment used and return a boolean value that indicates the success of the payment. In addition to the required RemoteException, these methods can throw an application-specific exception, the PaymentException. The PaymentException is thrown if any problems occur while processing the payment, such as a low check number or an expired credit card. Notice, however, that nothing about the ProcessPayment interface is specific to the reservation system. It could be used just about anywhere in Titan's system. In addition, each method defined in the remote interface is completely independent of the others. All the data that is required to process a payment is obtained through the method's arguments.
As an extension of the javax.ejb.EJBObject interface, the remote interface of a session bean inherits the same functionality as the remote interface of an entity bean. However, the getPrimaryKey() method throws a RemoteException, since session beans do not have a primary key to return:[2]
[2] The exact behavior of primary key related operations on session beans is undefined in EJB 1.0. Because it is undefined, certain vendors may choose to return null or throw a different exception when these methods are invoked. The behavior described here, however, is the recommended behavior for EJB 1.0 -compliant servers and is the required behavior in EJB 1.1.
public interface javax.ejb.EJBObject extends java.rmi.Remote { public abstract EJBHome getEJBHome() throws RemoteException; public abstract Handle getHandle() throws RemoteException; public abstract Object getPrimaryKey() throws RemoteException; public abstract boolean isIdentical(EJBObject obj) throws RemoteException; public abstract void remove() throws RemoteException, RemoveException; }
The getHandle() method returns a serializable handle object, just like the getHandle() method in the entity bean. For stateless session beans, this handle can be serialized and reused any time, as long as the stateless bean type is still available in the container that generated the handle.
NOTE
Unlike stateless session beans, stateful session beans are only available through the handle for as long as that specific bean instance is kept alive on the EJB server. If the client explicitly destroys the stateful session bean using one of the remove() methods, or if the bean times out, the instance is destroyed and the handle becomes invalid. As soon as the server removes a stateful session bean, its handle is no longer valid and will throw a RemoteException when its getEJBObject() is invoked.
A remote reference to the bean can be obtained from the handle by invoking its getEJBObject() method:
public interface javax.ejb.Handle { public abstract EJBObject getEJBObject() throws RemoteException; }
We've placed the ProcessPayment bean in its own package, which means it has its own directory in our development tree, dev/com/titan/processpayment. That's where we'll store all the code and compile class files for this bean.
The ProcessPayment bean's remote interface uses two classes in its definition that are particularly interesting: the CreditCard and Check classes. The definitions for these classes are as follows:
/* CreditCard.java */ package com.titan.processpayment; import java.util.Date; public class CreditCard implements java.io.Serializable { final static public String MASTER_CARD = "MASTER_CARD"; final static public String VISA = "VISA"; final static public String AMERICAN_EXPRESS = "AMERICAN_EXPRESS"; final static public String DISCOVER = "DISCOVER"; final static public String DINERS_CARD = "DINERS_CARD"; public long number; public Date expiration; public String type; public CreditCard(long nmbr, Date exp, String typ) { number = nmbr; expiration = exp; type = typ; } } /* Check.java */ package com.titan.processpayment; public class Check implements java.io.Serializable { String checkBarCode; int checkNumber; public Check(String barCode, int number) { checkBarCode = barCode; checkNumber = number; } }
If you examine the class definitions of the CreditCard and Check classes, you will see that they are not enterprise beans. They are simply serializable Java classes. These classes provide a convenient mechanism for transporting and binding together related data. CreditCard, for example, binds all the credit card data together in once class, making it easier to pass the information around as well as making our interfaces a little cleaner.
It may be surprising that these classes aren't entity beans, and there's certainly no restriction in the specification preventing you from implementing them that way. However, just because something can be an entity bean doesn't mean that it has to be, and in this case, there are good reasons for this design decision. Making everything an entity bean is unnecessary and will hurt the performance of your EJB application. Remember that supporting an entity bean requires a lot of overhead on the server's part. When that overhead isn't necessary, it's best avoided. Fine-grained, dependent objects like Check and CreditCard don't make practical entity beans, because they are not shared or changed over time and they are dependent on the Customer to provide them with context. The use of dependent classes should be limited to pass-by-value objects, which is explored in more detail in Chapter 9, "Design Strategies".
Any remote interface, whether it's for an entity bean or a session bean, can throw application exceptions, in addition to the required RemoteException. Application exceptions are created by the bean developer and should describe a business logic problem--in this particular case, a problem making a payment. Application exceptions should be meaningful to the client, providing an explanation of the error that is both brief and relevant.
It's important to understand what exceptions to use and when to use them. The RemoteException indicates subsystem-level problems and is used by the RMI facility. Likewise, exceptions like javax.naming.NamingException and java.sql.SQLException are thrown by other Java subsystems; usually these should not be thrown explicitly by your beans. The Java Compiler requires that you use try/catch blocks to capture checked exceptions like these.
When a checked exception from a subsystem ( JDBC, JNDI, JMS, etc.) is caught by a bean method, it should be rethrown as an EJBException or an application exception. You would rethrow a checked exception as an EJBException if it represented a system-level problem; checked exceptions are rethrown as application exceptions when they result from business logic problems. Your beans incorporate your business logic; if a problem occurs in the business logic, that problem should be represented by an application exception. When an EJBException is thrown, it's first processed by the container, and then a RemoteException is thrown to the client.
When a checked exception from a subsystem (JDBC, JNDI, JMS, etc.) is caught by a bean method, it should be rethrown as a RemoteException or an application exception. You would rethrow a checked exception as a RemoteException if it represented a system-level problem; checked exceptions are rethrown as application exceptions when they result from business logic problems. Your beans incorporate your business logic; if a problem occurs in the business logic, that problem should be represented by an application exception.
In either EJB 1.0 or EJB 1.1, if an unchecked exception, like java.lang.NullPointerException, is thrown by the bean instance, the container automatically throws a RemoteException to the client.
The PaymentException describes a specific business problem, so it is an application exception. Application exceptions extend java.lang.Exception. If you choose to include any instance variables in the exception, they should all be serializable. Here is the definition of ProcessPayment application exception:
package com.titan.processpayment; public class PaymentException extends java.lang.Exception { public PaymentException() { super(); } public PaymentException(String msg) { super(msg); } }
The home interface of all stateless session beans contains one create() method with no arguments. This is a requirement of the EJB specification. It is illegal to define create() methods with arguments, because stateless session beans don't maintain conversational state that needs to be initialized. There are no find methods in session beans, because session beans do not have primary keys and do not represent data in the database. Here is the definition of the home interface for the ProcessPayment bean:
package com.titan.processpayment; import java.rmi.RemoteException; import javax.ejb.CreateException; public interface ProcessPaymentHome extends javax.ejb.EJBHome { public ProcessPayment create() throws RemoteException, CreateException; }
The CreateException is mandatory, as is the RemoteException. The CreateException can be thrown by the bean itself to indicate an application error in creating the bean. A RemoteException is thrown when other system errors occur, for example, when there is a problem with network communication or when an unchecked exception is thrown from the bean class.
The ProcessPaymentHome interface, as an extension of the javax.ejb.EJBHome, offers the same EJBHome methods as entity beans. The only difference is that remove(ObjectprimaryKey) doesn't work because session beans don't have primary keys. If EJBHome.remove(ObjectprimaryKey) is invoked on a session bean (stateless or stateful), a RemoteException is thrown.[3] Logically, this method should never be invoked on the home interface of a session bean.
[3] The exact behavior of the EJBHome.remove(ObjectprimaryKey) in session beans is undefined in EJB 1.0. The recommended behavior in EJB 1.0 is to throw a RemoteException, which is the specified behavior in the EJB 1.1 specification. Check your vendor's documentation to determine how this behavior was implemented for EJB 1.0 containers.
Here are the definitions of the EJBHome interface for EJB 1.1 and EJB 1.0:
// EJBHome interface, EJB 1.1 public interface javax.ejb.EJBHome extends java.rmi.Remote { public abstract HomeHandle getHomeHandle() throws RemoteException; public abstract EJBMetaData getEJBMetaData() throws RemoteException; public abstract void remove(Handle handle) throws RemoteException, RemoveException; public abstract void remove(Object primaryKey) throws RemoteException, RemoveException; } // EJBHome interface, EJB 1.0 public interface javax.ejb.EJBHome extends java.rmi.Remote { public abstract EJBMetaData getEJBMetaData() throws RemoteException; public abstract void remove(Handle handle) throws RemoteException, RemoveException; public abstract void remove(Object primaryKey) throws RemoteException, RemoveException; }
The home interface of a session bean can return the EJBMetaData for the bean, just like an entity bean. EJBMetaData is a serializable object that provides information about the bean's interfaces. The only difference between the EJBMetaData for a session bean and an entity bean is that the getPrimaryKeyClass() on the session bean's EJBMetaData throws a java.lang.RuntimeException when invoked:[4]
[4] The exact behavior of the MetaData.getPrimaryKeyClass() method is undefined for session beans in EJB 1.0. The recommended behavior in EJB 1.0 is to throw a runtime exception, which is the specified behavior in the EJB 1.1 specification.
public interface javax.ejb.EJBMetaData { public abstract EJBHome getEJBHome(); public abstract Class getHomeInterfaceClass(); public abstract Class getPrimaryKeyClass(); public abstract Class getRemoteInterfaceClass(); public abstract boolean isSession(); public abstract boolean isStateless(); // EJB 1.0 only }
As stated earlier, the ProcessPayment bean accesses data that is not generally shared by systems, so it is an excellent candidate for a stateless session bean. This bean really represents a set of independent operations that can be invoked and then thrown away--another indication that it's a good candidate for a stateless session bean. Here is the complete definition of our ProcessPaymentBean class:
package com.titan.processpayment; import com.titan.customer.*; import java.sql.*; import java.rmi.RemoteException; import javax.ejb.SessionContext; import javax.naming.InitialContext; import javax.sql.DataSource; import javax.ejb.EJBException; import javax.naming.NamingException; public class ProcessPaymentBean implements javax.ejb.SessionBean { final public static String CASH = "CASH"; final public static String CREDIT = "CREDIT"; final public static String CHECK = "CHECK"; public SessionContext context; public void ejbCreate() { } public boolean byCash(Customer customer, double amount) throws PaymentException{// EJB 1.0: also throws RemoteException return process(getCustomerID(customer),amount, CASH,null,-1,-1,null); } public boolean byCheck(Customer customer, Check check, double amount) throws PaymentException{// EJB 1.0: also throws RemoteException int minCheckNumber = getMinCheckNumber(); if (check.checkNumber > minCheckNumber) { return process(getCustomerID(customer), amount, CHECK, check.checkBarCode,check.checkNumber,-1,null); } else { throw new PaymentException( "Check number is too low. Must be at least "+minCheckNumber); } } public boolean byCredit(Customer customer, CreditCard card, double amount) throws PaymentException {// EJB 1.0: also throws RemoteException if (card.expiration.before(new java.util.Date())) { throw new PaymentException("Expiration date has passed"); } else { return process(getCustomerID(customer), amount, CREDIT, null, -1, card.number, new java.sql.Date(card.expiration.getTime())); } } private boolean process(long customerID, double amount, String type, String checkBarCode, int checkNumber, long creditNumber, java.sql.Date creditExpDate) throws PaymentException{// EJB 1.0: also throws RemoteException Connection con = null; PreparedStatement ps = null; try { con = getConnection(); ps = con.prepareStatement ("INSERT INTO payment (customer_id, amount, type,"+ "check_bar_code,check_number,credit_number,"+ "credit_exp_date) VALUES (?,?,?,?,?,?,?)"); ps.setLong(1,customerID); ps.setDouble(2,amount); ps.setString(3,type); ps.setString(4,checkBarCode); ps.setInt(5,checkNumber); ps.setLong(6,creditNumber); ps.setDate(7,creditExpDate); int retVal = ps.executeUpdate(); if (retVal!=1) { // EJB 1.0: throw new RemoteException("Payment insert failed"); throw new EJBException("Payment insert failed"); } return true; } catch(SQLException sql) { // EJB 1.0: throw new RemoteException("",sql); throw new EJBException(sql); } finally { try { if (ps != null) ps.close(); if (con!= null) con.close(); } catch(SQLException se){se.printStackTrace();} } } public void ejbActivate() {} public void ejbPassivate() {} public void ejbRemove() {} public void setSessionContext(SessionContext ctx) { context = ctx; } private int getCustomerID(Customer customer) { // EJB 1.0: throws RemoteException try {// EJB 1.0: remove try/catch return ((CustomerPK)customer.getPrimaryKey()).id; } catch(RemoteException re) { throw new EJBException(re); } } private Connection getConnection() throws SQLException { // Implementations for EJB 1.0 and EJB 1.1 shown below } private int getMinCheckNumber() { // Implementations for EJB 1.0 and EJB 1.1 shown below } }
The three payment methods all use the private helper method process(), which does the work of adding the payment to the database. This strategy reduces the possibility of programmer error and makes the bean easier to maintain. The process() method simply inserts the payment information into the PAYMENT table. The use of JDBC in this method should be familiar to you from your work on the bean-managed Ship bean in Chapter 6, "Entity Beans". The JDBC connection is obtained from the getConnection() method; here are the EJB 1.1 and EJB 1.0 versions of this method:
// getConnection() for EJB 1.1 private Connection getConnection() throws SQLException { try { InitialContext jndiCntx = new InitialContext(); DataSource ds = (DataSource) jndiCntx.lookup("java:comp/env/jdbc/titanDB"); return ds.getConnection(); } catch(NamingException ne){throw new EJBException(ne);} } // getConnection() for EJB 1.0 private Connection getConnection() throws SQLException { return DriverManager.getConnection( context.getEnvironment().getProperty("jdbcURL")); }
Both the byCheck() and the byCredit() methods contain some logic to validate the data before processing it. The byCredit() method checks to ensure that the credit card's expiration data does not precede the current date. If it does, a PaymentException is thrown.
The byCheck() method checks to ensure that the check is above a minimum number, as determined by a property that's defined when the bean is deployed. If the check number is below this value, a PaymentException is thrown. The property is obtained from the getMinCheckNumber() method. In EJB 1.1, we can use the JNDI ENC to read the value of the minCheckNumber property. In 1.0, we read this property from the SessionContext:
// getMinCheckNumber() for EJB 1.1: uses JNDI ENC private int getMinCheckNumber() { try { InitialContext jndiCntx = new InitialContext( ); Integer value = (Integer) jndiCntx.lookup("java:comp/env/minCheckNumber"); return value.intValue(); } catch(NamingException ne){throw new EJBException(ne);} } // getMinCheckNumber() for EJB 1.0 private int getMinCheckNumber() { String min_check_string = context.getEnvironment().getProperty("minCheckNumber"); return Integer.parseInt(min_check_string); }
Thus, we are using an environment property, set in the deployment descriptor, to change the business behavior of a bean. It is a good idea to capture thresholds and other limits in the environment properties of the bean rather than hardcoding them. This gives you greater flexibility. If, for example, Titan decided to raise the minimum check number, you would only need to change the bean's deployment descriptor, not the class definition. (You could also obtain this type of information directly from the database.)
In EJB 1.1, the bean container contract has been extended to include the JNDI environment naming context ( JNDI ENC). The JNDI ENC is a JNDI name space that is specific to each bean type. This name space can be referenced from within any bean, not just entity beans, using the name "java:comp/env". The enterprise naming context provides a flexible, yet standard, mechanism for accessing properties, other beans, and resources from the container.
We've already seen the JNDI ENC several times. In Chapter 6, "Entity Beans", we used it to access a resource factory, the DataSource. The ProcessPaymentBean also uses the JNDI ENC to access a DataSource in the getConnection() method; further, it uses the ENC to access an environment property in the getMinCheckNumber() method. This section examines the use of the JNDI ENC to access environment properties.
Named properties can be declared in a bean's deployment descriptor. The bean accesses these properties at runtime by using the JNDI ENC. Properties can be of type String or one of several primitive wrapper types including Integer, Long, Double, Float, Byte, Boolean, and Short. By modifying the deployment descriptor, the bean deployer can change the bean's behavior without changing its code. As we've seen in the ProcessPayment bean, we could change the minimum check number that we're willing to accept by modifying the minCheckNumber property at deployment. Two ProcessPayment beans deployed in different containers could easily have different minimum check numbers, as shown in the following example:
<ejb-jar> <enterprise-beans> <session> <env-entry> <env-entry-name>minCheckNumber</env-entry-name> <env-entry-type>java.lang.Integer</env-entry-type> <env-entry-value>2000</env-entry-value> </env-entry> ... </session> ... <enterprise-beans> ... </ejb-jar>
The EJBContext.getEnvironment() method is optional in EJB 1.1, which means that it may or may not be supported. If it is not functional, the method will throw a RuntimeException. If it is functional, it returns only those values declared in the deployment descriptor as follows (where minCheckNumber is the property name):
<ejb-jar> <enterprise-beans> <session> <env-entry> <env-entry-name>ejb10-properties/minCheckNumber</env-entry-name> <env-entry-type>java.lang.String</env-entry-name> <env-entry-value>20000</env-entry-value> </env-entry> ... </session> ... </enterprise-beans> ... </ejb-jar>
The ejb10-properties subcontext specifies that the property minCheckNumber is available from both JNDI ENC context "java:comp/env/ejb10-properties/minCheckNumber" (as a String value), and from the getEnvironment() method.
Only those properties declared under the ejb10-properties subcontext are available via the EJBContext. Furthermore, such properties are only available through the EJBContext in containers that choose to support the EJB 1.0 getEnvironment() method; all other containers will throw a RuntimeException.
Deploying the ProcessPayment bean presents no significant problems. It's essentially the same as deploying the Ship or Cabin beans, except that the ProcessPayment bean has no primary key or persistent fields. Here is the XML deployment descriptor for the ProcessPayment bean:
<?xml version="1.0"?> <!DOCTYPE ejb-jar PUBLIC "-//Sun Microsystems, Inc.//DTD Enterprise JavaBeans 1.1//EN" "http://java.sun.com/j2ee/dtds/ejb-jar_1_1.dtd"> <ejb-jar> <enterprise-beans> <session> <description> A service that handles monetary payments. </description> <ejb-name>ProcessPaymentBean</ejb-name> <home>com.titan.processpayment.ProcessPaymentHome</home> <remote>com.titan.processpayment.ProcessPayment</remote> <ejb-class>com.titan.processpayment.ProcessPaymentBean</ejb-class> <session-type>Stateless</session-type> <transaction-type>Container</transaction-type> <env-entry> <env-entry-name>minCheckNumber</env-entry-name> <env-entry-type>java.lang.Integer</env-entry-type> <env-entry-value>2000</env-entry-value> </env-entry> <resource-ref> <description>DataSource for the Titan database</description> <res-ref-name>jdbc/titanDB</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> </resource-ref> </session> </enterprise-beans> <assembly-descriptor> <security-role> <description> This role represents everyone who is allowed full access to the ProcessPayment bean. </description> <role-name>everyone</role-name> </security-role> <method-permission> <role-name>everyone</role-name> <method> <ejb-name>ProcessPaymentBean</ejb-name> <method-name>*</method-name> </method> </method-permission> <container-transaction> <method> <ejb-name>ProcessPaymentBean</ejb-name> <method-name>*</method-name> </method> <trans-attribute>Required</trans-attribute> </container-transaction> </assembly-descriptor> </ejb-jar>
The DeploymentDescriptor for the ProcessPayment bean is created and serialized using an application called MakeDD, which is basically the same as the one used to create the TravelAgentDD.ser file in Chapter 4, "Developing Your First Enterprise Beans":
package com.titan.processpayment; import javax.ejb.deployment.*; import javax.naming.CompoundName; import java.util.*; import java.io.*; public class MakeDD { public static void main(String args []) { try { if (args.length <1) { System.out.println("must specify target directory"); return; } SessionDescriptor paymentDD = new SessionDescriptor(); paymentDD.setRemoteInterfaceClassName( "com.titan.processpayment.ProcessPayment"); paymentDD.setHomeInterfaceClassName( "com.titan.processpayment.ProcessPaymentHome"); paymentDD.setEnterpriseBeanClassName( "com.titan.processpayment.ProcessPaymentBean"); paymentDD.setSessionTimeout(60); paymentDD.setStateManagementType( SessionDescriptor.STATELESS_SESSION); Properties props = new Properties(); props.put("jdbcURL","jdbc:subprotocol:subname"); props.put("minCheckNumber","1000"); paymentDD.setEnvironmentProperties(props); ControlDescriptor cd = new ControlDescriptor(); cd.setIsolationLevel(ControlDescriptor.TRANSACTION_READ_COMMITTED); cd.setMethod(null); cd.setRunAsMode(ControlDescriptor.CLIENT_IDENTITY); cd.setTransactionAttribute(ControlDescriptor.TX_REQUIRED); ControlDescriptor [] cdArray = {cd}; paymentDD.setControlDescriptors(cdArray); CompoundName jndiName = new CompoundName("ProcessPaymentHome", new Properties()); paymentDD.setBeanHomeName(jndiName); String fileSeparator = System.getProperties().getProperty("file.separator"); if (! args[0].endsWith(fileSeparator)) args[0] += fileSeparator; FileOutputStream fis = new FileOutputStream(args[0]+"ProcessPaymentDD.ser"); ObjectOutputStream oos = new ObjectOutputStream(fis); oos.writeObject(paymentDD); oos.flush(); oos.close(); fis.close(); } catch(Throwable t) {t.printStackTrace();} } }
The class names, the JNDI name, and the environment properties are the only differences between the two descriptors. We create a properties table, add two properties to it, and call setEnvironmentProperties() to install the table. The two properties are "jdbcURL", which tells the bean how to contact the database, and "minCheckNumber", which tells the bean the minimum check number that it is allowed to accept. You will need to replace the value associated with the "jdbcURL" environment property with a JDBC URL specific to your EJB server and database.
In this context, it's important to notice that the state management type that we specify for this bean is STATELESS_SESSION. In the deployment descriptor for the TravelAgent bean, this was a piece of black magic that we didn't really explain in Chapter 4, "Developing Your First Enterprise Beans". Now we know what it means. This informs the container that the bean instance can be swapped between method invocations from EJB object to EJB object; it doesn't need to maintain conversational state.
Deploy the ProcessPayment bean and make some payments. You should also attempt to make payments in such a way that an application exception is thrown. You could, for example, submit a check payment with a check number that is too low or a credit card payment with an expiration date that has passed.
Copyright © 2001 O'Reilly & Associates. All rights reserved.