How to catch and wrap exceptions thrown by JTA whe

2019-01-21 14:25发布

问题:

I'm struggling with a problem with an EJB3 class that manages a non-trivial data model. I have constraint validation exceptions being thrown when my container-managed transactional methods commit. I want to prevent them from being wrapped in EJBException, instead throwing a sane application exception that callers can handle.

To wrap it in a suitable application exception, I must be able to catch it. Most of the time a simple try/catch does the job because the validation exception is thrown from an EntityManager call I've made.

Unfortunately, some constraints are only checked at commit time. For example, violation of @Size(min=1) on a mapped collection is only caught when the container managed transaction commits, once it leaves my control at the end of my transactional method. I can't catch the exception thrown when validation fails and wrap it, so the container wraps it in a javax.transaction.RollbackException and wraps that in a cursed EJBException. The caller has to catch all EJBExceptions and go diving in the cause chain to try to find out if it's a validation issue, which is really not nice.

I'm working with container managed transactions, so my EJB looks like this:

@Stateless
@TransactionManagement(TransactionManagementType.CONTAINER
class TheEJB {

    @Inject private EntityManager em;

    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
    public methodOfInterest() throws AppValidationException {
       try {
           // For demonstration's sake create a situation that'll cause validation to
           // fail at commit-time here, like
           someEntity.getCollectionWithMinSize1().removeAll();
           em.merge(someEntity);
       } catch (ValidationException ex) {
           // Won't catch violations of @Size on collections or other
           // commit-time only validation exceptions
           throw new AppValidationException(ex);
       }
    }

}

... where AppValidationException is a checked exception or an unchecked exception annotated @ApplicationException so it doesn't get wrapped by EJB3.

Sometimes I can trigger an early constraint violation with an EntityManager.flush() and catch that, but not always. Even then, I'd really like to be able to trap database-level constraint violations thrown by deferred constraint checks at commit time too, and those will only ever arise when JTA commits.

Help?


Already tried:

Bean managed transactions would solve my problem by allowing me to trigger the commit within code I control. Unfortunately they aren't an option because bean managed transactions don't offer any equivalent of TransactionAttributeType.REQUIRES_NEW - there's no way to suspend a transaction using BMT. One of the annoying oversights of JTA.

See:

  • Why we need JTA 2.0
  • Bean-Managed Transaction Suspension in J2EE (don't do this!)

... but see answers for caveats and details.

javax.validation.ValidationException is a JDK exception; I can't modify it to add an @ApplicationException annotation to prevent wrapping. I can't subclass it to add the annotation; it's thrown by EclpiseLink, not my code. I'm not sure that marking it @ApplicationException would stop Arjuna (AS7's JTA impl) wrapping it in a RollbackException anyway.

I tried to use a EJB3 interceptor like this:

@AroundInvoke
protected Object exceptionFilter(InvocationContext ctx) throws Exception {
    try {
        return ctx.proceed();
    } catch (ValidationException ex) {
        throw new SomeAppException(ex);
    }
}

... but it appears that interceptors fire inside JTA (which is sensible and usually desirable) so the exception I want to catch hasn't been thrown yet.

I guess what I want is to be able to define an exception filter that's applied after JTA does its thing. Any ideas?


I'm working with JBoss AS 7.1.1.Final and EclipseLink 2.4.0. EclipseLink is installed as a JBoss module as per these instructions, but that doesn't matter much for the issue at hand.


UPDATE: After more thought on this issue, I've realised that in addition to JSR330 validation exceptions, I really also need to be able to trap SQLIntegrityConstraintViolationException from the DB and deadlock or serialization failure rollbacks with SQLSTATE 40P01 and 40001 respectively. That's why an approach that just tries to make sure commit will never throw won't work well. Checked application exceptions can't be thrown through a JTA commit because the JTA interfaces naturally don't declare them, but unchecked @ApplicationException annotated exceptions should be able to be.

It seems that anywhere I can usefully catch an application exception I can also - albeit less prettily - catch an EJBException and delve inside it for the JTA exception and the underlying validation or JDBC exception, then do decision making based on that. Without an exception filter feature in JTA I'll probably have to.

回答1:

I haven't tried this. But I am guessing this should work.

@Stateless
@TransactionManagement(TransactionManagementType.CONTAINER)
class TheEJB {

    @Inject
    private TheEJB self;

    @Inject private EntityManager em;

    public methodOfInterest() throws AppValidationException {
       try {
           self.methodOfInterestImpl();
       } catch (ValidationException ex) {
           throw new AppValidationException(ex);
       }
    }

    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
    public methodOfInterestImpl() throws AppValidationException {
        someEntity.getCollectionWithMinSize1().removeAll();
        em.merge(someEntity);
    }    
}

The container is expected to start a new transaction and commit within the methodOfInterest, therefore you should be able to catch the exception in the wrapper method.

Ps: The answer is updated based on the elegant idea provided by @LairdNelson...



回答2:

There's a caveat to what I said about REQUIRES_NEW and BMT in the original question.

See the EJB 3.1 spec, section 13.6.1Bean-Managed Transaction Demarcation, in Container responsibilities. It reads:

The container must manage client invocations to an enterprise bean instance with bean-managed transaction demarcation as follows. When a client invokes a business method via one of the enterprise bean’s client views, the container suspends any transaction that may be associated with the client request. If there is a transaction associated with the instance (this would happen if a stateful session bean instance started the transaction in some previous business method), the container associates the method execution with this transaction. If there are interceptor methods associated with the bean instances, these actions are taken before the interceptor methods are invoked.

(italics mine). That's important, because it means that a BMT EJB doesn't inherit the JTA tx of a caller that has an associated container managed tx. Any current tx is suspended, so if a BMT EJB creates a tx it is a new transaction, and when it commits it commits only its transaction.

That means you can use a BMT EJB method that begins and commits a transaction as if it were effectively REQUIRES_NEW and do something like this:

@Stateless
@TransactionManagement(TransactionManagementType.BEAN)
class TheEJB {

    @Inject private EntityManager em;

    @Resource private UserTransaction tx; 

    // Note: Any current container managed tx gets suspended at the entry
    // point to this method; it's effectively `REQUIRES_NEW`.
    // 
    public methodOfInterest() throws AppValidationException, SomeOtherAppException {
       try {
           tx.begin();
           // For demonstration's sake create a situation that'll cause validation to
           // fail at commit-time here, like
           someEntity.getCollectionWithMinSize1().removeAll();
           em.merge(someEntity);
           tx.commit();
       } catch (ValidationException ex) {
           throw new AppValidationException(ex);
       } catch (PersistenceException ex) {
           // Go grubbing in the exception for useful nested exceptions
           if (isConstraintViolation(ex)) {
               throw new AppValidationException(ex);
           } else {
               throw new SomeOtherAppException(ex);
           }
       }
    }

}

This puts the commit under my control. Where I don't need a transaction to span several calls across multiple different EJBs this allows me to handle all errors, including errors at commit time, within my code.

The Java EE 6 tutorial page on bean managed transactions doesn't mention this, or anything else about how BMTs are called.

The talk about BMT not being able to simulate REQUIRES_NEW in the blogs I linked to is valid, just unclear. If you have a bean-managed transactional EJB you cannot suspend a transaction that you begun in order to begin another. A call to a separate helper EJB may suspend your tx and give you the equivalent of REQUIRES_NEW but I haven't tested yet.


The other half of the problem - cases where I need container managed transactions because I have work that must be done across several different EJBs and EJB methods - is solved by defensive coding.

Early eager flushing of the entity manager allows me to catch any validation errors that my JSR330 validation rules can find, so I just need to make sure they're complete and comprehensive so I never get any check constraint or integrity violation errors from the DB at commit time. I can't handle them cleanly so I need to really defensively avoid them.

Part of that defensive coding is:

  • Heavy use of javax.validation annotations on entity fields, and use of @AssertTrue validation methods where that isn't enough.
  • Validation constraints on collections, which I was delighted to see are supported. For example, I have an entity A with a collection of B. A must have at least one B, so I added a @Size(min=1) constraint to the collection of B where it's defined in A.
  • Custom JSR330 validators. I've added several custom validators for things like Australian Business Numbers (ABNs) to make sure I never try to send an invalid one to the database and trigger a DB-level validation error.
  • Early flushing of the entity manager with EntityManager.flush(). This forces validation to take place when it's under the control of your code, not later when JTA goes to commit the transaction.
  • Entity-specific defensive code and logic in my EJB facade to make sure that situations that cannot be detected by JSR330 validation do not arise and cause commit to fail.
  • Where practical, using REQUIRES_NEW methods to force early commit and allow me to handle failures within my EJBs, retry appropriately, etc. This sometimes requires a helper EJB to get around issues with self-calls to business methods.

I still can't gracefully handle and retry serialization failures or deadlocks like I could when I was using JDBC directly with Swing, so all this "help" from the container has put me a few steps backwards in some areas. It saves a huge amount of fiddly code and logic in other places, though.

Where those errors occur I've added a UI-framework level exception filter. It sees the EJBException wrapping the JTA RollbackException wrapping the PersistenceException wrapping the EclipseLink-specific exception wrapping the PSQLException, examines the SQLState, and makes decisions based on that. It's absurdly roundabout, but it works



回答3:

Setting eclipselink.exception-handler property to point to an implementation of ExceptionHandler looked promising, but didn't work out.

The JavaDoc for ExceptionHandler is ... bad ... so you'll want to look at the test implementation and the tests (1, 2) that use it. There's somewhat more useful documentation here.

It seems difficult to use the exception filter to handle a few specific cases while leaving everything else unaffected. I wanted to trap PSQLException, check for SQLSTATE 23514 (CHECK constraint violation), throw a useful exception for that and otherwise not change anything. That doesn't look practical.

In the end I've dropped the idea and gone for bean managed transactions where possible (now that I properly understand how they work) and a defensive approach to prevent unwanted exceptions when using JTA container managed transactions.



回答4:

javax.validation.ValidationException is a JDK exception; I can't modify it to add an @ApplicationException annotation to prevent wrapping

In addition to your answer: you can use the XML descriptor to annotate 3rd party classes as ApplicationException.