Dynamic Routers

Spring Integration 为常见的基于内容的路由用例提供了许多不同的路由器配置,以及实现自定义路由器作为 POJO 的选项。例如,PayloadTypeRouter 提供了一种简单的方法来配置一个根据传入消息的有效负载类型计算信道的路由器,而 HeaderValueRouter 在配置一个通过计算特定消息 Header 的值来计算信道的路由器时提供了同样的便利。还有一些基于表达式的(SpEL)路由器,其信道是根据计算表达式的结果决定的。所有这些类型的路由器都表现出一些动态特性。 然而,这些路由器都需要静态配置。即使是在基于表达式的路由器的情况下,表达式本身也被定义为路由器配置的一部分,这意味着对同一值执行的同一表达式始终会计算出同一信道。在大多数情况下,这是可以接受的,因为此类路由是定义明确的,因而可以预测。但有时我们需要动态更改路由器配置,以便消息流可以被路由到不同的信道。 例如,您可能想要将系统的某个部分关闭以进行维护,并将消息暂时重新路由到不同的消息流中。另一个示例是,您可能希望通过为处理更具体类型的 java.lang.Number 添加另一个路由来为消息流引入更多的粒度(在 PayloadTypeRouter 的情况下)。 不幸的是,对于静态路由配置来说,为了实现这两个目标,您将不得不关闭整个应用程序,更改路由器的配置(更改路由),然后重新启动应用程序。显然,这不是任何人希望得到的解决方案。 dynamic router 模式描述了您可以在不关闭系统或各个路由器的情况下更改或动态配置路由器的机制。 在我们了解 Spring Integration 如何支持动态路由的具体内容之前,我们需要考虑路由器的典型流程:

  1. 计算通道标识符,这是路由器在接收消息后计算的值。通常,它是一个字符串或实际 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. 计算通道标识符,该标识符是 payload 类型的完全限定名称(例如, java.lang.String)。

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

  3. 将通道名称解析为 MessageChannel 的实际实例,引用应用程序上下文中 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(希望是一个 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 Support)或 Spring Integration 控制总线(参见 Control Bus)功能公开这些相同的配置选项。

将信道键用作信道名称具有灵活性且方便。但是,如果你不信任消息创建者,恶意行为者(具有系统的知识)可以创建路由到意外信道的消息。例如,如果将密钥设置为路由器输入信道的信道名称,则这样一条消息将被路由回路由器,最终导致堆栈溢出错误。因此,你可能希望禁用此功能(设置 channelKeyFallback 属性为 false),并在需要时更改映射。

Manage Router Mappings using the Control Bus

管理路由器映射的一种方法是通过 control bus 模式,它公开一个控制通道,您可以向该通道发送控制消息以管理和监视 Spring Integration 组件(包括路由器)。

关于控制总线的更多信息,请参阅 Control Bus

通常,您会发送一条控制消息来要求对特定的受管理组件(例如路由器)调用特定操作。以下受管理操作(方法)特定于更改路由器解析流程:

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

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

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

  • public Map&lt;String, String&gt;getChannelMappings():返回当前映射。

  • public void replaceChannelMappings(Properties channelMappings):更新映射。请注意, channelMappings 参数是一个 Properties 对象。此设置允许控制总线命令使用内置的 StringToPropertiesConverter,如下例所示:

"@'router.handler'.replaceChannelMappings('foo=qux \n baz=bar')"

请注意,每个映射都用一个换行符(\n)分隔。为了出于类型安全性考虑对映射进行编程上的更改,我们建议您使用 setChannelMappings 方法。replaceChannelMappings 忽略不是 String 对象的键或值。

Manage Router Mappings by Using JMX

您还可以使用 Spring 的 JMX 支持来公开一个路由程序实例,然后使用您 favorite 的 JMX 客户端(例如,JConsole)来管理更改路由程序配置的那些操作(方法)。

关于 Spring Integration 的 JMX 支持的更多信息,请参阅 JMX Support