While you can use standard Fuse Mediation Router error handling techniques in a transactional route, it is important to understand the interaction between exceptions and transaction demarcation. In particular, you need to bear in mind that thrown exceptions usually cause transaction rollback.
You can use one of the following approaches to roll back a transaction:
The most common way to roll back a Spring transaction is to throw a
runtime (unchecked) exception—that is, where the exception is
an instance or subclass of java.lang.RuntimeException
. Java errors, of
java.lang.Error
type, also trigger transaction rollback. Checked
exceptions, on the other hand, do not trigger rollback. Figure 5.3 summarises how Java errors and
exceptions affect transactions, where the classes that trigger rollback are shaded
gray.
![]() | Tip |
---|---|
The Spring framework also provides a system of XML annotations that enable you to specify which exceptions should or should not trigger rollbacks. For details, see Rolling back in the Spring Reference Guide. |
![]() | Warning |
---|---|
If a runtime exception is handled within the transaction (that is, before the exception has the chance to percolate up to the code that does the transaction demarcation), the transaction will not be rolled back. See How to define a dead letter queue for details. |
If you want to trigger a rollback in the middle of a transacted route, you can do
this by calling the rollback()
DSL command, which throws an
org.apache.camel.RollbackExchangeException
exception. In other
words, the rollback()
command uses the standard approach of throwing a
runtime exception to trigger the rollback.
For example, if you decide that there should be an absolute limit on the size of money transfers in the account services application, you could trigger a rollback when the amount exceeds 100, using the following code:
Example 5.2. Rolling Back an Exception with rollback()
from("file:src/data?noop=true")
.transacted()
.beanRef("accountService","credit")
.choice().when(xpath("/transaction/transfer[amount > 100]"))
.rollback()
.otherwise()
.to("direct:txsmall");
from("direct:txsmall")
.beanRef("accountService","debit")
.beanRef("accountService","dumpTable")
.to("file:target/messages/small");
![]() | Note |
---|---|
If you trigger a rollback in the preceding route, it will get trapped in an
infinite loop. The reason for this is that the
|
The markRollbackOnly()
DSL command enables you to force the current
transaction to roll back, without throwing an exception. This
can be useful in cases where (as in Example 5.2) throwing an exception has
unwanted side effects.
For example, Example 5.3 shows how to
modify Example 5.2 by replacing
rollback()
with markRollbackOnly()
. This version of
the route solves the problem of the infinite loop. In this case, when the amount of
the money transfer exceeds 100, the current transaction is rolled back, but no
exception is thrown. Because the file endpoint does not receive an exception, it
does not retry the exchange, and the failed transactions is quietly
discarded.
Example 5.3. Rolling Back an Exception with markRollbackOnly()
from("file:src/data?noop=true")
.transacted()
.beanRef("accountService","credit")
.choice().when(xpath("/transaction/transfer[amount > 100]"))
.markRollbackOnly()
.otherwise()
.to("direct:txsmall");
...
The preceding route implementation is not ideal, however. Although the route cleanly rolls back the transaction (leaving the database in a consistent state) and avoids the pitfall of infinite looping, it does not keep any record of the failed transaction. In a real-world application, you would typically want to keep track of any failed transaction. For example, you might want to write a letter to the relevant customer in order to explain why the transaction did not succeed. A convenient way of tracking failed transactions is to add a dead-letter queue to the route.
In order to keep track of failed transactions, you can define an
onException()
clause, which enables you to divert the relevant
exchange object to a dead-letter queue. When used in the context of transactions,
however, you need to be careful about how you define the onException()
clause, because of potential interactions between exception handling and transaction
handling. Example 5.4 shows the correct
way to define an onException()
clause, assuming that you need to
suppress the rethrown exception.
Example 5.4. How to Define a Dead Letter Queue
// Java import org.apache.camel.spring.SpringRouteBuilder; public class MyRouteBuilder extends SpringRouteBuilder { ... public void configure() { onException(IllegalArgumentException.class) .maximumRedeliveries(1) .handled(true) .to("file:target/messages?fileName=deadLetters.xml&fileExist=Append") .markRollbackOnly(); // NB: Must come *after* the dead letter endpoint. from("file:src/data?noop=true") .transacted() .beanRef("accountService","credit") .beanRef("accountService","debit") .beanRef("accountService","dumpTable") .to("file:target/messages"); } }
In the preceding example, onException()
is configured to catch the
IllegalArgumentException
exception and send the offending exchange
to a dead letter file, deadLetters.xml
(of course, you can change this
definition to catch whatever kind of exception arises in your application). The
exception rethrow behavior and the transaction rollback behavior are controlled by
the following special settings in the onException()
clause:
handled(true)
—suppress the rethrown exception. In this particular example, the rethrown exception is undesirable because it triggers an infinite loop when it propagates back to the file endpoint (see The markRollbackOnly() DSL command). In some cases, however, it might be acceptable to rethrow the exception (for example, if the endpoint at the start of the route does not implement a retry feature).markRollbackOnly()
—marks the current transaction for rollback without throwing an exception. Note that it is essential to insert this DSL command after theto()
command that routes the exchange to the dead letter queue. Otherwise, the exchange would never reach the dead letter queue, becausemarkRollbackOnly()
interrupts the chain of processing.
Instead of using onException()
, a simple approach to handling
exceptions in a transactional route is to use the doTry()
and
doCatch()
clauses around the route. For example, Example 5.5 shows how you can catch and
handle the IllegalArgumentException
in a transactional route, without
the risk of getting trapped in an infinite loop.
Example 5.5. Catching Exceptions with doTry() and doCatch()
// Java import org.apache.camel.spring.SpringRouteBuilder; public class MyRouteBuilder extends SpringRouteBuilder { ... public void configure() { from("file:src/data?noop=true") .doTry() .to("direct:split") .doCatch(IllegalArgumentException.class) .to("file:target/messages?fileName=deadLetters.xml&fileExist=Append") .end(); from("direct:split") .transacted() .beanRef("accountService","credit") .beanRef("accountService","debit") .beanRef("accountService","dumpTable") .to("file:target/messages"); } }
In this example, the route is split into two segments. The first segment (from the
file:src/data
endpoint) receives the incoming exchanges and
performs the exception handling using doTry()
and
doCatch()
. The second segment (from the direct:split
endpoint) does all of the transactional work. If an exception occurs within this
transactional segment, it propagates first of all to the transacted()
command, causing the current transaction to be rolled back, and it is then caught by
the doCatch()
clause in the first route segment. The
doCatch()
clause does not rethrow the
exception, so the file endpoint does not do any retries and infinite looping is
avoided.