异常子句处理模式
异常子句 - 处理模式
使用处理器作为失败处理器
我们希望以特定方式处理某些异常,因此为该特定异常添加一个 onException 子句。
仅 Java:使用处理器作为失败处理器
// here we register exception cause for MyFunctionException
// when this exception occurs we want it to be processed by our
// processor
onException(MyFunctionalException.class)
.process(new MyFunctionFailureHandler())
.stop();所发生的情况是,每当抛出 MyFunctionalException 时,消息都会被路由到我们的处理器 MyFunctionFailureHandler。因此可以说,当处理过程中抛出 MyFunctionalException 时,Exchange 会被转而投递。这里需要特别说明的是,这完全是一种合法的处理方式。死信通道的默认重新投递策略不会被触发,因此我们的处理器会直接接收到 Exchange,而不会尝试任何重新投递。在处理器中,我们需要自行决定接下来要做什么。Camel 会将该 Exchange 视为已完成失败处理。因此,我们的处理器就是该路由的终点。下面我们来看处理器的代码。
纯 Java:自定义失败处理处理器
public static class MyFunctionFailureHandler implements Processor {
@Override
public void process(Exchange exchange) throws Exception {
// the caused by exception is stored in a property on the exchange
Throwable caused = exchange.getProperty(Exchange.EXCEPTION_CAUGHT, Throwable.class);
assertNotNull(caused);
// here you can do what you want, but Camel regards this exception as
// handled, and this processor as a failure handler, so it won't do redeliveries.
// So this is the end of this route.
}
}注意到我们是通过 Exchange 上的一个属性获取 caused by 异常的。Camel 正是把处理过程中捕获的任何异常保存在这里。因此你可以取出这个属性,检查异常消息,并做相应的处理。
将异常标记为已处理
另请参阅下文的处理并继续异常一节。
handled(true) 相当于告诉 Camel:不要把异常返回给调用方——我会在这个 onException 块中自行提供响应,之后就结束了。
有三点需要注意:
- 原路由会停止 —— exchange 不会从主路由中失败的地方继续执行,只有
onException块中的步骤会执行。 - 响应就是
onException块所产生的内容 —— 使用transform、bean或to来构建你希望调用方收到的响应。如果你没有设置响应,调用方得到的将是空消息体,而不是该异常。 exchange.getException()在onException块内为 null —— 请使用exchange.getProperty(Exchange.EXCEPTION_CAUGHT, Exception.class)来访问原始异常。
这与 continued(true) 不同:后者同样会抑制异常,但随后会从失败点继续执行原路由,就好像异常从未发生过一样。
使用 onException 处理已知异常是 Camel 中一项非常强大的功能。你可以使用 handle DSL 将异常标记为已处理,这样调用方就不会收到该 caused 异常作为响应。handle 是一个 Predicate,它被重载以接受三种类型的参数:
- Boolean
- Predicate
- Expression,它会按照如下规则集作为 Predicate 进行求值:如果表达式返回 Boolean,则直接使用;对于其他任何返回值,只要不为
not null,就被视为true。
例如,要将所有的 ValidationException 标记为已处理,可以这样写:
- Java
- XML
- YAML
onException(ValidationException)
.handled(true);<onException>
<exception>org.apache.camel.ValidationException</exception>
<handled>
<constant>true</constant>
</handled>
</onException>- onException:
exception:
- org.apache.camel.ValidationException
handled:
constant: "true"Java DSL 中使用 Handled 的示例
在下面的路由中,我们希望对所有 OrderFailedException 进行特殊处理,因为我们想向调用方返回自定义响应。首先这样配置路由:
纯 Java:使用自定义响应处理 OrderFailedException
// we do special error handling for when OrderFailedException is
// thrown
onException(OrderFailedException.class)
// we mark the exchange as handled so the caller doesn't
// receive the
// OrderFailedException but whatever we want to return
// instead
.handled(true)
// this bean handles the error handling where we can
// customize the error
// response using java code
.bean(OrderService.class, "orderFailed")
// and since this is an unit test we use mocks for testing
.to("mock:error");
// this is just the generic error handler where we set the
// destination
// and the number of redeliveries we want to try
errorHandler(deadLetterChannel("mock:error").maximumRedeliveries(1));
// this is our route where we handle orders
from("direct:start")
// this bean is our order service
.bean(OrderService.class, "handleOrder")
// this is the destination if the order is OK
.to("mock:result");接下来是我们的服务 Bean,它只是一个普通的 POJO,用于演示如何在 Camel 中使用 Bean 集成,从而避免与 Camel API 紧耦合:
纯 Java:使用 Bean 集成的 OrderService POJO
/**
* Order service as a plain POJO class
*/
public static class OrderService {
/**
* This method handle our order input and return the order
*/
public Object handleOrder(@Headers Map headers, @Body String payload) throws OrderFailedException {
headers.put("customerid", headers.get("customerid"));
if ("Order: kaboom".equals(payload)) {
throw new OrderFailedException("Cannot order: kaboom");
} else {
headers.put("orderid", "123");
return "Order OK";
}
}
/**
* This method creates the response to the caller if the order could not
* be processed
*/
public Object orderFailed(@Headers Map headers, @Body String payload) {
headers.put("customerid", headers.get("customerid"));
headers.put("orderid", "failed");
return "Order ERROR";
}
}最后,抛出的异常只是一个普通异常:
仅 Java:自定义异常类
public static class OrderFailedException extends Exception {
public OrderFailedException(String message) {
super(message);
}
}那么会发生什么呢?
如果发送的订单正在被正常处理,调用方将收到一个 Exchange 作为回复,其中**Order OK** 作为消息体,而**orderid=123** 位于一个消息头中。
如果订单未能被处理,从而抛出了 OrderFailedException,调用方将不会收到这个异常,而是收到我们在 OrderService 的 orderFailed 方法中精心构造的自定义响应。因此,调用方收到的 Exchange 中消息体为 Order ERROR,消息头中为 orderid=failed。
在 Spring XML DSL 中使用 Handled
在 Spring XML DSL 中的同一条路由:
仅 XML:使用自定义响应处理 OrderFailedException
<!-- setup our error handler as the deal letter channel -->
<bean id="errorHandler" class="org.apache.camel.builder.DeadLetterChannelBuilder">
<property name="deadLetterUri" value="mock:error"/>
</bean>
<!-- this is our POJO bean with our business logic defined as a plain spring bean -->
<bean id="orderService" class="org.apache.camel.spring.processor.onexception.OrderService" />
<!-- this is the camel context where we define the routes -->
<!-- define our error handler as a global error handler -->
<camelContext errorHandlerRef="errorHandler" xmlns="http://camel.apache.org/schema/spring">
<onException>
<!-- the exception is full qualified names as plain strings -->
<!-- there can be more just add a 2nd, 3rd exception element (unbounded) -->
<exception>org.apache.camel.spring.processor.onexception.OrderFailedException</exception>
<!-- we can set the redelivery policy here as well -->
<redeliveryPolicy maximumRedeliveries="1" />
<!-- mark this as handled -->
<handled>
<constant>true</constant>
</handled>
<!-- let our order service handle this exception, call the orderFailed method -->
<bean ref="orderService" method="orderFailed" />
<!-- and since this is a unit test we use mock for assertions -->
<to uri="mock:error" />
</onException>
<route>
<!-- the route -->
<from uri="direct:start" />
<!-- in the normal route then route to our order service and call handleOrder method -->
<bean ref="orderService" method="handleOrder" />
<!-- and since this is a unit test we use mock for assertions -->
<to uri="mock:result" />
</route>
</camelContext>在 YAML DSL 中使用 Handled
YAML DSL 中的相同示例
仅 YAML:使用自定义响应处理 OrderFailedException
- beans:
- name: "orderService"
type: "org.apache.camel.spring.processor.onexception.OrderService"
- errorHandler:
deadLetterChannel:
deadLetterUri: mock:error
- onException:
exception:
- org.apache.camel.spring.processor.onexception.OrderFailedException
handled:
constant:
expression: "true"
redeliveryPolicy:
maximumRedeliveries: 1
steps:
- bean:
ref: orderService
method: orderFailed
- to:
uri: mock:error
- route:
from:
uri: direct:start
steps:
- bean:
ref: orderService
method: handleOrder
- to:
uri: mock:result使用 onException 时异常为什么是 null?
如果使用 onException 处理异常,并希望从 Java 代码中的 Processor 里获取引发的 Exception,例如如下所示:
仅限 Java:onException 处理器中的异常为 null
.onException(Exception.class)
.handled(true)
.process(new Processor() {
@Override
public void process(Exchange exchange) throws Exception {
Exception cause = exchange.getException();
// why cause exception is null ???
}
})
.end()请注意,此时引发的异常已无法通过 exchange.getException() 获取,因为消息是在 onException 代码块中处理的。
此时,可以通过 Exchange 上以 Exchange.EXCEPTION_CAUGHT 为键的属性来获取引发的异常,如下所示:
仅限 Java:从属性中访问捕获的异常
Exception cause = exchange.getProperty(Exchange.EXCEPTION_CAUGHT, Exception.class);示例中要使用的正确代码在这里:
仅限 Java:在 onException 中获取异常的正确方式
.onException(Exception.class).handled(true)
.process(new Processor() {
@Override
public void process(Exchange exchange) throws Exception {
Exception cause = exchange.getProperty(Exchange.EXCEPTION_CAUGHT, Exception.class);
// we now have the caused exception
}
})
.end()处理异常并向客户端返回固定响应
在上面的路由中,我们处理了异常,但将其路由到了另一个端点。如果你需要修改响应,并向原始调用者(客户端)返回一个固定的响应呢?这并没有什么秘诀,只需像平常的 Camel 路由那样,使用 transform 来设置响应即可,如下例所示:
仅 Java 实现:使用 transform 返回固定响应
// we catch MyFunctionalException and want to mark it as handled
// (= no failure returned to client)
// but we want to return a fixed text response, so we transform
// OUT body as Sorry.
onException(MyFunctionalException.class)
.handled(true)
.transform().constant("Sorry");我们对示例稍作修改,让它返回原始的导致异常(caused exception)的消息,而不是固定文本 Sorry:
仅限 Java:返回异常消息
// we catch MyFunctionalException and want to mark it as handled
// (= no failure returned to client)
// but we want to return a fixed text response, so we transform
// OUT body and return the exception message
onException(MyFunctionalException.class)
.handled(true)
.transform(exceptionMessage());我们还可以使用 Simple 语言,结合异常的 cause 信息设置一段易读的错误提示:
仅限 Java:在错误响应中使用 Simple 语言
// we catch MyFunctionalException and want to mark it as handled
// (= no failure returned to client)
// but we want to return a fixed text response, so we transform
// OUT body and return a nice message
// using the simple language where we want insert the exception
// message
onException(MyFunctionalException.class)
.handled(true)
.transform().simple("Error reported: ${exception.message} - cannot process this message.");处理并继续异常
continued 选项允许你既处理异常,又在原路由中继续路由,就好像异常从未发生过一样。
例如:要忽略 IDontCareException 的抛出并继续执行,可以这样做:
- Java
- XML
- YAML
onException(IDontCareException.class)
.continued(true);<onException>
<exception>com.foo.IDontCareException</exception>
<continued>
<constant>true</constant>
</continued>
</onException>- onException:
exception:
- com.foo.IDontCareException
continued:
constant: "true"你可以将其类比为在每一步外面包一个 try … catch 块,然后直接忽略该异常。使用 continued 在 Camel 中更为简便,否则对于这类用例,你不得不采用 Try Catch Finally 风格的写法。
使用 continued 的示例
在下面这条路由中,我们希望对所有的 IllegalArgumentException 进行特殊处理,因为我们只是想继续路由。
- Java
- XML
- YAML
onException(IllegalArgumentException.class).continued(true);
from("direct:start")
.to("mock:start")
.throwException(new IllegalArgumentException("Forced"))
.to("mock:result");<onException>
<exception>java.lang.IllegalArgumentException</exception>
<!-- tell Camel to handle and continue when this exception was thrown -->
<continued><constant>true</constant></continued>
</onException>
<route>
<from uri="direct:start"/>
<to uri="mock:start"/>
<throwException message="Forced" exceptionType="java.lang.IllegalArgumentException"/>
<to uri="mock:result"/>
</route>- onException:
exception:
- java.lang.IllegalArgumentException
continued:
constant:
expression: "true"
- route:
from:
uri: direct:start
steps:
- to:
uri: mock:start
- throwException:
message: Forced
exceptionType: java.lang.IllegalArgumentException
- to:
uri: mock:resultHandled 与 Continued 有何区别?
如果 handled 为 true,那么抛出的异常将被处理,Camel 将不会在原路由中继续路由,而是跳出原路由。不过,你可以在 onException 中配置一个路由,该路由将被用来替代原路由。如果你需要为调用方创建一些自定义的响应消息,或者因为抛出了该异常而需要执行其他处理,就可以使用这个路由。
如果 continued 为 true,那么 Camel 会捕获该异常,实际上只是忽略它,并在原路由中继续路由。不过,如果你在 onException 中配置了路由,则会先执行该路由,然后再在原路由中继续路由。
在 onException 中使用原始消息
useOriginalMessage 选项用于路由原始的输入消息,而不是路由过程中可能已被修改的当前消息。
例如:如果你有如下路由:
- Java
- XML
- YAML
from("jms:queue:order:input")
.to("bean:validateOrder")
.to("bean:transformOrder")
.to("bean:handleOrder");<route>
<from uri="jms:queue:order:input"/>
<to uri="bean:validateOrder"/>
<to uri="bean:transformOrder"/>
<to uri="bean:handleOrder"/>
</route>- route:
from:
uri: jms:queue:order:input
steps:
- to:
uri: bean:validateOrder
- to:
uri: bean:transformOrder
- to:
uri: bean:handleOrder该路由监听 JMS 消息,并对其进行校验、转换和处理。在此过程中,Exchange 的负载会被转换/修改。因此,一旦出现问题而我们又想把消息移动到另一个 JMS 目的地,就可以添加一个 onException。但当我们把 Exchange 移动到该目的地时,我们并不知道消息当前处于什么状态——错误是在 transformOrder 之前发生的,还是之后?所以为了确保安全,我们希望移动从 jms:queue:order:input 收到的原始输入消息。为此,我们可以启用 useOriginalMessage 选项,如下所示:
- Java
- XML
- YAML
// will use original input message (body and headers)
onException(MyOrderException.class)
.useOriginalMessage()
.handled(true)
.to("jms:queue:order:failed");
from("jms:queue:order:input")
.to("bean:validateOrder")
.to("bean:transformOrder")
.to("bean:handleOrder");<onException useOriginalMessage="true">
<exception>com.foo.MyOrderException</exception>
<handled>
<constant>true</constant>
</handled>
<to uri="jms:queue:order:failed"/>
</onException>
<route>
<from uri="jms:queue:order:input"/>
<to uri="bean:validateOrder"/>
<to uri="bean:transformOrder"/>
<to uri="bean:handleOrder"/>
</route>- onException:
useOriginalMessage: "true"
exception:
- com.foo.MyOrderException
handled:
constant:
expression: "true"
steps:
- to:
uri: jms:queue:order:failed
- route:
from:
uri: jms:queue:order:input
steps:
- to:
uri: bean:validateOrder
- to:
uri: bean:transformOrder
- to:
uri: bean:handleOrder那么路由到 jms:queue:order:failed 的消息就是原始输入。如果想手动重试,我们可以把 JMS 消息从 failed 队列移回 input 队列,这完全没问题,因为该消息与我们接收到的原始消息完全相同。
原始消息的边界
原始输入指的是被当前工作单元(unit of work)所限定的输入消息。一个工作单元通常跨越一条路由;如果多条路由通过 direct 或 seda 等内部端点相连,则会跨越多条路由。当消息通过 JMS 或 HTTP 等外部端点传递时,消费者会创建一个新的工作单元,并将其接收到的消息作为输入,即作为原始输入。此外,splitter(拆分器)、multicast(多播)等某些 EIP 模式也会为其子路由(即拆分后的消息)中的消息创建新的工作单元边界;不过,这些 EIP 提供了一个名为 shareUnitOfWork 的选项,允许在错误处理方面与父工作单元合并,从而使用父工作单元的原始消息。
结合 onException 使用原始消息体
useOriginalBody 与上文所述的 useOriginalMessage 类似。当你希望在将消息发送到错误处理器或死信通道之前,为消息补充自定义报头并保留原始消息体时,可能需要使用 useOriginalBody。
例如,如果你有如下路由:
- Java
- XML
- YAML
// will use original input body
onException(MyOrderException.class)
.useOriginalBody()
.handled(true)
.to("jms:queue:order:failed");
from("jms:queue:order:input")
.setHeader("application", constant("OrderApp"))
.to("bean:validateOrder")
.to("bean:transformOrder")
.to("bean:handleOrder");<onException useOriginalBody="true">
<exception>com.foo.MyOrderException</exception>
<handled>
<constant>true</constant>
</handled>
<to uri="jms:queue:order:failed"/>
</onException>
<route>
<from uri="jms:queue:order:input"/>
<setHeader name="application">
<constant>OrderApp</constant>
</setHeader>
<to uri="bean:validateOrder"/>
<to uri="bean:transformOrder"/>
<to uri="bean:handleOrder"/>
</route>- onException:
useOriginalBody: "true"
exception:
- com.foo.MyOrderException
handled:
constant:
expression: "true"
steps:
- to:
uri: jms:queue:order:failed
- route:
from:
uri: jms:queue:order:input
steps:
- setHeader:
name: application
expression:
constant:
expression: OrderApp
- to:
uri: bean:validateOrder
- to:
uri: bean:transformOrder
- to:
uri: bean:handleOrder随后,消息在被 JMS 端点接收之后,会被添加一个名为 application 的消息头。如果发生错误,onException 将处理该异常,并原样使用原始消息体以及当前消息的消息头,这意味着这些消息头中会包含 application 消息头。
评论
登录后参与评论
KnowForge