动态路由器
Spring Integration 为常见的内容路由场景提供了多种不同的路由器配置,同时也支持将自定义路由器实现为 POJO。例如,PayloadTypeRouter 提供了一种简单的方式来配置基于传入消息的有效负载类型计算通道的路由器,而 HeaderValueRouter 则提供了类似的便利性,用于配置通过评估特定消息头的值来计算通道的路由器。此外还有基于表达式(SpEL)的路由器,其中通道的确定基于对表达式的求值。所有这些类型的路由器都展现出一定的动态特性。
然而,这些路由器都需要静态配置。即使在基于表达式的路由器中,表达式本身也是作为路由器配置的一部分来定义的,这意味着对相同值执行相同表达式总是会计算出相同的通道。这在大多数情况下是可以接受的,因为此类路由是明确定义的,因此是可预测的。但有时我们需要动态更改路由器配置,以便消息流可以被路由到不同的通道。
例如,您可能希望暂停系统的某一部分进行维护,并临时将消息重新路由到不同的消息流。再比如,您可能希望通过添加另一条路由来处理更具体的 java.lang.Number 类型(在 PayloadTypeRouter 的情况下),从而为消息流引入更细粒度的控制。
遗憾的是,如果采用静态路由器配置来实现上述任一目标,您将不得不停止整个应用程序的运行,更改路由器的配置(修改路由),然后重新启动应用程序。这显然不是任何人想要的解决方案。
动态路由器模式描述了无需关闭系统或单个路由器即可动态更改或配置路由器的机制。
在深入探讨 Spring Integration 如何支持动态路由之前,我们需要先了解路由器的典型流程:
-
计算通道标识符,这是路由器接收到消息后计算出的值。通常,它是一个字符串或实际的
MessageChannel实例。 -
将通道标识符解析为通道名称。我们将在本节后面详细描述此过程的具体细节。
-
将通道名称解析为实际的
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>
在载荷类型路由器的上下文中,前述三个步骤将按如下方式实现:
-
计算通道标识符,即负载类型的完全限定名(例如
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 的引用(该 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 控制总线(参见 控制总线)功能暴露这些相同的配置选项。
回退到使用通道键(channel key)作为通道名称的做法既灵活又方便。然而,如果你不信任消息的创建者,恶意行为者(了解系统的人)可能会创建一条被路由到意外通道的消息。例如,如果键被设置为路由器输入通道的名称,这样的消息将被路由回路由器,最终导致堆栈溢出错误。因此,你可能希望禁用此功能(将 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 支持。