前言
在日常開發(fā)中經(jīng)常遇到運(yùn)營審核經(jīng)銷商活動(dòng)、任務(wù)等等類似業(yè)務(wù)需求,大部分需求中狀態(tài)穩(wěn)定且單一無需使用狀態(tài)機(jī),但是也會(huì)出現(xiàn)大量的if...else前置狀態(tài)代碼,也是不夠那么的“優(yōu)雅”。隨著業(yè)務(wù)的發(fā)展、需求迭代,每一次的業(yè)務(wù)代碼改動(dòng)都需要維護(hù)使用到狀態(tài)的代碼,更讓開發(fā)人員頭疼的是這些維護(hù)狀態(tài)的代碼,像散彈一樣遍布在各個(gè)Service的方法中,不僅增加發(fā)布的風(fēng)險(xiǎn),同時(shí)也增加了回歸測試的工作量。
1. 什么是狀態(tài)機(jī)?
通常所說的狀態(tài)機(jī)為有限狀態(tài)機(jī)(英語:finite-state machine,縮寫:FSM),簡稱狀態(tài)機(jī), 是表示有限個(gè)狀態(tài)以及在這些狀態(tài)之間的轉(zhuǎn)移和動(dòng)作等行為的數(shù)學(xué)模型。
應(yīng)用FSM模型可以幫助對(duì)象生命周期的狀態(tài)的順序以及導(dǎo)致狀態(tài)變化的事件進(jìn)行管理。 將狀態(tài)和事件控制從不同的業(yè)務(wù)Service方法的if else中抽離出來。FSM的應(yīng)用范圍很廣,狀態(tài)機(jī) 可以描述核心業(yè)務(wù)規(guī)則,核心業(yè)務(wù)內(nèi)容. 無限狀態(tài)機(jī),顧名思義狀態(tài)無限,類似于“π”,暫不做研究。
狀態(tài)機(jī)可歸納為4個(gè)要素,即現(xiàn)態(tài)、條件、動(dòng)作、次態(tài)。這樣的歸納,主要是出于對(duì)狀態(tài)機(jī)的內(nèi)在因果關(guān)系的考慮?!艾F(xiàn)態(tài)”和“條件”是因,“動(dòng)作”和“次態(tài)”是果。詳解如下:
現(xiàn)態(tài):是指當(dāng)前所處的狀態(tài)。
條件:又稱為“事件”,當(dāng)一個(gè)條件被滿足,將會(huì)觸發(fā)一個(gè)動(dòng)作,或者執(zhí)行一次狀態(tài)的遷移。
動(dòng)作:條件滿足后執(zhí)行的動(dòng)作。動(dòng)作執(zhí)行完畢后,可以遷移到新的狀態(tài),也可以仍舊保持原狀態(tài)。動(dòng)作不是必需的,當(dāng)條件滿足后,也可以不 執(zhí)行任何動(dòng)作,直接遷移到新狀態(tài)。
次態(tài):條件滿足后要遷往的新狀態(tài)?!按螒B(tài)”是相對(duì)于“現(xiàn)態(tài)”而言的,“次態(tài)”一旦被激活,就轉(zhuǎn)變成新的“現(xiàn)態(tài)”了。
動(dòng)作是在給定時(shí)刻要進(jìn)行的活動(dòng)的描述。有多種類型的動(dòng)作:
進(jìn)入動(dòng)作(entry action):在進(jìn)入狀態(tài)時(shí)進(jìn)行
退出動(dòng)作(exit action):在退出狀態(tài)時(shí)進(jìn)行
輸入動(dòng)作:依賴于當(dāng)前狀態(tài)和輸入條件進(jìn)行
轉(zhuǎn)移動(dòng)作:在進(jìn)行特定轉(zhuǎn)移時(shí)進(jìn)行
其他術(shù)語:
Transition: 狀態(tài)轉(zhuǎn)移節(jié)點(diǎn),是組成狀態(tài)機(jī)引擎的核心。
source/from:現(xiàn)態(tài)。
target/to:次態(tài)。
event/trigger:觸發(fā)節(jié)點(diǎn)從現(xiàn)態(tài)轉(zhuǎn)移到次態(tài)的動(dòng)作,這里也可能是一個(gè)timer。
guard/when:狀態(tài)遷移前的校驗(yàn),執(zhí)行于action前。
action:用于實(shí)現(xiàn)當(dāng)前節(jié)點(diǎn)對(duì)應(yīng)的業(yè)務(wù)邏輯處理。
文字描述比較不容易理解,讓我們舉個(gè)栗子:每天上班都需要坐地鐵,從刷卡進(jìn)站到閘機(jī)關(guān)閉這個(gè)過程,將閘機(jī)抽象為一個(gè)狀態(tài)機(jī)模型,如下圖:

2. 什么場景使用?
以下的場景您可能會(huì)需要使用:
您可以將應(yīng)用程序或其結(jié)構(gòu)的一部分表示為狀態(tài)。
您希望將復(fù)雜的邏輯拆分為更小的可管理任務(wù)。
應(yīng)用程序已經(jīng)遇到了并發(fā)問題,例如異步執(zhí)行導(dǎo)致了一些異常情況。
當(dāng)您執(zhí)行以下操作時(shí),您已經(jīng)在嘗試實(shí)現(xiàn)狀態(tài)機(jī):
使用布爾標(biāo)志或枚舉來建模情況。
具有僅對(duì)應(yīng)用程序生命周期的某些部分有意義的變量。
在if...else結(jié)構(gòu)(或者更糟糕的是,多個(gè)這樣的結(jié)構(gòu))中循環(huán),檢查是否設(shè)置了特定的標(biāo)志或枚舉,然后在標(biāo)志和枚舉的某些組合存在或不存在時(shí),做出進(jìn)一步的異常處理。
3. 為什么要用?有哪些好處?
最初活動(dòng)模塊功能設(shè)計(jì)時(shí),并沒有想使用狀態(tài)機(jī),僅僅想把狀態(tài)的變更和業(yè)務(wù)剝離開,規(guī)范狀態(tài)轉(zhuǎn)換和程序在不同狀態(tài)下所能提供的能力,去掉復(fù)雜的邏輯判斷也就是if...else,想換一種模式實(shí)現(xiàn)思路,此前了解過spring“全家桶”有狀態(tài)機(jī)就想到了“它”,場景也符合。
從個(gè)人使用的經(jīng)驗(yàn),開發(fā)階段和迭代維護(hù)期總結(jié)了以下幾點(diǎn):
使用狀態(tài)機(jī)來管理狀態(tài)好處更多體現(xiàn)在代碼的可維護(hù)性、對(duì)于流程復(fù)雜易變的業(yè)務(wù)場景能大大減輕維護(hù)和測試的難度。
解耦,業(yè)務(wù)邏輯與狀態(tài)流程隔離,避免業(yè)務(wù)與狀態(tài)“散彈式”維護(hù),且狀態(tài)持久化在同一個(gè)事務(wù)。
狀態(tài)流轉(zhuǎn)越復(fù)雜,越能體現(xiàn)狀態(tài)流轉(zhuǎn)的邏輯清晰,減少的“膠水”代碼也越多。
4. 實(shí)踐
java語言狀態(tài)機(jī)框架有很多,目前github star 數(shù)比較多的有 spring-statemachine(star 1.3K) 、squirrel-foundation(star1.9K)即“松鼠”狀態(tài)機(jī),stateless4j相較前兩個(gè)名氣較小,未深入研究。spring-statemachine是spring官方提供的狀態(tài)機(jī)實(shí)現(xiàn),功能強(qiáng)大,但是相對(duì)來說很“重”,加載實(shí)例的時(shí)間也長于squirrel-foundation,不過好在一直都是有更新(目前官方已更新3.2.0),相信會(huì)越來越成熟。
實(shí)際生產(chǎn)中使用的是spring statemachine ,版本是2.2.0.RELEASE。線下對(duì)比使用的是squirrel-foundation,版本是0.3.10。這里僅供使用對(duì)比。
從創(chuàng)建活動(dòng)到活動(dòng)下線狀態(tài)流轉(zhuǎn)作為示例,如下圖:

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>
狀態(tài)&事件定義
public enum State {
INIT("初始化"),
DRAFT("草稿"),
WAIT_VERIFY("待審核"),
PASSED("審核通過"),
REJECTED("已駁回"),
//已發(fā)起上線操作,未到上線時(shí)間的狀態(tài)
WAIT_ONLIE("待上線"),
ONLINED("已上線"),
//過渡狀態(tài)無實(shí)際意義,無需事件觸發(fā)
OFFLINING("下線中"),
OFFLINED("已下線"),
FINISHED("已結(jié)束");
private final String desc;
}
public enum Event {
SAVE("保存草稿"),
SUBMIT("提交審核"),
PASS("審核通過"),
REJECT("提交駁回"),
ONLINE("上線"),
OFFLINE("下線"),
FINISH("結(jié)束");
private final String desc;
}
狀態(tài)流轉(zhuǎn)定義
@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(StateMachineTransitionConfigurer<State, Event> transitions) throws Exception {
// 待提交審核 --提交審核--> 待審核
// @formatter:off
// 現(xiàn)態(tài)-->事件-->次態(tài)
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()
// 已下線-->結(jié)束-->已結(jié)束
.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
}
}
說明:
- 多個(gè)狀態(tài)節(jié)點(diǎn)配置可用.and()串聯(lián)。
- withExternal是當(dāng)現(xiàn)態(tài)和次態(tài)不相同時(shí)使用。
- withChoice是當(dāng)執(zhí)行一個(gè)動(dòng)作,當(dāng)前狀態(tài)(瞬時(shí)狀態(tài))可能遷移不同的的狀態(tài),此時(shí)可以使用Choice和Guard組合使用,且無需事件觸發(fā)。相當(dāng)于if...else的分支狀態(tài)功能。
- StateMachineService 這個(gè)類是spring statemachine自帶的接口,用于獲取和釋放一個(gè)狀態(tài)機(jī)的輔助service,依賴狀態(tài)機(jī)工廠和持久化 實(shí)例, 但由于默認(rèn)實(shí)現(xiàn) StateMachinePersist< S, E, String> 規(guī)定了StateMachineContext的泛型為String 類型,故而持久層的參數(shù)contextObj 為string 類型,實(shí)際是狀態(tài)機(jī)的id。
- 持久化 spring-statemachine官方支持MongoDB和Redis持久化存儲(chǔ),開發(fā)無需關(guān)心狀態(tài)持久化,但是存在業(yè)務(wù)數(shù)據(jù)存儲(chǔ)和狀態(tài)存儲(chǔ)事務(wù)的問題, 這里需要自己實(shí)現(xiàn)(StateMachineRuntimePersister)持久化以存儲(chǔ)狀態(tài)。
- 上下文傳遞時(shí)都使用的StateMachineContext,其內(nèi)部包含StateMachine實(shí)例,可以通過增加StateMachine實(shí)例擴(kuò)展參數(shù)傳遞參數(shù)。
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 門衛(wèi),條件判斷返回true時(shí)再執(zhí)行狀態(tài)轉(zhuǎn)移,可以做業(yè)務(wù)前置校驗(yàn)。
持久化配置
@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 并實(shí)現(xiàn)了StateMachineRuntimePersister接口, AbstractPersistingStateMachineInterceptor主要攔截狀態(tài)變更時(shí)的狀態(tài)監(jiān)聽。不同于StateMachineListener被動(dòng)監(jiān)聽,interceptor擁有可以改變狀態(tài)變化鏈的能力。
- 序列化存儲(chǔ)實(shí)現(xiàn)參考了spring-statemachine-data-redis的實(shí)現(xiàn)。
狀態(tài)服務(wù)調(diào)用
@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", "當(dāng)前狀態(tài)不滿足執(zhí)行條件");
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為發(fā)送事件的載體,其內(nèi)部封裝了消息體、事件等上下文擴(kuò)展參數(shù)。
- StateMachineListenerAdapter為默認(rèn)監(jiān)聽接口的空實(shí)現(xiàn),依據(jù)業(yè)務(wù)需要重寫監(jiān)聽的方法。
- eventNotAccepted此為事件未正確執(zhí)行時(shí)的監(jiān)聽器。
集成單元測試
@SpringBootTest
@RunWith(SpringRunner.class)
public class StateMachineITest {
@Autowired
private StateTransitService transmitService;
@Autowired
private ActivityStateService activityStateService;
@Test
public void test() {
String machineId = "test";//業(yè)務(wù)主鍵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);
}
}
注意事項(xiàng)
- 由于框架中每次都是加載一個(gè)狀態(tài)機(jī)內(nèi)存實(shí)例,所以在執(zhí)行狀態(tài)轉(zhuǎn)移相關(guān)代碼時(shí)一定要加分布式鎖?。?!建議狀態(tài)維護(hù)提供統(tǒng)一調(diào)用service, 開啟事務(wù)、處理異常。
- spring-statemachine異常包裝比較另類,如guard、action以及l(fā)istener中發(fā)生異常,狀態(tài)機(jī)會(huì)捕獲并把異常信息捕獲為警告,狀態(tài)也能夠成功轉(zhuǎn)移到次態(tài),這顯然不符合 我們的需求,所以調(diào)用后需要手動(dòng)判斷是否發(fā)生異常stateMachine.hasStateMachineError(),但statemachine并沒有給提供獲取異常信息的接口,所以在guard 和action中將異常信息用變量的方式解決此問題,stateMachine.getExtendedState().getVariables().put("message", "當(dāng)前狀態(tài)不滿足執(zhí)行條件");
- @EnableStateMachineFactory開啟工廠模式,然后通過StateMachineService從持久化層加載一個(gè)狀態(tài)機(jī)實(shí)例。
- 當(dāng)一個(gè)project中有多個(gè)業(yè)務(wù)狀態(tài)機(jī)時(shí),@EnableStateMachineFactory(name = "xxx")為工廠配置名稱以區(qū)別不同的業(yè)務(wù)狀態(tài)機(jī)。
- 當(dāng)使用withChoice()時(shí),一定要在配置StateMachineStateConfigurer.choice()配置分支狀態(tài),否則將不生效。
擴(kuò)展-與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) {
//轉(zhuǎn)移狀態(tài)未執(zhí)行
}
@Override
protected void afterTransitionCausedException(S fromState, S toState, E event, C context) {
// 轉(zhuǎn)移狀態(tài)時(shí)發(fā)生異常
}
@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實(shí)例化狀態(tài)機(jī),配置上大體相同只是使用的是from、to、on、when詞不同,框架builder的約束太強(qiáng)。
- 不支持choice分支狀態(tài)。
- 狀態(tài)機(jī)異常處理afterTransitionCausedException相比spring-statemachine更加方便、易用。
- 狀態(tài)的持久化通過重寫afterTransitionCompleted方法即可。
5.使用后的效果如何?
以下是在開發(fā)和迭代維護(hù)期間,真切體會(huì)到狀態(tài)機(jī)帶來好處的兩個(gè)小場景。
- 由于新項(xiàng)目中涉及到跨部門卡券業(yè)務(wù),在開發(fā)初期審核活動(dòng)通過時(shí)同步創(chuàng)建卡券批次,卻忽略了異步生成券碼的時(shí)間,隨著開發(fā)的深入才意識(shí)到此問題。此時(shí)只需要在狀態(tài)審核通過時(shí)加一個(gè)過渡狀態(tài)并啟動(dòng)一個(gè)任務(wù)去輪詢?nèi)a是否創(chuàng)建完成即可,絲毫不影響已開發(fā)的代碼。
- 最初的需求設(shè)計(jì)時(shí),活動(dòng)下線后是不能再次上線的,在需求迭代期內(nèi)又增加了再次上線的功能,狀態(tài)機(jī)流轉(zhuǎn)邏輯清晰,只需要再增加個(gè)狀態(tài)配置流轉(zhuǎn)事件就行,就為狀態(tài)機(jī)賦予了再次上線的能力。
6.總結(jié)
在實(shí)踐的過程中,在spring-statemachine官方文檔結(jié)合Google摸索使用的過程中,遇到持久化存儲(chǔ)StateMachineContext、異常處理,以及狀態(tài)分支等問題。目前回頭看來也不復(fù)雜,如今寫出來總結(jié)一下,希望對(duì)小伙伴們有所幫助。
最后建議在狀態(tài)流程不是很復(fù)雜的情況,如果您也厭煩了if...else,那么不妨嘗試一下squirrel-foundation,相信也是不錯(cuò)的選擇。
參考文獻(xiàn)
- ??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/??
作者簡介

姜強(qiáng)強(qiáng)
■ 經(jīng)銷商技術(shù)部-商業(yè)資源團(tuán)隊(duì)。
■ 2016年加入汽車之家,目前主要負(fù)責(zé)經(jīng)銷商事業(yè)部內(nèi)創(chuàng)新商業(yè)項(xiàng)目的研發(fā)工作,熱衷于業(yè)內(nèi)新技術(shù)的探索與實(shí)踐。