Although this section covers JTA, it is strongly recommended that you do not attempt to manage transactions explicitly. Through transaction attributes, Enterprise JavaBeans provides a comprehensive and simple mechanism for delimiting transactions at the method level and propagating transactions automatically. Only developers with a thorough understanding of transactional systems should attempt to use JTA with EJB.
In EJB, implicit transaction management is provided on the enterprise bean method level so that we can define transactions that are delimited by the scope of the method being executed. This is one of the primary advantages of EJB over cruder distributed object implementations: it reduces complexity and therefore programmer error. In addition, declarative transaction demarcation, as used in EJB, separates the transactional behavior from the business logic; a change to transactional behavior does not require changes to the business logic. In rare situations, however, it may be necessary to take control of transactions explicitly. To do this, it is necessary to have a much more complete understanding of transactions.
Explicit management of transactions is complex and is normally accomplished using the OMG’s Object Transaction Service (OTS) or the Java implementation of OTS, the Java Transaction Service ( JTS). OTS and JTS provide APIs that allow developers to work with transaction managers and resources (e.g., databases and JMS providers) directly. While the JTS implementation of OTS is robust and complete, it is not the easiest API to work with; it requires clean and intentional control over the bounds of enrollment in transactions.
Enterprise JavaBeans supports a much simpler API, the Java
Transaction API ( JTA), for working with transactions. This API is
implemented by the javax.transaction
package. JTA
actually consists of two components: a high-level transactional
client interface and a low-level X/Open XA interface. We are
concerned with the high-level client interface, since that is the one
accessible to the enterprise beans and is the recommended
transactional interface for client applications. The low-level XA
interface is used by the EJB server and container to automatically
coordinate transactions with resources such as databases.
As an application and EJB developer, your use of explicit transaction
management will probably focus on one very simple interface:
javax.transaction.UserTransaction
. UserTransaction
provides an interface to the transaction manager that allows the
application developer to manage the scope of a transaction
explicitly. Here is an example of how explicit demarcation might be
used in an EJB or client application:
Object ref = getInitialContext().lookup("TravelAgentHomeRemote"); TravelAgentHome home = (TravelAgentHome) PortableRemoteObject.narrow(ref, TravelAgentHome.class); TravelAgent tr1 = home.create(customer); tr1.setCruiseID(cruiseID); tr1.setCabinID(cabin_1); TravelAgent tr2 = home.create(customer); tr2.setCruiseID(cruiseID); tr2.setCabinID(cabin_2); javax.transaction.UserTransaction tran = ...; // Get the UserTransaction. tran.begin(); tr1.bookPassage(visaCard,price); tr2.bookPassage(visaCard,price); tran.commit();
The client application needs to book two cabins for the same
customer—in this case, the customer is purchasing a cabin for
himself and his children. The customer does not want to book either
cabin unless he can get both, so the client application is designed
to include both bookings in the same transaction. Explicitly marking
the transaction’s boundaries through the use of the
javax.transaction.UserTransaction
object does
this. Each enterprise bean method invoked by the current thread
between the
UserTransaction.begin()
and
UserTransaction.commit()
methods is included in the same transaction scope, according to the
transaction attributes of the enterprise bean methods invoked.
Obviously this example is contrived, but the point it makes is clear.
Transactions can be controlled directly, instead of depending on
method scope to delimit them. The advantage of using explicit
transaction demarcation is that it gives the client control over the
bounds of a transaction. The client, in this case, may be a client
application or another enterprise bean.[58] In either case, the same
javax.transaction.UserTransaction
is used, but it
is obtained from different sources depending on whether it is needed
on the client or in an enterprise bean.
Java 2 Enterprise Edition ( J2EE) specifies how a client application
can obtain a UserTransaction
object using JNDI.
Here’s how a client obtains a
UserTransaction
object if the EJB container is
part of a J2EE system ( J2EE and its relationship with EJB is covered
in more detail in Chapter 17):
... Context jndiCntx = new InitialContext(); UserTransaction tran = (UserTransaction) jndiCntx.lookup("java:comp/UserTransaction"); utx.begin(); ... utx.commit(); ...
Enterprise beans can also manage transactions explicitly. Only
session beans and
message-driven beans with the
<transaction-type>
value of Bean
can manage their own transactions.
Enterprise beans that manage their own transactions are frequently
referred to as bean-managed transaction (BMT) beans. Entity beans can
never be BMT beans. BMT beans do not declare transaction attributes
for their methods. Here’s how a session bean declares that it
will manage transactions explicitly:
<ejb-jar>
<enterprise-beans>
...
<session>
...
<transaction-type>Bean</transaction-type>
...
To manage its own transaction, an enterprise bean needs to obtain a
UserTransaction
object. An enterprise bean obtains
a reference to the UserTransaction
from the
EJBContext
, as shown here:
public class HypotheticalBean extends SessionBean { SessionContext ejbContext; public void someMethod() { try { UserTransaction ut = ejbContext.getUserTransaction(); ut.begin(); // Do some work. ut.commit(); } catch(IllegalStateException ise) {...} catch(SystemException se) {...} catch(TransactionRolledbackException tre) {...} catch(HeuristicRollbackException hre) {...} catch(HeuristicMixedException hme) {...}
An enterprise bean can also access the
UserTransaction
from the JNDI ENC, as shown in the
following example. Both methods are legal and proper. The enterprise
bean performs the lookup using the
"java:comp/env/UserTransaction"
context:
InitialContext jndiCntx = new InitialContext(); UserTransaction tran = (UserTransaction) jndiCntx.lookup("java:comp/env/UserTransaction");
With stateless session beans, transactions
that are managed using the UserTransaction
must be
started and completed within the same method. In other words,
UserTransaction
transactions cannot be started in
one method and ended in another. This makes sense because stateless
session bean instances are shared across many clients; so while one
stateless instance may service a client’s first request, a
completely different instance may service a subsequent request by the
same client. With stateful session beans, however, a transaction can
begin in one method and be committed in another because a stateful
session bean is used by only one client. This allows a stateful
session bean to associate itself with a transaction across several
different client-invoked methods. As an example, imagine the
TravelAgent EJB as a BMT bean. In the following code, the transaction
is started in the setCruiseID()
method and
completed in the bookPassage()
method. This allows
the TravelAgent EJB’s methods to be associated with the same
transaction.
The definition of the TravelAgentBean
class looks
like this in EJB 2.0:
import com.titan.reservation.*; import java.sql.*; import javax.sql.DataSource; import java.util.Vector; import java.rmi.RemoteException; import javax.naming.NamingException; import javax.ejb.EJBException; public class TravelAgentBean implements javax.ejb.SessionBean { ... public void setCruiseID(Integer cruiseID) throws javax.ejb.FinderException { try {ejbContext.getUserTransaction().begin();
CruiseHomeLocal home = (CruiseHomeLocal) jndiContext.lookup("java:comp/env/ejb/CruiseHome"); cruise = home.findByPrimaryKey(cruiseID); } catch(RemoteException re) { throw new EJBException(re); } } public TicketDO bookPassage(CreditCardDO card, double price) throws IncompleteConversationalState {try {
if (ejbContext.getUserTransaction().getStatus() !=
javax.transaction.Status.STATUS_ACTIVE) {
throw new EJBException("Transaction is not active");
}
} catch(javax.transaction.SystemException se) {
throw new EJBException(se);
}
if (customer == null || cruise == null || cabin == null) { throw new IncompleteConversationalState(); } try { ReservationHomeLocal resHome = (ReservationHomeLocal) jndiContext.lookup("java:comp/env/ejb/ReservationHomeLocal"); ReservationLocal reservation = resHome.create(customer, cruise, cabin, price); Object ref = jndiContext.lookup("java:comp/env/ejb/ProcessPaymentHomeRemote"); ProcessPaymentHomeRemote ppHome = (ProcessPaymentHomeRemote) PortableRemoteObject.narrow(ref, ProcessPaymentHomeRemote.class); ProcessPaymentRemote process = ppHome.create(); process.byCredit(customer, card, price); TicketDO ticket = new TicketDO(customer,cruise,cabin,price);ejbContext.getUserTransaction().commit();
return ticket; } catch(Exception e) { throw new EJBException(e); } } ... }
In EJB 1.1, the TravelAgentBean
class definition
looks like this:
public class TravelAgentBean implements javax.ejb.SessionBean { ... public void setCruiseID(Integer cruiseID) throws javax.ejb.FinderException { try {ejbContext.getUserTransaction().begin();
CruiseHomeRemote home = (CruiseHomeRemote) getHome("CruiseHome", CruiseHomeRemote. class); cruise = home.findByPrimaryKey(cruiseID); } catch(RemoteException re) { throw new EJBException(re); } } public TicketDO bookPassage(CreditCardDO card, double price) throws IncompleteConversationalState {try {
if (ejbContext.getUserTransaction().getStatus() !=
javax.transaction.Status.STATUS_ACTIVE) {
throw new EJBException("Transaction is not active");
}
} catch(javax.transaction.SystemException se) {
throw new EJBException(se);
}
if (customer == null || cruise == null || cabin == null) { throw new IncompleteConversationalState(); } try { ReservationHomeRemote resHome = (ReservationHomeRemote) getHome("ReservationHomeRemote", ReservationHomeRemote.class); ReservationRemote reservation = resHome.create(customer, cruise, cabin, price); ProcessPaymentHomeRemote ppHome = (ProcessPaymentHomeRemote) getHome("ProcessPaymentHomeRemote", ProcessPaymentHomeRemote.class); ProcessPaymentRemote process = ppHome.create(); process.byCredit(customer, card, price); TicketDO ticket = new TicketDO(customer,cruise,cabin,price);ejbContext.getUserTransaction().commit();
return ticket; } catch(Exception e) { throw new EJBException(e); } } ... }
Repeated calls to the
EJBContext.getUserTransaction()
method return a reference to the same
UserTransaction
object. The container is required
to retain the association between the transaction and the stateful
bean instance across multiple client calls until the transaction
terminates.
In the bookPassage()
method, we can check the
status of the transaction to ensure that it is still active. If the
transaction is no longer active, we throw an exception. The use of
the getStatus()
method is covered in more detail
later in this chapter.
When a bean-managed transaction method is invoked by a client that is already involved in a transaction, the client’s transaction is suspended until the method returns. This suspension occurs regardless of whether the BMT bean explicitly started its own transaction within the method or the transaction was started in a previous method invocation. The client transaction is always suspended until the BMT method returns.
Message-driven beans also have the option of managing their own
transactions. In the case of MDBs, the scope of the transaction must
begin and end within the onMessage()
method—it is not possible for a bean-managed transaction to
span onMessage()
calls.
You can transform the ReservationProcessor EJB into a BMT bean simply
by changing its <transaction-type>
value to
Bean
:
<ejb-jar>
<enterprise-beans>
...
<message-driven>
...
<transaction-type>Bean</transaction-type>
...
In this case, the ReservationProcessorBean
class
would be modified to use the
javax.transaction.UserTransaction
to mark the
beginning and end of the transaction in
onMessage()
:
public class ReservationProcessorBean implements javax.ejb.MessageDrivenBean, javax.jms.MessageListener {MessageDrivenContext ejbContext;
Context jndiContext; public void onMessage(Message message) { try { ejbContext.getUserTransaction().begin(); MapMessage reservationMsg = (MapMessage)message; Integer customerPk = (Integer)reservationMsg.getObject("CustomerID"); Integer cruisePk = (Integer)reservationMsg.getObject("CruiseID"); Integer cabinPk = (Integer)reservationMsg.getObject("CabinID"); double price = reservationMsg.getDouble("Price"); //get the credit card Date expirationDate = new Date(reservationMsg.getLong("CreditCardExpDate")); String cardNumber = reservationMsg.getString("CreditCardNum"); String cardType = reservationMsg.getString("CreditCardType"); CreditCardDO card = new CreditCardDO(cardNumber,expirationDate,cardType); CustomerRemote customer = getCustomer(customerPk); CruiseLocal cruise = getCruise(cruisePk); CabinLocal cabin = getCabin(cabinPk); ReservationHomeLocal resHome = (ReservationHomeLocal) jndiContext.lookup("java:comp/env/ejb/ReservationHomeLocal"); ReservationLocal reservation = resHome.create(customer,cruise,cabin,price,new Date()); Object ref = jndiContext.lookup("java:comp/env/ejb/ProcessPaymentHomeRemote"); ProcessPaymentHomeRemote ppHome = (ProcessPaymentHomeRemote) PortableRemoteObject.narrow(ref,ProcessPaymentHomeRemote.class); ProcessPaymentRemote process = ppHome.create(); process.byCredit(customer,card,price); TicketDO ticket = new TicketDO(customer,cruise,cabin,price); deliverTicket(reservationMsg,ticket);ejbContext.getUserTransaction.commit();
} catch(Exception e) { throw new EJBException(e); } } ...
It is important to understand that in BMT, the message consumed by the MDB is not part of the transaction. When an MDB uses container-managed transactions, the message it is handling is a part of the transaction, so if the transaction is rolled back, the consumption of the message is also rolled back, forcing the JMS provider to redeliver the message. But with bean-managed transactions, the message is not part of the transaction, so if the BMT transaction is rolled back, the JMS provider will not be aware of the transaction’s failure. However, all is not lost, because the JMS provider can still rely on message acknowledgment to determine if the message was successfully delivered.
The EJB container will acknowledge the message if the
onMessage()
method returns successfully. If,
however, a RuntimeException
is thrown by the
onMessage()
method, the container will not
acknowledge the message and the JMS provider will suspect a problem
and will probably attempt to redeliver the message. If redelivery of
a message is important when a transaction fails in BMT, your best
course of action is to ensure that the onMessage()
method throws an EJBException,
so that the
container will not acknowledge the message
received from the JMS provider.
Vendors use proprietary (declarative) mechanisms to specify the
number of times to redeliver messages to
BMT/NotSupported
MDBs that “fail” to
acknowledge receipt. The JMS provider may provide a “dead
message” area into which such messages will be placed if they
cannot be successfully processed according to the retry count. The
dead message area can be monitored by administrators, and delivered
messages can be detected and handled manually.
Although the message is not part of the transaction, everything else
between the UserTransaction.begin()
and
UserTransaction.commit()
methods is part of the
same transaction. This includes creating a new Reservation EJB and
processing the credit card using the ProcessPayment EJB. If a
transaction failure occurs, these operations will be rolled back. The
transaction also includes the use of the JMS API in the
deliverTicket()
method to send the ticket message.
If a transaction failure occurs, the ticket message will not be
sent.
Transactions are normally controlled by a transaction manager (often the EJB server) that manages the ACID characteristics across several enterprise beans, databases, and servers. This transaction manager uses a two-phase commit (2-PC) to manage transactions. 2-PC is a protocol for managing transactions that commits updates in two stages. 2-PC is complex, but basically it requires that servers and databases cooperate through an intermediary, the transaction manager, to ensure that all the data is made durable together. Some EJB servers support 2-PC while others do not, and the value of this transaction mechanism is a source of some debate. The important point to remember is that a transaction manager controls the transaction; based on the results of a poll against the resources (databases, JMS providers, and other resources), it decides whether all the updates should be committed or rolled back. A heuristic decision is when one of the resources makes a unilateral decision to commit or roll back without permission from the transaction manager. Once a heuristic decision has been made, the atomicity of the transaction is lost and data-integrity errors can occur.
UserTransaction
, discussed in the next section,
throws a few different exceptions related to heuristic decisions;
these are included in the following discussion.
UserTransaction
is
a Java interface defined in the following code. EJB servers are not
required to support the rest of JTA, nor are they required to use JTS
for their transaction service. The
UserTransaction
is
defined as follows:
public interface javax.transaction.UserTransaction { public abstract void begin() throws IllegalStateException, SystemException; public abstract void commit() throws IllegalStateException, SystemException, TransactionRolledbackException, HeuristicRollbackException, HeuristicMixedException; public abstract int getStatus(); public abstract void rollback() throws IllegalStateException, SecurityException, SystemException; public abstract void setRollbackOnly() throws IllegalStateException, SystemException; public abstract void setTransactionTimeout(int seconds) throws SystemException; }
Here’s what the methods defined in this interface do:
begin()
Invoking the begin()
method creates a new
transaction. The thread that executes the begin()
method is immediately associated with the new transaction, which is
then propagated to any EJB that supports existing transactions. The
begin()
method can throw one of two checked
exceptions. An IllegalStateException
is thrown
when begin()
is called by a thread that is already
associated with a transaction. You must complete any transactions
associated with that thread before beginning a new transaction. A
SystemException
is thrown if the transaction
manager (i.e., the EJB server) encounters an unexpected error
condition.
commit()
The commit()
method completes the transaction that
is associated with the current thread. When
commit()
is executed, the current thread is no
longer associated with a transaction. This method can throw several
checked exceptions. An IllegalStateException
is
thrown if the current thread is not associated with a transaction. A
SystemException
is thrown if the transaction
manager (the EJB server) encounters an unexpected error condition. A
TransactionRolledbackException
is thrown when the
entire transaction is rolled back instead of committed; this can
happen if one of the resources was unable to perform an update or if
the UserTransaction.rollBackOnly()
method was
called. A
HeuristicRollbackException
indicates that heuristic decisions were made by one or more resources
to roll back the transaction. A
HeuristicMixedException
indicates that heuristic
decisions were made by resources to both roll back and commit the
transaction; that is, some resources decided to roll back while
others decided to commit.
rollback()
The rollback()
method is invoked to roll back the
transaction and undo updates. The rollback()
method can throw one of three different checked exceptions. A
SecurityException
is thrown if the thread using
the UserTransaction
object is not allowed to roll
back the transaction. An IllegalStateException
is
thrown if the current thread is not associated with a transaction. A
SystemException
is thrown if the transaction
manager (the EJB server) encounters an unexpected error condition.
setRollbackOnly()
The setRollbackOnly()
method is invoked to mark
the transaction for rollback. This means that, whether or not the
updates executed within the transaction succeed, the transaction must
be rolled back when completed. This method can be invoked by any
TX_BEAN_MANAGED
EJB participating in the
transaction or by the client application. The
setRollBackOnly()
method can throw one of two
checked exceptions. An IllegalStateException
is
thrown if the current thread is not associated with a transaction. A
SystemException
is thrown if the transaction
manager (the EJB server) encounters an unexpected error condition.
setTransactionTimeout(int seconds)
The setTransactionTimeout(int
seconds)
method sets the life span of a
transaction; i.e., how long it will live before timing out. The
transaction must complete before the transaction timeout is reached.
If this method is not called, the transaction manager (EJB server)
automatically sets the timeout. If this method is invoked with a
value of 0
seconds, the default timeout of the
transaction manager will be used. This method must be invoked after
the begin()
method. A
SystemException
is thrown if the transaction
manager (EJB server) encounters an unexpected error condition.
getStatus()
The
getStatus()
method returns
an integer that can be compared to constants defined in the
javax.transaction.Status
interface. This method
can be used by a sophisticated programmer to determine the status of
a transaction associated with a UserTransaction
object. A SystemException
is thrown if the
transaction manager (EJB server) encounters an unexpected error
condition.
Status
is a simple
interface that contains no methods, only constants. Its sole purpose
is to provide a set of constants that describe the current status of
a transactional object—in this case, the
UserTransaction
:
interface javax.transaction.Status { public final static int STATUS_ACTIVE; public final static int STATUS_COMMITTED; public final static int STATUS_COMMITTING; public final static int STATUS_MARKED_ROLLBACK; public final static int STATUS_NO_TRANSACTION; public final static int STATUS_PREPARED; public final static int STATUS_PREPARING; public final static int STATUS_ROLLEDBACK; public final static int STATUS_ROLLING_BACK; public final static int STATUS_UNKNOWN; }
The value returned by
getStatus()
tells the
client using the UserTransaction
the status of a
transaction. Here’s what the constants mean:
STATUS_ACTIVE
An active transaction is associated with the
UserTransaction
object. This status is returned
after a transaction has been started and prior to a transaction
manager beginning a two-phase commit. (Transactions that have been
suspended are still considered active.)
STATUS_COMMITTED
A transaction is associated with the
UserTransaction
object; the transaction has been
committed. It is likely that heuristic decisions have been made;
otherwise, the transaction would have been destroyed and the
STATUS_NO_TRANSACTION
constant would have been
returned instead.
STATUS_COMMITTING
A transaction is associated with the
UserTransaction
object; the transaction is in the
process of committing. The UserTransaction
object
returns this status if the transaction manager has decided to commit
but has not yet completed the process.
STATUS_MARKED_ROLLBACK
A transaction is associated with the
UserTransaction
object; the transaction has been
marked for rollback, perhaps as a result of a
UserTransaction.setRollbackOnly()
operation
invoked somewhere else in the application.
STATUS_NO_TRANSACTION
No transaction is currently associated with the
UserTransaction
object. This occurs after a
transaction has completed or if no transaction has been created. This
value is returned rather than throwing an
IllegalStateException
.
STATUS_PREPARED
A transaction is associated with the
UserTransaction
object. The transaction has been
prepared, which means that the first phase of the two-phase commit
process has completed.
STATUS_PREPARING
A transaction is associated with the
UserTransaction
object; the transaction is in the
process of preparing, which means that the transaction manager is in
the middle of executing the first phase of the two-phase commit.
STATUS_ROLLEDBACK
A transaction is associated with the
UserTransaction
object; the outcome of the
transaction has been identified as a rollback. It is likely that
heuristic decisions have been made; otherwise, the transaction would
have been destroyed and the STATUS_NO_TRANSACTION
constant would have been returned.
STATUS_ROLLING_BACK
A transaction is associated with the
UserTransaction
object; the transaction is in the
process of rolling back.
STATUS_UNKNOWN
A transaction is associated with the
UserTransaction
object; its current status cannot
be determined. This is a transient condition and subsequent
invocations will ultimately return a different status.
Only BMT beans have access to the
UserTransaction
from the
EJBContext
and JNDI ENC. Container-managed
transaction (CMT) beans cannot use the
UserTransaction
. CMT beans use the
setRollbackOnly()
and
getRollbackOnly()
methods of the
EJBContext
to interact with the current
transaction instead.
The
setRollbackOnly()
method
gives an enterprise bean the power to veto a transaction. This power
can be used if the enterprise bean detects a condition that would
cause inconsistent data to be committed when the transaction
completes. Once an enterprise bean invokes the
setRollbackOnly()
method, the current transaction
is marked for rollback and cannot be committed by any other
participant in the transaction—including the container.
The
getRollbackOnly()
method
returns true
if the current transaction has been
marked for rollback. This can be used to avoid executing work that
would not be committed anyway. If, for example, an exception is
thrown and captured within an enterprise bean method,
getRollbackOnly()
can be used to determine whether
the exception caused the current transaction to be rolled back. If it
did, there is no sense in continuing the processing. If it did not,
the EJB has an opportunity to correct the problem and retry the task
that failed. Only expert EJB developers should attempt to retry tasks
within a transaction. Alternatively, if the exception did not cause a
rollback (getRollbackOnly()
returns
false
), a rollback can be forced using the
setRollbackOnly()
method.
BMT beans must not use the
setRollbackOnly()
and
getRollbackOnly()
methods of the
EJBContext
. BMT beans should use the
getStatus()
and rollback()
methods on the UserTransaction
object to check for
rollback and force a rollback, respectively.
[58] Only beans
declared as managing their own transactions (bean-managed transaction
beans) can use the UserTransaction
interface.