Spring Statemachine 概念及應用
1 Finite-state machine
1.1 狀態機定義
有限狀態機,(英語:Finite-state machine, FSM),又稱有限狀態自動機,簡稱狀態機,是表示有限個狀態以及在這些狀態之間的轉移和動作等行為的數學模型。
有限狀態機體現了兩點:首先是離散的,然後是有限的。
- State:狀態這個詞有些難以定義,狀態儲存關於過去的資訊,就是說它反映從系統開始到現在時刻的輸入變化。
- Actions & Transitions:轉換指示狀態變更,並且用必須滿足來確使轉移發生的條件來描述它。動作是在給定時刻要進行的活動的描述。
- Guards:檢測器出現的原因是為了檢測是否滿足從一個狀態切換到另外一個狀態的條件。
- Event:事件,又見事件,籠統說來,對系統重要的某件事情被稱為事件。
1.2 狀態機示例
現實中的例子:驗票閘門,來自wiki

旋轉門
用於控制地鐵和遊樂園遊樂設施的旋轉門是一個門,在腰高處有三個旋轉臂,一個橫跨入口通道。最初,手臂被鎖定,阻擋了入口,阻止了顧客通過。將硬幣或代幣存放在旋轉門上的槽中可解鎖手臂,允許單個客戶穿過。在顧客通過之後,再次鎖定臂直到插入另一枚硬幣。
旋轉門被視為狀態機,有兩種可能的狀態:鎖定和解鎖。有兩種可能影響其狀態的輸入:將硬幣放入槽(硬幣)並推動手臂(推動)。在鎖定狀態下,推動手臂無效; 無論輸入推送次數多少,它都處於鎖定狀態。投入硬幣 - 即給機器輸入硬幣 - 將狀態從鎖定轉換為解鎖。在解鎖狀態下,放入額外的硬幣無效; 也就是說,給予額外的硬幣輸入不會改變狀態。然而,顧客推動手臂,進行推動輸入,將狀態轉回Locked。
旋轉門狀態機可由狀態轉換表表示,顯示每個可能狀態,它們之間的轉換(基於給予機器的輸入)和每個輸入產生的輸出:
Current State | Input | Next State | Output |
---|---|---|---|
Locked | coin | Unlocked | 解鎖旋轉門,以便遊客能夠通過 |
Locked | push | Locked | None |
Unlocked | coin | Unlocked | None |
Unlocked | push | Locked | 當遊客通過,鎖定旋轉門 |
旋轉柵狀態機也可以由稱為狀態圖的有向圖表示 (上面)。每個狀態由節點(圓圈)表示。邊(箭頭)顯示從一個狀態到另一個狀態的轉換。每個箭頭都標有觸發該轉換的輸入。不引起狀態改變的輸入(例如處於未鎖定狀態的硬幣輸入)由返回到原始狀態的圓形箭頭表示。從黑點進入Locked節點的箭頭表示它是初始狀態。

狀態圖
2 Spring Statemachine
2.1 定位及特色
Spring Statemachine is a framework for application developers to use state machine concepts with Spring applications. Spring Statemachine 是應用程式開發人員在Spring應用程式中使用狀態機概念的框架。
Spring Statemachine 提供如下特色:
- Easy to use flat one level state machine for simple use cases.(易於使用的扁平單級狀態機,用於簡單的使用案例。)
- Hierarchical state machine structure to ease complex state configuration.(分層狀態機結構,以簡化複雜的狀態配置。)
- State machine regions to provide even more complex state configurations.(狀態機區域提供更復雜的狀態配置。)
- Usage of triggers, transitions, guards and actions.(使用觸發器、transitions、guards和actions。)
- Type safe configuration adapter.(應用安全的配置介面卡。)
- Builder pattern for easy instantiation for use outside of Spring Application context(用於在Spring Application上下文之外使用的簡單例項化的生成器模式)
- Recipes for usual use cases(通常用例的手冊)
- Distributed state machine based on a Zookeeper State machine event listeners.(基於Zookeeper的分散式狀態機狀態機事件監聽器。)
- UML Eclipse Papyrus modeling.(UML Eclipse Papyrus 建模)
- Store machine config in a persistent storage.(儲存狀態機配置到持久層)
- Spring IOC/">IOC integration to associate beans with a state machine.(Spring IOC整合將bean與狀態機關聯起來)
2.2 發展及社群
相關資源:
- Spring Statemachine 主頁: ofollow,noindex">Home
- Spring Statemachine Github: Github ,Star:500+ & Fork:200+ (201809)
- Spring Statemachine Gitter: Gitter , less than 100 people
釋出版本:
最新版本: v2.0.2.RELEASE 及 v1.2.12.RELEASE 已經44次版本釋出,更詳盡和最新情況請檢視Github主頁。
3 功能示例
3.1 基本功能
繼續旋轉門的現例項子,新增Maven依賴,本例子中使用版本如下:
<dependency> <groupId>org.springframework.statemachine</groupId> <artifactId>spring-statemachine-core</artifactId> <version>2.0.2.RELEASE</version> </dependency>
定義旋轉門所處狀態:鎖定、解鎖,使用列舉
public enum TurnstileStates { Unlocked, Locked }
定義旋轉門操作事件:推門和投幣,使用列舉
public enum TurnstileEvents { COIN, PUSH }
狀態機配置,其中turnstileUnlock()和customerPassAndLock()即為當前狀態變更後的擴充套件業務操作,可以根據實際業務場景進行修改
@Configuration @EnableStateMachine public class StatemachineConfigurer extends EnumStateMachineConfigurerAdapter<TurnstileStates, TurnstileEvents> { @Override public void configure(StateMachineStateConfigurer<TurnstileStates, TurnstileEvents> states) throws Exception { states .withStates() // 初識狀態:Locked .initial(TurnstileStates.Locked) .states(EnumSet.allOf(TurnstileStates.class)); } @Override public void configure(StateMachineTransitionConfigurer<TurnstileStates, TurnstileEvents> transitions) throws Exception { transitions .withExternal() .source(TurnstileStates.Unlocked).target(TurnstileStates.Locked) .event(TurnstileEvents.COIN).action(customerPassAndLock()) .and() .withExternal() .source(TurnstileStates.Locked).target(TurnstileStates.Unlocked) .event(TurnstileEvents.PUSH).action(turnstileUnlock()) ; } @Override public void configure(StateMachineConfigurationConfigurer<TurnstileStates, TurnstileEvents> config) throws Exception { config.withConfiguration() .machineId("turnstileStateMachine") ; } public Action<TurnstileStates, TurnstileEvents> turnstileUnlock() { return context -> System.out.println("解鎖旋轉門,以便遊客能夠通過" ); } public Action<TurnstileStates, TurnstileEvents> customerPassAndLock() { return context -> System.out.println("當遊客通過,鎖定旋轉門" ); } }
啟動類及測試用例
@SpringBootApplication public class StatemachineApplication implements CommandLineRunner { @Autowired private StateMachine<TurnstileStates, TurnstileEvents> stateMachine; public static void main(String[] args) { SpringApplication.run(StatemachineApplication.class, args); } @Override public void run(String... strings) throws Exception { stateMachine.start(); System.out.println("--- coin ---"); stateMachine.sendEvent(TurnstileEvents.COIN); System.out.println("--- coin ---"); stateMachine.sendEvent(TurnstileEvents.COIN); System.out.println("--- push ---"); stateMachine.sendEvent(TurnstileEvents.PUSH); System.out.println("--- push ---"); stateMachine.sendEvent(TurnstileEvents.PUSH); stateMachine.stop(); } }
結果輸出,與上午所描述的狀態機所描述的內容一致。
--- push --- 解鎖旋轉門,以便遊客能夠通過 --- push --- --- coin --- 當遊客通過,鎖定旋轉門 --- coin ---
3.2 實用功能
3.2.1 狀態儲存
狀態機持久化,實際環境中,當前狀態往往都是從持久化介質中實時獲取的,Spring Statemachine通過實現StateMachinePersist介面,write和read當前狀態機的狀態
本例中,使用的是HashMap作為模擬儲存介質,正式專案中需要使用真實的狀態獲取途徑
@Component public class BizStateMachinePersist implements StateMachinePersist<TurnstileStates, TurnstileEvents, Integer> { static Map<Integer, TurnstileStates> cache = new HashMap<>(16); @Override public void write(StateMachineContext<TurnstileStates, TurnstileEvents> stateMachineContext, Integer integer) throws Exception { cache.put(integer, stateMachineContext.getState()); } @Override public StateMachineContext<TurnstileStates, TurnstileEvents> read(Integer integer) throws Exception { // 注意狀態機的初識狀態與配置中定義的一致 return cache.containsKey(integer) ? new DefaultStateMachineContext<>(cache.get(integer), null, null, null, null, "turnstileStateMachine") : new DefaultStateMachineContext<>(TurnstileStates.Locked, null, null, null, null, "turnstileStateMachine"); } }
在StatemachineConfigurer中釋出
@Autowired private BizStateMachinePersist bizStateMachinePersist; @Bean public StateMachinePersister<TurnstileStates, TurnstileEvents, Integer> stateMachinePersist() { return new DefaultStateMachinePersister<>(bizStateMachinePersist); }
在StatemachineApplication中使用,自動注入StateMachinePersister物件,測試用例如下
stateMachine.start(); stateMachinePersist.restore(stateMachine, 1); System.out.println("--- push ---"); stateMachine.sendEvent(TurnstileEvents.PUSH); stateMachinePersist.persist(stateMachine, 1); stateMachinePersist.restore(stateMachine, 1); System.out.println("--- push ---"); stateMachine.sendEvent(TurnstileEvents.PUSH); stateMachinePersist.persist(stateMachine, 1); stateMachinePersist.restore(stateMachine, 1); System.out.println("--- coin ---"); stateMachine.sendEvent(TurnstileEvents.COIN); stateMachinePersist.persist(stateMachine, 1); stateMachinePersist.restore(stateMachine, 1); System.out.println("--- coin ---"); stateMachine.sendEvent(TurnstileEvents.COIN); stateMachinePersist.persist(stateMachine, 1); stateMachine.stop();
3.2.2 動作監聽
定義動作監聽類,StatemachineMonitor(名稱隨意),添加註解 @WithStateMachine
。本例中使用id進行狀態機繫結,根據文件定義,可以使用name和id兩種屬性繫結需要監聽的狀態機例項。如果不定義任何name或者id,預設監聽名稱為 stateMachine
的狀態機。
@WithStateMachine(id = "turnstileStateMachine") public class StatemachineMonitor { @OnTransition public void anyTransition() { System.out.println("--- OnTransition --- init"); } @OnTransition(target = "Unlocked") public void toState1() { System.out.println("--- OnTransition --- toState1"); } @OnStateChanged(source = "Unlocked") public void fromState1() { System.out.println("--- OnTransition --- fromState1"); } }
其他Context的事件監聽,後續文章進行描述, 官網連結
3.2.2 狀態機工程
實際業務環境中,往往是多執行緒處理不同的業務ID對應的狀態,狀態機中利用事件的context傳遞資料,會出現多執行緒問題,需要利用狀態機工程,利用UUID建立不同狀態機。
在StatemachineConfigurer類中,修改 @EnableStateMachine
為 @EnableStateMachineFactory
,同時新增狀態機處理動作封裝方法,讀者可以根據業務場景定製,本例為一種可行方案
@Service public class StatemachineService { @Autowired private StateMachinePersister<TurnstileStates, TurnstileEvents, Integer> stateMachinePersist; @Autowired private StateMachineFactory<TurnstileStates, TurnstileEvents> stateMachineFactory; public void execute(Integer businessId, TurnstileEvents event, Map<String, Object> context) { // 利用隨記ID建立狀態機,建立時沒有與具體定義狀態機繫結 StateMachine<TurnstileStates, TurnstileEvents> stateMachine = stateMachineFactory.getStateMachine(UUID.randomUUID()); stateMachine.start(); try { // 在BizStateMachinePersist的restore過程中,繫結turnstileStateMachine狀態機相關事件監聽 stateMachinePersist.restore(stateMachine, businessId); // 本處寫法較為繁瑣,實際為注入Map<String, Object> context內容到message中 MessageBuilder<TurnstileEvents> messageBuilder = MessageBuilder .withPayload(event) .setHeader("BusinessId", businessId); if (context != null) { context.entrySet().forEach(p -> messageBuilder.setHeader(p.getKey(), p.getValue())); } Message<TurnstileEvents> message = messageBuilder.build(); // 傳送事件,返回是否執行成功 boolean success = stateMachine.sendEvent(message); if (success) { stateMachinePersist.persist(stateMachine, businessId); } else { System.out.println("狀態機處理未執行成功,請處理,ID:" + businessId + ",當前context:" + context); } } catch (Exception e) { e.printStackTrace(); } finally { stateMachine.stop(); } } }
完整示例參見: 連結