异常子句

异常子句处理模式

师成师成· 更新于 2026-09-28· 阅读 39 分钟· 0 次阅读

登录后可跨设备保存划线和私人笔记登录

异常子句 - 处理模式

返回异常子句

使用处理器作为失败处理器

我们希望以特定方式处理某些异常,因此为该特定异常添加一个 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:result

Handled 与 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 消息头。

评论

登录后参与评论

正在加载评论…