I am consistently finding that my already-existing transaction is getting committed inside any method of an EJB marked @ejb.transaction
type="Required"
. Can this be correct?
My expectation is, an EJB "requiring" a transaction means: if there's one already there, it will politely leave it uncommitted when done so that whoever invoked begin() can continue to use it for further operations before invoking commit()
or rollback()
. [Of course, if there was no transaction in the first place, then the EJB method would invoke both begin()
and commit()
/rollback()
.]
Is my expectation wrong, or should I be looking for a configuration bug?
It might be relevant to add that I'm using Hibernate 3 inside the EJB. I'm obtaining a UserTransaction before calling the EJB method. The EJB generated wrapper invokes ServerTransaction.commit()
on exit, which Hibernate hooks into and uses the opportunity to close its Session. The error I'm getting is a Hibernate lazy loading exception, because the session is closed when I try to access getters on a Hibernate-persi开发者_StackOverflowsted object. So technically, I'm not 100% sure whether the ServerTransaction.commit()
I observed necessarily committed the UserTransaction
I started (maybe ServerTransaction.commit()
doesn't always actually follow through with a "real" commit?), but if it did not -- then on what basis is Hibernate closing the Session?
Update: I believe my assumptions above were correct, but my observations were a bit off. See below for my self-supplied Answer.
REQUIRED can be evil
I personally do not like the REQUIRED transaction attribute and would strongly discourage its use.
Lazily creating transactions (which is what REQUIRED does) results in not really knowing when and where a transaction was actually started and when it will commit. That's not a good thing. People should explicitly design transactional boundaries.
MANDATORY and UserTransaction are soul mates
Your desire to use a UserTransaction
is very good and does work with CMT -- there is no difference between a JTA transaction started via UserTransaction supplied by the container or a JTA transaction started for you by the container.
The approach I would recommend is to switch all REQUIRED usage to MANDATORY. With MANDATORY the container will not start transactions for you. Instead it will protect your bean by ensuring it cannot be called unless a transaction is in progress. This is an amazing and underutilized feature. MANDATORY is the best friend of anyone who wants to create a truly deterministic transactional app and to enforce it. With this setup you might fall in love with CMT.
In this scenario you start the transactions with UserTransactions
and then the container is like your big bodyguard kicking people to the curb unless they've appropriately started a transaction before attempting to invoke your code.
Considerations
- A Servlet or a BMT EJB can use a UserTransaction.
- CMT beans cannot use a UserTransaction, but they can participate in a transaction started by a UserTransaction (as noted above, a JTA transaction is a JTA transaction).
- BMT beans cannot participate in an existing transaction. If you call a BMT bean while a transaction is already in progress, the transaction will be suspended by the container prior to invoking the BMT bean and resumed after the method completes.
@Resource UserTransaction
will get you the user transaction via injectionjava:comp/UserTransaction
will get you the user transaction via lookup@TransactionAttribute(MANDATORY)
used at the class level will affect the methods of that exact class (i.e. thefooClass.getDecaredMethods()
methods). Methods of super classes and subclasses will default to@TransactionAttribute(REQUIRED)
unless those classes are also explicitly annotated@TransactionAttribute(MANDATORY)
- If you get sick of calling
userTransaction.begin()
anduserTransaction.commit()
and doing the related exception handling, consider@TransactionAttribute(REQUIRES_NEW)
. Your transaction boundaries will still be documented and explicit. RuntimeException
s thrown from a method of a CMT bean will cause the transaction to be marked for rollback, even if you catch and handle the exception in the calling code. Use@ApplicationException
to disable this on a case by case basis for custom runtime exception classes.
Losing your Transaction context
A couple things can cause your in-progress transaction to stop, suspend or otherwise not propagate to the called bean.
- BMT beans stop transaction propagation. If a transaction in progress calls a BMT bean, that transaction will be suspended before the BMT bean is called and will be resumed after the bean returns. BMT beans can be the origin of transactions but cannot participate in existing transactions. If propagation is mysteriously failing, make sure there are not unintentional calls to BMT beans in mid-transaction.
- Do not use anything other than a container-supplied UserTransaction or container supplied methods like SessionContext.setRollbackOnly to manage transactions. Use of a "resource-manager specific transaction demarcation API" such as JPA
EntityTransaction
orjava.sql.Connection.commit()
will circumvent transaction management. - Do not start your own threads. Transaction propagation happens on a per-thread basis. There are no standard APIs in Java EE that support a transaction spanning multiple threads. If you leave the thread either from starting your own thread or using @Asynchronous, you will leave your existing transaction behind.
NClark, consider the following code I ran on GlassFish 3.1.1. I hope it will help in any way :-)
@Stateless
@TransactionManagement(TransactionManagementType.BEAN)
public class ReusingTransaction {
// It's BMT - we can't control Tx through context - must use...
@Resource
SessionContext sctx;
// ... the UserTransaction instead.
@Resource
UserTransaction utx;
// This CMT EJB will reuse BMT started transaction
@EJB
AnotherBean reuseTx;
public void testMethod() throws Exception {
// Begin Tx and check it's status - compare value with:
// http://java.sun.com/javaee/6/docs/api/constant-values.html#javax.transaction.Status.STATUS_ACTIVE
utx.begin();
System.out.println("####testMethod#### Tx status: " + utx.getStatus());
// Our BMT started a Tx - now invoke CMT and reuse this Tx
// Notice: AnotherBean has MANDATORY Tx attribute, so if no Tx would
// exist, the AnotherBean couldn't be even invoked.
reuseTx.testIt();
// Check if the CMT AnotherBean affected Tx we started.
System.out.println("####testMethod#### Tx status: " + utx.getStatus());
// Just to prevent exceptions.
utx.rollback();
}
// Implicitly CMT - must reuse the Tx
@Stateless
@TransactionAttribute(TransactionAttributeType.MANDATORY)
public static class AnotherBean {
// It's CMT, so Tx control is made through it's context.
@Resource
SessionContext sctx;
// Can inject it, but cannot use it - will throw an Exception.
@Resource
UserTransaction utx;
public void testIt() throws Exception {
// Give a sign that rollback must be made.
sctx.setRollbackOnly();
System.out.println("####testIt#### Tx status: " + getTxStatus());
}
}
// Small hack to get the status of current thread JTA Tx
// http://java.sun.com/javaee/6/docs/api/javax/transaction/TransactionSynchronizationRegistry.html
private static int getTxStatus() throws Exception {
InitialContext ctx = new InitialContext();
TransactionSynchronizationRegistry tsr = (TransactionSynchronizationRegistry)
ctx.lookup("java:comp/TransactionSynchronizationRegistry");
return tsr.getTransactionStatus();
}
}
This EJB can be invoked i.e. from Singleton EJB with @Startup to see immediately how your AS will react.
On Glassfish 3.1.1 you'll come with the following result:
INFO: ####testMethod#### Tx status: 0
INFO: ####testIt#### Tx status: 1
INFO: ####testMethod#### Tx status: 1
Cheers!
Closer inspection reveals a different answer than suggested above. What I'm actually seeing is that the UserTransaction I started has remained open, but CMT created a new transaction on entry to the EJB method, despite the "Required" attribute.
I believe this is happening because I broke the rules. :) You aren't supposed to access the UserTransaction API when using CMT. CMT happily ignored my UserTransaction and started its own, having taken its place as the arbiter of all transaction boundaries. Since it started the transaction, it committed it as well, and of course left my UserTransaction untouched.
Seems brittle and silly to me, perhaps a naive opinion, but appears consistent with the "rules" as I read them. I don't know why CMT chooses not to play nice with UserTransactions started at a higher level. Perhaps to force developers to "do the right J2EE thing" and create another session bean layer to handle the broader transaction context. This would work because CMT would be managing the outer transaction and therefore would be fine with enlisting any inner transactions, and I believe in such a case, the "umbrella" transaction would not be committed by the inner EJBs; CMT would wait until the outer transaction completes, then commit the whole thing. It would have to, actually.
I'm not in the mood to create more session EJBs in this already EJB-bloated app, but it may be the only solution short of ripping CMT out in a whole bunch of places I'd rather not touch.
That's how CMT managed transactions work. The container automatically commits the transaction when the business method returns. If you dont' wont this behaviour use BMT instead of CMT.
精彩评论