前言
在日常開發中經常遇到運營審核經銷商活動、任務等等類似業務需求,大部分需求中狀態穩定且單一無需使用狀態機,但是也會出現大量的if...else前置狀態代碼,也是不夠那么的“優雅”。隨著業務的發展、需求迭代,每一次的業務代碼改動都需要維護使用到狀態的代碼,更讓開發人員頭疼的是這些維護狀態的代碼,像散彈一樣遍布在各個Service的方法中,不僅增加發布的風險,同時也增加了回歸測試的工作量。
1. 什么是狀態機?
通常所說的狀態機為有限狀態機(英語:finite-state machine,縮寫:FSM),簡稱狀態機, 是表示有限個狀態以及在這些狀態之間的轉移和動作等行為的數學模型。
應用FSM模型可以幫助對象生命周期的狀態的順序以及導致狀態變化的事件進行管理。 將狀態和事件控制從不同的業務Service方法的if else中抽離出來。FSM的應用范圍很廣,狀態機 可以描述核心業務規則,核心業務內容. 無限狀態機,顧名思義狀態無限,類似于“π”,暫不做研究。
狀態機可歸納為4個要素,即現態、條件、動作、次態。這樣的歸納,主要是出于對狀態機的內在因果關系的考慮。“現態”和“條件”是因,“動作”和“次態”是果。詳解如下:
現態:是指當前所處的狀態。
條件:又稱為“事件”,當一個條件被滿足,將會觸發一個動作,或者執行一次狀態的遷移。
動作:條件滿足后執行的動作。動作執行完畢后,可以遷移到新的狀態,也可以仍舊保持原狀態。動作不是必需的,當條件滿足后,也可以不 執行任何動作,直接遷移到新狀態。
次態:條件滿足后要遷往的新狀態。“次態”是相對于“現態”而言的,“次態”一旦被激活,就轉變成新的“現態”了。
動作是在給定時刻要進行的活動的描述。有多種類型的動作:
進入動作(entry action):在進入狀態時進行
退出動作(exit action):在退出狀態時進行
輸入動作:依賴于當前狀態和輸入條件進行
轉移動作:在進行特定轉移時進行
其他術語:
Transition: 狀態轉移節點,是組成狀態機引擎的核心。
source/from:現態。
target/to:次態。
event/trigger:觸發節點從現態轉移到次態的動作,這里也可能是一個timer。
guard/when:狀態遷移前的校驗,執行于action前。
action:用于實現當前節點對應的業務邏輯處理。
文字描述比較不容易理解,讓我們舉個栗子:每天上班都需要坐地鐵,從刷卡進站到閘機關閉這個過程,將閘機抽象為一個狀態機模型,如下圖:
2. 什么場景使用?
以下的場景您可能會需要使用:
您可以將應用程序或其結構的一部分表示為狀態。
您希望將復雜的邏輯拆分為更小的可管理任務。
應用程序已經遇到了并發問題,例如異步執行導致了一些異常情況。
當您執行以下操作時,您已經在嘗試實現狀態機:
使用布爾標志或枚舉來建模情況。
具有僅對應用程序生命周期的某些部分有意義的變量。
在if...else結構(或者更糟糕的是,多個這樣的結構)中循環,檢查是否設置了特定的標志或枚舉,然后在標志和枚舉的某些組合存在或不存在時,做出進一步的異常處理。
3. 為什么要用?有哪些好處?
最初活動模塊功能設計時,并沒有想使用狀態機,僅僅想把狀態的變更和業務剝離開,規范狀態轉換和程序在不同狀態下所能提供的能力,去掉復雜的邏輯判斷也就是if...else,想換一種模式實現思路,此前了解過spring“全家桶”有狀態機就想到了“它”,場景也符合。
從個人使用的經驗,開發階段和迭代維護期總結了以下幾點:
使用狀態機來管理狀態好處更多體現在代碼的可維護性、對于流程復雜易變的業務場景能大大減輕維護和測試的難度。
解耦,業務邏輯與狀態流程隔離,避免業務與狀態“散彈式”維護,且狀態持久化在同一個事務。
狀態流轉越復雜,越能體現狀態流轉的邏輯清晰,減少的“膠水”代碼也越多。
4. 實踐
JAVA語言狀態機框架有很多,目前Github star 數比較多的有 spring-statemachine(star 1.3K) 、squirrel-foundation(star1.9K)即“松鼠”狀態機,stateless4j相較前兩個名氣較小,未深入研究。spring-statemachine是spring官方提供的狀態機實現,功能強大,但是相對來說很“重”,加載實例的時間也長于squirrel-foundation,不過好在一直都是有更新(目前官方已更新3.2.0),相信會越來越成熟。
實際生產中使用的是spring statemachine ,版本是2.2.0.RELEASE。線下對比使用的是squirrel-foundation,版本是0.3.10。這里僅供使用對比。
從創建活動到活動下線狀態流轉作為示例,如下圖:
pom
<?xml versinotallow="1.0" encoding="utf-8" ?>
<!-- spring statemachine -->
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-starter</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
<!-- spring statemachine context 序列化 -->
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-kryo</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
<!-- squirrel-foundation -->
<dependency>
<groupId>org.squirrelframework</groupId>
<artifactId>squirrel-foundation</artifactId>
<version>0.3.10</version>
</dependency>
狀態&事件定義
public enum State {
INIT("初始化"),
DRAFT("草稿"),
WAIT_VERIFY("待審核"),
PASSED("審核通過"),
REJECTED("已駁回"),
//已發起上線操作,未到上線時間的狀態
WAIT_ONLIE("待上線"),
ONLINED("已上線"),
//過渡狀態無實際意義,無需事件觸發
OFFLINING("下線中"),
OFFLINED("已下線"),
FINISHED("已結束");
private final String desc;
}
public enum Event {
SAVE("保存草稿"),
SUBMIT("提交審核"),
PASS("審核通過"),
REJECT("提交駁回"),
ONLINE("上線"),
OFFLINE("下線"),
FINISH("結束");
private final String desc;
}
狀態流轉定義
@Configuration
@EnableStateMachineFactory
public class ActivitySpringStateMachineAutoConfiguration extends StateMachineConfigurerAdapter<State, Event> {
@Autowired
private ApplicationContext applicationContext;
@Autowired
private StateMachineRuntimePersister<State, Event, String> activityStateMachinePersister;
@Bean
public StateMachineService<State, Event> activityStateMachineService(StateMachineFactory<State, Event> stateMachineFactory) {
return new DefaultStateMachineService<>(stateMachineFactory, activityStateMachinePersister);
}
@Override
public void configure(StateMachineConfigurationConfigurer<State, Event> config) throws Exception {
// @formatter:off
config
.withPersistence()
.runtimePersister(activityStateMachinePersister)
.and().withConfiguration()
.stateDoActionPolicy(StateDoActionPolicy.TIMEOUT_CANCEL)
.stateDoActionPolicyTimeout(300, TimeUnit.SECONDS)
.autoStartup(false);
// @formatter:on
}
@Override
public void configure(StateMachineStateConfigurer<State, Event> states) throws Exception {
states.withStates()
.initial(State.INIT)
.choice(State.OFFLINING)
.states(EnumSet.allOf(State.class));
}
@Override
public void configure(StateMach.NETransitionConfigurer<State, Event> transitions) throws Exception {
// 待提交審核 --提交審核--> 待審核
// @formatter:off
// 現態-->事件-->次態
transitions.withExternal()
.source(State.INIT).target(State.DRAFT).event(Event.SAVE)
.and().withExternal()
.source(State.DRAFT).target(State.WAIT_VERIFY).event(Event.SUBMIT)
.guard(applicationContext.getBean(SubmitCondition.class));
transitions.withExternal().source(State.WAIT_VERIFY).target(State.PASSED).event(Event.PASS)
.action(applicationContext.getBean(PassAction.class));
transitions.withExternal().source(State.WAIT_VERIFY).target(State.REJECTED).event(Event.REJECT)
.guard(applicationContext.getBean(RejectCondition.class));
transitions.withExternal()
.source(State.REJECTED)
.target(State.WAIT_VERIFY)
.event(Event.SUBMIT)
.guard(applicationContext.getBean(SubmitCondition.class));
// 審核通過-->上線-->待上線
transitions.withExternal().source(State.PASSED).target(State.WAIT_ONLIE).event(Event.ONLINE);
// 待上線-->上線-->已上線
transitions.withExternal().source(State.WAIT_ONLIE).target(State.ONLINED).event(Event.ONLINE);
// 已上線-->下線-->已下線
transitions.withExternal()
.source(State.ONLINED).target(State.OFFLINING).event(Event.OFFLINE);
// 待上線-->下線-->下線中
transitions.withExternal()
.source(State.WAIT_ONLIE).target(State.OFFLINING).event(Event.OFFLINE)
.and()
// 已下線-->結束-->已結束
.withChoice()
.source(State.OFFLINING)
.first(State.FINISHED, new Guard<State, Event>() {
@Override
public boolean evaluate(StateContext<State, Event> context) {
return true;
}
})
.last(State.OFFLINED);
// @formatter:on
}
}
說明:
- 多個狀態節點配置可用.and()串聯。
- withExternal是當現態和次態不相同時使用。
- withChoice是當執行一個動作,當前狀態(瞬時狀態)可能遷移不同的的狀態,此時可以使用Choice和Guard組合使用,且無需事件觸發。相當于if...else的分支狀態功能。
- StateMachineService 這個類是spring statemachine自帶的接口,用于獲取和釋放一個狀態機的輔助service,依賴狀態機工廠和持久化 實例, 但由于默認實現 StateMachinePersist< S, E, String> 規定了StateMachineContext的泛型為String 類型,故而持久層的參數contextObj 為string 類型,實際是狀態機的id。
- 持久化 spring-statemachine官方支持MongoDB和redis持久化存儲,開發無需關心狀態持久化,但是存在業務數據存儲和狀態存儲事務的問題, 這里需要自己實現(StateMachineRuntimePersister)持久化以存儲狀態。
- 上下文傳遞時都使用的StateMachineContext,其內部包含StateMachine實例,可以通過增加StateMachine實例擴展參數傳遞參數。
Guard與Action
@Component
public class SaveGuard implements Guard<State, Event> {
@Override
public boolean evaluate(StateContext<State, Event> context) {
log.info("[execute save guard]");
return true;
}
}
@Component
public class SaveAction implements Action<State, Event> {
@Override
public void execute(StateContext<State, Event> context) {
try {
log.info("[execute saveAction]");
} catch (Exception e) {
context.getExtendedState().getVariables().put("ERROR", e.getMessage());
}
}
}
說明:
- Guard 門衛,條件判斷返回true時再執行狀態轉移,可以做業務前置校驗。
持久化配置
@Component
public class ActivityStateMachinePersister extends AbstractStateMachineRuntimePersister<State, Event, String> {
@Autowired
private ActivityStateService activityStateService;
@Override
public void write(StateMachineContext<State, Event> context, String id) {
Activity state = new Activity();
state.setMachineId(id);
state.setState(context.getState());
activityStateService.save(state);
}
@Override
public StateMachineContext<State, Event> read(String id) {
return deserialize(activityStateService.getContextById(id));
}
}
說明:
- AbstractStateMachineRuntimePersister 繼承AbstractPersistingStateMachineInterceptor 并實現了StateMachineRuntimePersister接口, AbstractPersistingStateMachineInterceptor主要攔截狀態變更時的狀態監聽。不同于StateMachineListener被動監聽,interceptor擁有可以改變狀態變化鏈的能力。
- 序列化存儲實現參考了spring-statemachine-data-redis的實現。
狀態服務調用
@Service
public class StateTransitService {
@Autowired
private StateMachineService<State, Event> stateMachineService;
@Transactional
public void transimit(String machineId, Message<Event> message) {
StateMachine<State, Event> stateMachine = stateMachineService.acquireStateMachine(machineId);
stateMachine.addStateListener(new DefaultStateMachineListener<>(stateMachine));
stateMachine.sendEvent(message);
if (stateMachine.hasStateMachineError()) {
String errorMessage = stateMachine.getExtendedState().get("message", String.class);
stateMachineService.releaseStateMachine(machineId);
throw new ResponseException(errorMessage);
}
}
}
@AllArgsConstructor
public class DefaultStateMachineListener<S, E> extends StateMachineListenerAdapter<S, E> {
private final StateMachine<S, E> stateMachine;
@Override
public void eventNotAccepted(Message<E> event) {
stateMachine.getExtendedState().getVariables().put("message", "當前狀態不滿足執行條件");
stateMachine.setStateMachineError(new ResponseException(500, "Event not accepted"));
}
@Override
public void transitionEnded(Transition<S, E> transition) {
log.info("source {} to {}", transition.getSource().getId(), transition.getTarget().getId());
}
}
說明:
- Message為發送事件的載體,其內部封裝了消息體、事件等上下文擴展參數。
- StateMachineListenerAdapter為默認監聽接口的空實現,依據業務需要重寫監聽的方法。
- eventNotAccepted此為事件未正確執行時的監聽器。
集成單元測試
@SpringBootTest
@RunWith(SpringRunner.class)
public class StateMachineITest {
@Autowired
private StateTransitService transmitService;
@Autowired
private ActivityStateService activityStateService;
@Test
public void test() {
String machineId = "test";//業務主鍵ID
transmitService.transimit(machineId, MessageBuilder.withPayload(Event.SAVE).build());
transmitService.transimit(machineId, MessageBuilder.withPayload(Event.SUBMIT).build());
transmitService.transimit(machineId, MessageBuilder.withPayload(Event.PASS).build());
transmitService.transimit(machineId, MessageBuilder.withPayload(Event.ONLINE).build());
transmitService.transimit(machineId, MessageBuilder.withPayload(Event.ONLINE).build());
transmitService.transimit(machineId, MessageBuilder.withPayload(Event.OFFLINE).build());
assert activityStateService.getStateById(machineId).equals(State.FINISHED);
}
}
注意事項
- 由于框架中每次都是加載一個狀態機內存實例,所以在執行狀態轉移相關代碼時一定要加分布式鎖!!!建議狀態維護提供統一調用service, 開啟事務、處理異常。
- spring-statemachine異常包裝比較另類,如guard、action以及listener中發生異常,狀態機會捕獲并把異常信息捕獲為警告,狀態也能夠成功轉移到次態,這顯然不符合 我們的需求,所以調用后需要手動判斷是否發生異常stateMachine.hasStateMachineError(),但statemachine并沒有給提供獲取異常信息的接口,所以在guard 和action中將異常信息用變量的方式解決此問題,stateMachine.getExtendedState().getVariables().put("message", "當前狀態不滿足執行條件");
- @EnableStateMachineFactory開啟工廠模式,然后通過StateMachineService從持久化層加載一個狀態機實例。
- 當一個project中有多個業務狀態機時,@EnableStateMachineFactory(name = "xxx")為工廠配置名稱以區別不同的業務狀態機。
- 當使用withChoice()時,一定要在配置StateMachineStateConfigurer.choice()配置分支狀態,否則將不生效。
擴展-與squirrel-foundation異同
@Component
public class ActivityMachine extends SquirrelStateMachine<ActivityMachine, State, Event, TransmitCmd> {
private final ActivityStateService activityStateService;
public ActivityMachine(ApplicationContext applicationContext) {
super(applicationContext);
activityStateService = applicationContext.getBean(ActivityStateService.class);
}
@Override
public void buildStateMachine(StateMachineBuilder<ActivityMachine, State, Event, TransmitCmd> stateMachineBuilder) {
stateMachineBuilder.externalTransition().from(State.INIT).to(State.DRAFT).on(Event.SAVE).when(applicationContext.getBean(SubmitCondition.class));
//以下省略,大致與spring-statemachine相同
}
@Override
public ActivityMachine createStateMachine(State stateId) {
ActivityMachine activityMachine = super.createStateMachine(stateId);
activityMachine.addStartListener(new StartListener<ActivityMachine, State, Event, TransmitCmd>() {
});
return activityMachine;
}
@Override
protected void afterTransitionDeclined(S fromState, E event, C context) {
//轉移狀態未執行
}
@Override
protected void afterTransitionCausedException(S fromState, S toState, E event, C context) {
// 轉移狀態時發生異常
}
@Override
protected void afterTransitionCompleted(State fromState, State toState, Event event, TransmitCmd context) {
log.info("from {} to {} on {}, {}", fromState.getDesc(), toState.getDesc(), event.getDesc(), context);
}
}
說明:
- squirrel-foundation直接可繼承AbstractStateMachine實例化狀態機,配置上大體相同只是使用的是from、to、on、when詞不同,框架builder的約束太強。
- 不支持choice分支狀態。
- 狀態機異常處理afterTransitionCausedException相比spring-statemachine更加方便、易用。
- 狀態的持久化通過重寫afterTransitionCompleted方法即可。
5.使用后的效果如何?
以下是在開發和迭代維護期間,真切體會到狀態機帶來好處的兩個小場景。
- 由于新項目中涉及到跨部門卡券業務,在開發初期審核活動通過時同步創建卡券批次,卻忽略了異步生成券碼的時間,隨著開發的深入才意識到此問題。此時只需要在狀態審核通過時加一個過渡狀態并啟動一個任務去輪詢券碼是否創建完成即可,絲毫不影響已開發的代碼。
- 最初的需求設計時,活動下線后是不能再次上線的,在需求迭代期內又增加了再次上線的功能,狀態機流轉邏輯清晰,只需要再增加個狀態配置流轉事件就行,就為狀態機賦予了再次上線的能力。
6.總結
在實踐的過程中,在spring-statemachine官方文檔結合google摸索使用的過程中,遇到持久化存儲StateMachineContext、異常處理,以及狀態分支等問題。目前回頭看來也不復雜,如今寫出來總結一下,希望對小伙伴們有所幫助。
最后建議在狀態流程不是很復雜的情況,如果您也厭煩了if...else,那么不妨嘗試一下squirrel-foundation,相信也是不錯的選擇。
參考文獻
- ??https://baike.baidu.com/item/%E7%8A%B6%E6%80%81%E6%9C%BA/6548513?fr=aladdin??
- ??https://zh.wikipedia.org/wiki/%E6%9C%89%E9%99%90%E7%8A%B6%E6%80%81%E6%9C%BA??
- ??https://spring.io/projects/spring-statemachine#learn??
- ??http://hekailiang.github.io/squirrel/??
作者簡介
姜強強
■ 經銷商技術部-商業資源團隊。
■ 2016年加入汽車之家,目前主要負責經銷商事業部內創新商業項目的研發工作,熱衷于業內新技術的探索與實踐。