动态路由器

Spring Integration 为常见的基于内容的路由用例提供了相当多的不同路由器配置,以及将自定义路由器实现为 POJO 的选项。例如,PayloadTypeRouter 提供了一种简单的方式来配置路由器,该路由器根据入站消息的负载类型计算通道,而 HeaderValueRouter 在配置通过评估特定消息头的价值来计算通道的路由器方面提供了同样的便利。还有基于表达式 (SpEL) 的路由器,其中通道是根据评估表达式来确定的。所有这些类型的路由器都表现出一定的动态特性。

然而,这些路由器都需要静态配置。即使是基于表达式的路由器,表达式本身也是路由器配置的一部分,这意味着作用于相同值的相同表达式总是会计算出相同的通道。这在大多数情况下是可以接受的,因为这样的路由是明确定义的,因此是可预测的。但有时我们需要动态地改变路由器配置,以便将消息流路由到不同的通道。

例如,您可能需要关闭系统的一部分进行维护,并临时将消息重新路由到不同的消息流。另一个例子是,您可能希望通过添加另一条路由来处理更具体的 java.lang.Number 类型(在使用 PayloadTypeRouter 的情况下),从而为消息流引入更多粒度。

不幸的是,如果使用静态路由器配置来实现这两个目标中的任何一个,您将不得不关闭整个应用程序,更改路由器配置(更改路由),然后再重新启动应用程序。这显然不是任何人想要看到的解决方案。

动态路由器 模式描述了在不关闭系统或单个路由器的情况下动态更改或配置路由器的机制。

在深入探讨 Spring Integration 如何支持动态路由的具体细节之前,我们需要考虑路由器的典型流程:

  1. 计算一个通道标识符,这是路由器接收到消息后计算出的一个值。通常,它是一个 String 或实际的 MessageChannel 实例。

  2. 将通道标识符解析为通道名称。我们将在本节后面描述此过程的具体细节。

  3. 将通道名称解析为实际的 MessageChannel 实例

如果步骤 1 的结果是 MessageChannel 的实际实例,那么在动态路由方面就没多少可做的了,因为 MessageChannel 是任何路由器工作的最终产物。然而,如果第一步的结果是通道标识符而不是 MessageChannel 的实例,那么您有很多可能的方式来影响获取 MessageChannel 的过程。考虑以下负载类型路由器的示例:

<int:payload-type-router input-channel="routingChannel">
    <int:mapping type="java.lang.String"  channel="channel1" />
    <int:mapping type="java.lang.Integer" channel="channel2" />
</int:payload-type-router>

在负载类型路由器的上下文中,前面提到的三个步骤将实现如下:

  1. 计算一个通道标识符,它是负载类型的完全限定名(例如,java.lang.String)。

  2. 将通道标识符解析为通道名称,其中上一步的结果用于从 mapping 元素中定义的负载类型映射中选择适当的值。

  3. 将通道名称解析为实际的 MessageChannel 实例,它是应用程序上下文中由上一步结果标识的 bean 的引用(希望该 bean 是一个 MessageChannel)。

换句话说,每一步都为下一步提供输入,直到整个过程完成。

现在考虑一个头值路由器的示例:

<int:header-value-router input-channel="inputChannel" header-name="testHeader">
    <int:mapping value="foo" channel="fooChannel" />
    <int:mapping value="bar" channel="barChannel" />
</int:header-value-router>

现在我们可以考虑这三个步骤如何应用于头值路由器:

  1. 计算一个通道标识符,它是通过 header-name 属性标识的头的值。

  2. 将通道标识符解析为通道名称,其中上一步的结果用于从 mapping 元素中定义的通用映射中选择适当的值。

  3. 将通道名称解析为实际的 MessageChannel 实例,它是应用程序上下文中由上一步结果标识的 bean 的引用(希望该 bean 是一个 MessageChannel)。

前面两种不同路由器类型的配置看起来几乎完全相同。然而,如果您查看 HeaderValueRouter 的另一种配置,我们可以清楚地看到没有 mapping 子元素,如下面的列表所示:

<int:header-value-router input-channel="inputChannel" header-name="testHeader"/>

然而,此配置仍然完全有效。那么很自然的问题是,第二步的映射在哪里?

第二步现在是可选的。如果未定义 mapping,则第一步计算出的通道标识符值会自动被视为 channel name,然后像第三步中那样解析为实际的 MessageChannel。这也意味着第二步是为路由器提供动态特性的关键步骤之一,因为它引入了一个过程,允许您改变通道标识符解析为通道名称的方式,从而影响从初始通道标识符确定最终 MessageChannel 实例的过程。

例如,在前面的配置中,假设 testHeader 的值是 'kermit',它现在是一个通道标识符(第一步)。由于此路由器中没有映射,将此通道标识符解析为通道名称(第二步)是不可能的,并且此通道标识符现在被视为通道名称。然而,如果存在映射但针对的是不同的值呢?最终结果仍然相同,因为如果在将通道标识符解析为通道名称的过程中无法确定新值,则通道标识符将成为通道名称。

剩下要做的就是第三步,将通道名称 ('kermit') 解析为此名称标识的 MessageChannel 的实际实例。这基本上涉及对提供的名称进行 bean 查找。现在,所有包含 testHeader=kermit 头-值对的消息都将路由到 bean 名称(其 id)为 'kermit' 的 MessageChannel

但是,如果您想将这些消息路由到 'simpson' 通道怎么办?显然,更改静态配置是可行的,但这需要关闭系统。然而,如果您可以访问通道标识符映射,您可以引入一个新的映射,其中头-值对现在是 kermit=simpson,这样第二步就可以将 'kermit' 作为通道标识符,并将其解析为 'simpson' 作为通道名称。

同样的道理也适用于 PayloadTypeRouter,您现在可以重新映射或移除特定的负载类型映射。实际上,它适用于所有其他路由器,包括基于表达式的路由器,因为它们的计算值现在有机会通过第二步解析为实际的 channel name

任何是 AbstractMappingMessageRouter 子类的路由器(包括大多数框架定义的路由器)都是动态路由器,因为 channelMapping 是在 AbstractMappingMessageRouter 级别定义的。该映射的 setter 方法作为公共方法公开,同时还有 'setChannelMapping' 和 'removeChannelMapping' 方法。只要您拥有路由器本身的引用,这些方法就可以让您在运行时更改、添加和删除路由器映射。这也意味着您可以通过 JMX(参见 JMX 支持)或 Spring Integration 控制总线(参见 控制总线)功能暴露这些相同的配置选项。

将通道键作为通道名称回退是灵活方便的。但是,如果您不信任消息创建者,恶意行为者(了解系统的人)可能会创建一个消息,该消息被路由到意外的通道。例如,如果键设置为路由器的输入通道名称,此类消息将被路由回路由器,最终导致堆栈溢出错误。因此,您可能希望禁用此功能(将 channelKeyFallback 属性设置为 false),并在需要时更改映射。

使用控制总线管理路由器映射

管理路由器映射的一种方法是通过 控制总线 模式,该模式暴露了一个控制通道,您可以通过该通道发送控制消息来管理和监视 Spring Integration 组件,包括路由器。

有关控制总线的更多信息,请参见 控制总线

通常,您会发送一条控制消息,请求在特定受管组件(例如路由器)上调用特定操作。以下受管操作(方法)专门用于更改路由器解析过程:

  • public void setChannelMapping(String key, String channelName): 允许您添加新的或修改现有的 channel identifierchannel name 之间的映射

  • public void removeChannelMapping(String key): 允许您移除特定的通道映射,从而断开 channel identifierchannel name 之间的关联

请注意,这些方法可用于简单的更改(例如更新单个路由或添加或移除路由)。但是,如果您想移除一个路由并添加另一个路由,则更新不是原子的。这意味着在更新之间,路由表可能处于不确定状态。从 4.0 版本开始,您现在可以使用控制总线原子地更新整个路由表。以下方法可实现此目的:

  • public Map<String, String>getChannelMappings(): 返回当前的映射。

  • public void replaceChannelMappings(Properties channelMappings): 更新映射。请注意,channelMappings 参数是一个 Properties 对象,因此必须将其添加到相应的 IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS 头中

Properties newMapping = new Properties();
newMapping.setProperty("foo", "bar");
newMapping.setProperty("baz", "qux");
Message<?> replaceChannelMappingsCommandMessage =
                     MessageBuilder.withPayload("'router.handler'.replaceChannelMappings")
                            .setHeader(IntegrationMessageHeaderAccessor.CONTROL_BUS_ARGUMENTS, List.of(newMapping))
                            .build();

对于映射的编程更改,由于类型安全问题,我们建议您使用 setChannelMappings 方法。replaceChannelMappings 会忽略非 String 对象的键或值。

使用 JMX 管理路由器映射

您还可以使用 Spring 的 JMX 支持来暴露路由器实例,然后使用您喜欢的 JMX 客户端(例如 JConsole)来管理那些用于更改路由器配置的操作(方法)。

有关 Spring Integration 的 JMX 支持的更多信息,请参见 JMX 支持