动态路由器
Spring Integration 提供了相当多不同的路由器配置,适用于常见的基于内容的路由用例,同时也提供了将自定义路由器作为 POJO 实现的选项。例如,PayloadTypeRouter
提供了一种简单的方法来配置一个路由器,该路由器根据传入消息的有效载荷类型计算通道,而 HeaderValueRouter
则提供了相同的便利来配置一个通过评估特定消息头的值来计算通道的路由器。还有基于表达式(SpEL)的路由器,其中通道是根据评估表达式的结果来确定的。所有这些类型的路由器都表现出一些动态特性。
但是,这些路由器都需要静态配置。即使是在基于表达式的路由器的情况下,表达式本身也是作为路由器配置的一部分定义的,这意味着相同的表达式作用于相同的值总是会计算出相同的通道。在大多数情况下这是可以接受的,因为这样的路由是明确定义的,因此是可以预测的。但有时我们需要动态更改路由器配置,以便消息流可以被路由到不同的通道。
例如,你可能希望将系统的一部分下线进行维护,并暂时重新路由消息到不同的消息流。另一个例子是,你可能希望通过添加另一个路由来处理更具体的 java.lang.Number
类型(在 PayloadTypeRouter
的情况下),从而为你的消息流引入更多的粒度。
不幸的是,使用静态路由配置来实现这两个目标中的任何一个,你都必须关闭整个应用程序,更改路由器的配置(更改路由),然后再启动应用程序。这显然不是任何人想要的解决方案。
动态路由器 模式描述了如何在不关闭系统或个别路由器的情况下,动态地更改或配置路由器的机制。
在我们深入了解 Spring Integration 如何支持动态路由之前,我们需要考虑路由器的典型流程:
-
计算一个通道标识符,这是路由器在接收到消息后计算的一个值。通常,它是一个字符串或实际
MessageChannel
的一个实例。 -
将通道标识符解析为通道名称。我们将在本节后面部分描述此过程的具体细节。
-
将通道名称解析为实际的
MessageChannel
。
如果第一步的结果是 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>
在有效负载类型路由器的上下文中,前面提到的三个步骤将如下实现:
-
计算一个通道标识符,该标识符是有效负载类型的完全限定名称(例如,
java.lang.String
)。 -
将通道标识符解析为通道名称,其中上一步的结果用于从
mapping
元素中定义的有效负载类型映射中选择适当的值。 -
将通道名称解析为实际的
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>
现在我们可以考虑这三步是如何为一个头部值路由器工作的:
-
计算一个通道标识符,该标识符是
header-name
属性标识的头的值。 -
将通道标识符解析为通道名称,其中上一步的结果用于从
mapping
元素中定义的通用映射中选择适当的值。 -
将通道名称解析为
MessageChannel
的实际实例,作为对应用程序上下文中由上一步结果标识的 bean 的引用(希望它是一个MessageChannel
)。
前面两种不同路由器类型的配置看起来几乎相同。但是,如果您查看 HeaderValueRouter
的备用配置,我们可以清楚地看到没有 mapping
子元素,如下所示:
<int:header-value-router input-channel="inputChannel" header-name="testHeader"/>
但是,配置仍然是完全有效的。所以自然而然的问题是,第二步中的映射会怎样?
第二步现在是可选的。如果 mapping
未定义,则在第一步中计算的通道标识符值将自动被视为 通道名称
,现在将其解析为实际的 MessageChannel
,如第三步所示。这也意味着第二步是为路由器提供动态特性的关键步骤之一,因为它引入了一个过程,使您可以更改通道标识符解析为通道名称的方式,从而影响从初始通道标识符确定最终 MessageChannel
实例的过程。
例如,在前面的配置中,假设 testHeader
的值是 'kermit',它现在是一个通道标识符(第一步)。由于在这个路由器中没有映射,将此通道标识符解析为通道名称(第二步)是不可能的,因此该通道标识符现在被当作通道名称处理。但是,如果存在映射,只是针对不同的值呢?最终结果仍然会相同,因为,如果无法通过将通道标识符解析为通道名称的过程确定新值,则该通道标识符就成为通道名称。
剩下的只是第三步将通道名称('kermit')解析为由该名称标识的 MessageChannel
的实际实例。这基本上涉及按提供的名称查找 bean。现在,所有包含头值对为 testHeader=kermit
的消息都将被路由到一个其 bean 名称(它的 id
)为 'kermit' 的 MessageChannel
。
但是如果你想将这些消息路由到 'simpson' 通道呢?显然,更改静态配置是可以的,但这样做也需要将你的系统停机。然而,如果你可以访问通道标识符映射,你可以引入一个新的映射,其中头值对现在是 kermit=simpson
,从而让第二步在解析时将 'kermit' 作为通道标识符处理,而将其解析为名为 'simpson' 的通道。
显然,这同样适用于 PayloadTypeRouter
,你现在可以重新映射或移除特定的有效载荷类型映射。实际上,这适用于每一个其他路由器,包括基于表达式的路由器,因为它们的计算值现在有机会通过第二步解析为实际的 channel name
。
回退到将通道键作为通道名称是灵活且方便的。但是,如果你不信任消息创建者,一个恶意行为者(了解系统的人)可能会创建一条路由到意外通道的消息。例如,如果键被设置为路由器输入通道的通道名称,这样的消息会被路由回路由器,最终导致堆栈溢出错误。因此,你可能希望禁用此功能(将 channelKeyFallback
属性设置为 false
),并在需要时更改映射。
使用控制总线管理路由映射
管理路由映射的一种方法是通过 控制总线 模式,它暴露了一个控制通道,你可以通过该通道发送控制消息来管理和监控 Spring Integration 组件,包括路由器。
有关控制总线的更多信息,请参阅 控制总线。
通常情况下,你会发送一条控制消息,要求调用特定受管组件(例如路由器)上的特定操作。以下受管操作(方法)专门用于更改路由器解析过程:
-
public void setChannelMapping(String key, String channelName)
: 让你可以添加新的或修改现有的channel identifier
和channel name
之间的映射 -
public void removeChannelMapping(String key)
: 让你可以移除特定的频道映射,从而断开channel identifier
和channel 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 支持。