动态路由器
Spring Integration 为常见的基于内容的路由用例提供了相当多的不同路由器配置,以及将自定义路由器实现为 POJO 的选项。
例如,PayloadTypeRouter
提供了一种简单的方式来配置一个基于传入消息的有效载荷类型计算通道的路由器,而 HeaderValueRouter
在配置一个通过评估特定消息头的值来计算通道的路由器时提供了同样的便利。
还有基于表达式(SpEL)的路由器,其中通道是根据表达式的评估结果确定的。
所有这些类型的路由器都表现出一些动态特性。
然而,这些路由器都要求静态配置。
即使在基于表达式的路由器中,表达式本身也是作为路由器配置的一部分定义的,这意味着对相同值操作的相同表达式总是导致相同通道的计算。
这在大多数情况下是可以接受的,因为这样的路由是明确定义且可预测的。
但有时我们需要动态更改路由器配置,以便消息流可以路由到不同的通道。
例如,您可能希望关闭系统的一部分进行维护,并暂时将消息重新路由到不同的消息流。
再例如,您可能希望通过添加另一个路由来处理更具体的 java.lang.Number
类型(在 PayloadTypeRouter
的情况下)来增加消息流的粒度。
不幸的是,对于静态路由器配置,要实现这些目标中的任何一个,您都必须关闭整个应用程序,更改路由器的配置(更改路由),然后重新启动应用程序。
这显然不是任何人想要的解决方案。
动态路由器 模式描述了如何在不关闭系统或单个路由器的情况下动态更改或配置路由器的机制。
在我们深入了解 Spring Integration 如何支持动态路由的细节之前,我们需要考虑路由器的典型流程:
-
计算通道标识符,这是路由器接收消息后计算出的值。 通常,它是一个 String 或实际
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 的引用(它最好是一个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)
: 允许您添加新的或修改通道标识符
和通道名称
之间的现有映射 -
public void removeChannelMapping(String key)
: 允许您删除特定的通道映射,从而断开通道标识符
和通道名称
之间的关系
请注意,这些方法可用于简单的更改(例如更新单个路由或添加或删除路由)。 但是,如果您想删除一个路由并添加另一个路由,则更新不是原子的。 这意味着路由表可能在更新之间处于不确定状态。 从 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 支持。 |