测试

AdviceWith

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

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

用户手册

AdviceWith

AdviceWith 用于测试 Camel 路由,你可以在路由被测试之前对其增强(advice)。

adviceWith 允许在运行测试之前更改路由中的某些要素,例如:

  • 拦截发送到端点的消息
  • 用另一个端点替换入站端点
  • 移除或替换路由中的节点
  • 向路由中插入新节点
  • 模拟(mock)端点

所有这些功能都可从 AdviceWithRouteBuilder 获得,它是一个特化的 RouteBuilder。

AdviceWithRouteBuilder API

AdviceWithRouteBuilder 继承了常规的 RouteBuilder,并添加了用于增强路由的特化方法。下表列出了最常用的方法:

方法说明
mockEndpoints模拟路由中的所有端点。
mockEndpoints(patterns)模拟路由中与给定模式相匹配的所有端点。可以在给定的模式中使用通配符和正则表达式来匹配多个端点。
mockEndpointsAndSkip(patterns)模拟所有端点,并跳过向路由中与给定模式相匹配的端点发送消息。可以在给定的模式中使用通配符和正则表达式来匹配多个端点。
replaceFromWith(uri)用新的端点替换路由的输入。
weaveByUri(pattern)在发送到与该模式匹配的端点的节点处操作路由。
weaveById(pattern)在与该模式匹配的节点 ID 处操作路由。
weaveByToString(pattern)在与该模式匹配的节点字符串表示(toString 方法的输出)处操作路由。
weaveByType(Class)在与该模式匹配的节点类型(类名)处操作路由。
weaveAddFirst轻松在路由的开头织入新节点。
weaveAddLast轻松在路由的末尾织入新节点。

模式匹配

pattern 选项用于匹配。它使用与 Intercept 相同的规则,按以下顺序应用:

  • 精确匹配
  • 通配符匹配(*)
  • 正则表达式匹配

例如,要进行精确匹配,可以使用 weaveById("foo"),它只会匹配路由中 id 值为 foo 的位置。通配符匹配是指模式以 * 字符结尾,例如:weaveById("foo*") 会匹配所有以 foo 开头的 id,如 foo、foobar、foobie 等。正则表达式匹配更高级,允许匹配多个 id,例如 weaveById("(foo|bar)") 会同时匹配 foo 和 bar。

如果尝试对精确的端点 URI 进行模式匹配,请注意 URI 选项的顺序可能会影响匹配结果,因此最好使用通配符进行匹配。

例如,使用 mockEndpointsAndSkip("activemq:queue:foo?*") 来匹配 foo 队列并忽略任何选项。

使用 AdviceWith

要进行 advice,需要使用 AdviceWithRouteBuilder 来操作路由。但首先,需要选择要操作的路由,可以通过路由 ID 或路由索引来完成。

仅限 Java:使用 AdviceWithRouteBuilder 对路由进行 advice

AdviceWith.adviceWith("myRoute", context, new AdviceWithRouteBuilder() {
        @Override
        public void configure() throws Exception {
            weaveAddLast().to("mock:result");
        }
});

我们引入了一种更现代的 API,使用 Java lambda 风格来对路由进行 advice(增强/修改)。

下面,我们对 ID 为 myRoute 的路由进行 advice:

纯 Java:使用 lambda 风格对路由进行 advice

AdviceWith.adviceWith(context, "myRoute", a ->
     a.weaveAddLast().to("mock:result")
);

变量 a 是 AdviceWithRouteBuilder 的 lambda 风格写法,可用作内联编写路由操作的简写形式。

在使用 adviceWith 之前,最好先告诉 Camel 正在使用 advice,这一点将在下文介绍。

在测试中启用 advice

使用 adviceWith 时,Camel 会重新启动经过 advice 处理的路由。这是因为路由被修改了,Camel 需要:

  1. 停止现有路由
  2. 移除现有路由
  3. 添加经过 advice 处理的新路由
  4. 启动新路由

在单元测试启动期间,每个经过 advice 处理的路由都会经历这一过程。这个过程很快:一个路由被启动后,随即又被停止并移除。这不是我们期望的行为,应当避免经过 advice 处理的路由被重新启动。解决方法如下:

  1. 告诉 Camel 路由正在被 advice,这样可以防止 Apache Camel 自动启动这些路由。
  2. 在单元测试方法中对路由进行 advice
  3. 在完成对路由的 advice 之后再启动 Camel

使用 camel-test-junit5 进行单元测试时,可以通过重写 CamelTestSupport 的 isUsedAdviceWith 方法来告知 Camel 正在使用 advice,如下所示:

仅 Java:在 CamelTestSupport 中启用 AdviceWith

public class MyAdviceWithTest extends CamelTestSupport {
    @Override
    public boolean isUseAdviceWith() {
        return true; // turn on advice with
    }
}

或者在使用 camel-test-spring-junit5 进行单元测试时,你可以使用 @UseAdviceWith 注解,如下所示:

仅 Java:使用 @UseAdviceWith 注解启用 AdviceWith

@UseAdviceWith
public class MyAdviceWithTest extends CamelSpringTestSupport {
}

然后按如下方式通知路由并启动 Camel:

纯 Java 方式:先通知路由,再启动 CamelContext

@Test
public void testMockEndpoints() throws Exception {
    AdviceWith.adviceWith(context, "myRoute", a ->
         a.mockEndpoints();
    );

    context.start();

在上面的单元测试方法中,我们首先按 ID 对路由进行增强(advise),并在此过程中自动模拟(mock)所有端点。之后再启动 Camel。

记录路由增强前后的日志

使用 adviceWith 时,Camel 会自动以 XML 格式记录每条被增强路由的前后状态。

不过,这需要将 camel-xml-jaxb 作为依赖项;如果使用 Maven,可以将其添加为 test 作用域:

<dependency>
    <groupId>org.apache.camel</groupId>
    <artifactId>camel-xml-jaxb</artifactId>
    <version>x.x.x</version>
    <!-- use the same version as your Camel core version -->
    <scope>test</scope>
</dependency>

可以通过将日志设置为 false 来关闭 XML 格式的日志记录,如下所示:

仅限 Java:在为路由添加增强时禁用 XML 日志记录

AdviceWith.adviceWith(context, "myRoute", false, a ->
     a.mockEndpoints();
);

替换路由端点

你可能构建过这样的 Camel 路由:它们从消费数据库、消息中间件、云系统或其他外部系统的端点开始。

为了使这类路由的单元测试更加容易,可以将路由的输入端点替换为内部端点,例如 direct、seda、stub。

下面演示了如何做到这一点:

仅使用 Java:为测试替换路由输入端点

@Test
public void testReplaceFrom() throws Exception {
    AdviceWith.adviceWith(context, "myRoute", a ->
        a.replaceFromWith("direct:start");
    );

    context.start();

这会将 ID 为 myRoute 的路由中的输入端点(from)替换为 direct 端点,从而在单元测试中可以方便地向该路由发送消息。

模拟端点

在对路由进行 advice 时使用 mockEndpoints 方法,Camel 会在启动期间记录哪些端点被 advice 了,以及它们对应的 mock uri,例如:

INFO  ceptSendToMockEndpointStrategy - Adviced endpoint [seda://camel] with mock endpoint [mock:seda:camel]
INFO  ceptSendToMockEndpointStrategy - Adviced endpoint [seda://other] with mock endpoint [mock:seda:other]

这里 Camel 对两个端点进行了 advice(替换):

  • seda:camel -→ mockcamel
  • seda:other -→ mockother

这样就可以在单元测试中使用 mock 端点进行测试,例如:

纯 Java 方式:在单元测试中使用自动 mock 的端点

public void testMockEndpoints() throws Exception {
    // advice the route goes here
    // start camel after advice

    // use the auto mocked uris during testing
    getMockEndpoint("mock:seda:camel").expectedMessageCount(3);
    getMockEndpoint("mock:seda:other").expectedMessageCount(1);

    // send messages

    MockEndpoint.assertIsSatisfied(context);
}

使用 adviceWith 替换输入端点或在 Camel 路由中模拟端点,仅仅是路由操作能力的开始。下面的章节将介绍如何更进一步。

使用 weave 修改路由

在测试 Camel 路由时,你可以在测试之前使用 adviceWith 对路由进行编织(weave)。

weave 方法提供以下操作:

方法说明
remove移除选中的节点。
replace用后续节点替换选中的节点。
before在选中的节点之前添加后续节点。
after在选中的节点之后添加后续节点。

例如,给定如下的路由:

仅 Java:用于演示 weave 的示例路由

from("direct:start")
  .to("mock:foo")
  .to("mock:bar").id("bar")
  .to("mock:result");

接下来我们逐一介绍这四种方法,看看如何在单元测试中使用它们。

替换(Replace)

仅限 Java:通过 ID 织入某个节点并进行替换

AdviceWith.adviceWith(context.getRouteDefinitions().get(0), context, new AdviceWithRouteBuilder() {
    @Override
    public void configure() throws Exception {
        // weave the node in the route which has id = bar
        // and replace it with the following route path
        weaveById("bar").replace().multicast().to("mock:a").to("mock:b");
    }
});

在这个示例中,我们将 .to("mock:bar").id("bar") 替换为 .multicast().to("mock:a").to("mock:b")。这意味着消息不再发送到 mock:bar 端点,而是改为多播(Multicast)到 mock:a 和 mock:b 端点。

移除

在下面的示例中,我们直接从路由中移除 .to("mock:bar").id("bar"):

仅 Java:按 ID 织入节点并将其移除

AdviceWith.adviceWith(context.getRouteDefinitions().get(0), context, new AdviceWithRouteBuilder() {
    @Override
    public void configure() throws Exception {
        // weave the node in the route which has id = bar and remove it
        weaveById("bar").remove();
    }
});

之前

在下面的示例中,我们在 id 为 bar 的节点之前添加以下节点 to("mock:a").transform(constant("Bye World"))。

仅限 Java:在特定节点之前织入节点

AdviceWith.adviceWith(context.getRouteDefinitions().get(0), context, new AdviceWithRouteBuilder() {
    @Override
    public void configure() throws Exception {
        // weave the node in the route which has id = bar
        // and insert the following route path before the adviced node
        weaveById("bar").before().to("mock:a").transform(constant("Bye World"));
    }
});

这意味着在发送到 mock:bar 之前,消息已经被转换为常量消息 Bye World

之后(After)

在下面的示例中,我们在 id 为 bar 的节点之后添加了以下节点 to("mock:a").transform(constant("Bye World"))。

仅限 Java:在特定节点之后织入节点

AdviceWith.adviceWith(context.getRouteDefinitions().get(0), context, new AdviceWithRouteBuilder() {
    @Override
    public void configure() throws Exception {
        // weave the node in the route which has id = bar
        // and insert the following route path after the advice node
        weaveById("bar").after().to("mock:a").transform(constant("Bye World"));
    }
});

这意味着在 mock:bar 之后发送的消息已经被转换成常量消息 Bye World。

不使用 ID 进行织入

在织入路由时,你需要使用某个 weaveBy 方法作为条件,以便在路由图中选择一个或多个节点。

假设你在路由中使用了 Split EIP,那么就可以使用 weaveByType 来选择这个 EIP。给定如下路由:

纯 Java 实现:用于 weaveByType 示例的、包含 Split EIP 的路由

from("file:inbox").routeId("inbox")
    .split(body())
    .transform(simple("${body.toLowerCase()}"))
        .to("mock:line")
    .end()
    .to("mock:combined");

由于该路由中只有一个 Split EIP,你可以使用 weaveByType 来定位路由中的这个唯一切分器。使用 weaveByType 需要传入该 EIP 的模型类型,模型类型的名称遵循 _name_Definition 这一命名模式。

仅限 Java:使用 weaveByType 在 Split EIP 之前插入一个节点

weaveByType(SplitDefinition.class)
    .before()
        .transform(simple("${body},Camel is awesome"));

在此我们编织并选择 Split EIP,并织入一个消息转换,该转换在调用拆分器之前执行。这意味着消息体将追加 Camel is awesome。

weaveByToUri

weaveByToUri 是一个便捷方法,可轻松编织发送消息到给定端点 URI 或模式的 Camel 路由。

给定以下在 Content Based Router EIP 中包含两个分支的路由:

纯 Java 实现:用于 weaveByToUri 示例的基于内容的路由器路由

from("direct:start")
    .choice()
        .when(header("foo")).to("direct:branch-1")
    .otherwise()
        .to("direct:branch-2");

然后我们希望能够轻松地对该路由进行单元测试,验证消息是发送到 branch-1 还是 branch-2。这可以通过 weaveByToUri 来完成,如下所示:

仅 Java:使用 weaveByToUri 进行通配符匹配

weaveByToUri("direct:branch*").replace().to("mock:cheese");

注意 weaveByToUri 方法使用通配符(*)来匹配两个分支。

你也可以在上面的示例中使用 mockEndpoints 来自动模拟,而不是使用 weaveByToUri。weave 方法拥有更强大的功能来操作路由,例如消息转换、消息路由等等。

weaveAddFirst 和 weaveAddLast

weaveAddFirst 和 weaveAddLast 是用于轻松向路由添加节点的简写方法。这些方法只能向已存在的路由中添加节点。如果你想操作路由本身,请使用前面已介绍的其他 weave 方法。

weaveAddFirst 方法在路由的开头添加节点,而 weaveAddLast 在路由的末尾添加节点。它们的用法与其他 weaveBy 方法相同,示例请参见上文。

使用节点选择的 weave 方法

weaveBy 方法会选中所有匹配的节点,匹配结果可能为零个、一个、两个或更多节点。在某些情况下,你可能需要将选择范围缩小到特定的某个节点。这可以通过使用 select 方法来实现:

  • selectFirst 只选中第一个节点。
  • selectLast 只选中最后一个节点。
  • selectIndex(index) 只选中第 n 个节点。索引从 0 开始。
  • selectRange(from, to) 选中给定范围内的节点。索引从 0 开始。
  • maxDeep(level) 将选择范围限制在 Camel 路由树中最多 N 层深度。第一层的编号为 1,因此编号 2 表示第一层节点的子节点。

假设存在以下包含多个 Filter EIP 的路由,我们只想对第二个过滤器进行 advice。

仅 Java:包含多个 Filter EIP 的路由,用于 selectIndex 示例

from("file:inbox").routeId("inbox")
    .filter(header("foo"))
        .to("mock:foo")
    .end()
    .to("mock:a")
    .filter(header("bar"))
        .to("mock:bar")
    .end()
    .to("mock:b")
    .filter(header("cheese"))
        .to("mock:cheese")
    .end()
    .to("mock:c")

然后可以使用 weaveByType 匹配 Filter EIP,并用 selectIndex 匹配找到的第二个节点:

仅 Java:使用 selectIndex 定位到特定的匹配节点

weaveByType(FilterDefinition.class).selectIndex(1).replace().to("mock:changed");

评论

登录后参与评论

正在加载评论…