動態路由器
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 的引用(該 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,你現在可以重新對映或刪除特定的 payload 型別對映。事實上,它適用於所有其他路由器,包括基於表示式的路由器,因為它們的計算值現在有機會透過第二步解析為實際的 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 支援。 |