# 背景
在实际的项目开发中,开发者经常会遇见类似多级审核之类的开发需求,比如某个文件审核,需要经过申请->直系领导审核->总经理审核
等多个步骤。如果是一次动作触发整个审核过程,开发者可能会想到使用责任链模式
来进行开发。但如果多级审核的间隔时间长,审核触发的条件不一样,责任链模式会不太能够解耦这项需求。如果采用平铺直叙式开发,无疑会将审核状态转移过程散落在系统间各个位置,前后两个状态之间的关系没有直观进行维护,同时状态转移时的条件、执行的方式和状态之间的逻辑关系很容易让开发者写出“面条代码”。在项目开发初期可能还好,随着需求的增量变化,平铺直叙式开发将使得状态转移逻辑和业务逻辑高度混合,且每增加一级节点审核,就要新增对应的审核状态及状态转移的逻辑,长此以往变得难以阅读和维护。所以,在这种情况下使用状态机
这样建模方式就显得尤为必要。
# 状态机概述
在计算机领域谈及状态机一般有有限状态机(FSM finite state machine)
和无限状态机(ISM Infinite state machine)
,由于无限状态机只是理论存在,所以应用中均使用有限状态机。
有限状态机是一种抽象的计算模型,其核心思想在于系统在不同状态下对于输入会产生不同的响应,并且可以根据输入和当前状态转移到新的状态。
状态机通常由状态(State)
、事件(Event)
、动作(Action)
三个基本元素构成。其中动作不是必须的,可以只根据事件进行状态转移。
对于开发者视角的状态机通常还会增加转移条件(Condtion)
的概念,此时状态机模型变更为
其中转移条件也是可选的。
# 状态机选型
对于开源状态机框架的选型和多种实现方式不是本文讨论的重点,详情可查看状态机引擎在vivo营销自动化中的深度实践 (opens new window)。
引用文章中的一张图概况开源状态机框架现状
本文选用的为COLA-Statemachine
# 基本实现
本文涉及的MVP
代码地址github (opens new window)
以小朋友要出去玩需要经过爸爸同意、妈妈同意这样的场景为例。
# 实体建模
状态(State)
可以建模为
- 已申请、爸爸同意、妈妈同意、爸爸不同意、妈妈不同意、已完成
事件(Event)
可以建模为
- 同意、不同意、已完成
状态转译成代码可以用枚举类表示
代码语言:javascript复制public enum AuditState {
/**
* 已申请
*/
APPLY("APPLY", "已申请"),
/**
* 爸爸同意
*/
DAD_PASS("DAD_PASS", "爸爸同意"),
/**
* 妈妈同意
*/
MOM_PASS("MOM_PASS", "妈妈同意"),
/**
* 爸爸不同意
*/
DAD_REJ("DAD_REJ", "爸爸不同意"),
/**
* 妈妈不同意
*/
MOM_REJ("MOM_REJ", "妈妈不同意"),
/**
* 已完成
*/
DONE("DONE", "已完成");
private static final Map<String, AuditState> CODE_MAP = new ConcurrentHashMap<>();
static {
for (AuditState auditState : EnumSet.allOf(AuditState.class)) {
CODE_MAP.put(auditState.getCode(), auditState);
}
}
public static AuditState getEnumsByCode(String code) {
return CODE_MAP.get(code);
}
/**
* code
*/
private String code;
/**
* desc
*/
private String desc;
AuditState(String code, String desc) {
this.code = code;
this.desc = desc;
}
// 省略get/set
}
事件转译成代码为
代码语言:javascript复制public enum AuditEvent {
/**
* 同意
*/
PASS(0,"同意"),
/**
* 不同意
*/
REJECT(1,"不同意"),
/**
* 已完成
*/
DONE(2,"已完成");
/**
* code
*/
private Integer code;
/**
* desc
*/
private String desc;
private static final Map<Integer, AuditEvent> CODE_MAP = new ConcurrentHashMap<>();
static {
for (AuditEvent auditEvent : EnumSet.allOf(AuditEvent.class)) {
CODE_MAP.put(auditEvent.getCode(), auditEvent);
}
}
public static AuditEvent getEnumsByCode(Integer code) {
return CODE_MAP.get(code);
}
AuditEvent(Integer code, String desc) {
this.code = code;
this.desc = desc;
}
// 省略get/set
}
在使用COLA
状态机时,还要求开发者传递Context
参数,可用于后续的Condition
和Action
等,根据我们的场景,只需要知道该审核单的id
和当前接口审核的事件AuditEvent
即可,所以Context
可以建模为
@Data
public class AuditContext {
/**
* id
*/
private Long id;
/**
* 事件
*/
private Integer auditEvent;
}
# Spring体系下的可扩展状态机
完成实体建模后就是状态机的构建了,通常来说我们应该结合SpringBoot
体系达成系统内多个状态机的自动识别和自动获取。
# 构建状态机
建立一个基本的策略接口
代码语言:javascript复制public interface StateMachineStrategy {
String getMachineType();
}
状态机枚举
代码语言:javascript复制public enum StateMachineEnum {
/**
* 测试状态机
*/
TEST_MACHINE("testMachine","测试状态机");
/**
* code
*/
private String code;
/**
* desc
*/
private String desc;
StateMachineEnum(String code, String desc) {
this.code = code;
this.desc = desc;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
}
状态机实现
代码语言:javascript复制@Component
public class AuditMachine implements StateMachineStrategy {
@Resource
@Qualifier("passOrRejectAction")
private Action<AuditState, AuditEvent, AuditContext> passOrRejectAction;
@Resource
@Qualifier("doneAction")
private Action<AuditState, AuditEvent, AuditContext> doneAction;
@Resource
@Qualifier("passOrRejectCondition")
private Condition<AuditContext> passOrRejectCondition;
@Resource
@Qualifier("doneCondition")
private Condition<AuditContext> doneCondition;
@Override
public String getMachineType() {
return StateMachineEnum.TEST_MACHINE.getCode();
}
/**
* | From(开始状态) | To(抵达状态) | Event(事件) | When(条件) | Perform(执行动作) |
* | -------------- | ------------ | ----------- | --------------------- | ------------------ |
* | 已申请 | 爸爸同意 | 审核通过 | passOrRejectCondition | passOrRejectAction |
* | 爸爸同意 | 妈妈同意 | 审核通过 | passOrRejectCondition | passOrRejectAction |
* | 已申请 | 爸爸不同意 | 审核驳回 | passOrRejectCondition | passOrRejectAction |
* | 爸爸同意 | 妈妈不同意 | 审核驳回 | passOrRejectCondition | passOrRejectAction |
* | 已申请 | 已完成状态 | 已完成 | doneCondition | doneAction |
* | 爸爸同意 | 已完成状态 | 已完成 | doneCondition | doneAction |
* | 妈妈同意 | 已完成状态 | 已完成 | doneCondition | doneAction |
*
* @return StateMachine stateMachine
*/
@Bean
public StateMachine<AuditState, AuditEvent, AuditContext> stateMachine() {
StateMachineBuilder<AuditState, AuditEvent, AuditContext> builder = StateMachineBuilderFactory.create();
// 已申请->爸爸同意
builder.externalTransition().from(AuditState.APPLY).to(AuditState.DAD_PASS)
.on(AuditEvent.PASS)
.when(passOrRejectCondition)
.perform(passOrRejectAction);
// 已申请->爸爸不同意
builder.externalTransition().from(AuditState.APPLY).to(AuditState.DAD_REJ)
.on(AuditEvent.REJECT)
.when(passOrRejectCondition)
.perform(passOrRejectAction);
// 爸爸同意->妈妈同意
builder.externalTransition().from(AuditState.DAD_PASS).to(AuditState.MOM_PASS)
.on(AuditEvent.PASS)
.when(passOrRejectCondition)
.perform(passOrRejectAction);
// 爸爸同意->妈妈不同意
builder.externalTransition().from(AuditState.DAD_PASS).to(AuditState.MOM_REJ)
.on(AuditEvent.REJECT)
.when(passOrRejectCondition)
.perform(passOrRejectAction);
// 已申请->已完成
// 爸爸同意->已完成
// 妈妈同意->已完成
builder.externalTransitions().fromAmong(AuditState.APPLY, AuditState.DAD_PASS, AuditState.MOM_PASS)
.to(AuditState.DONE)
.on(AuditEvent.DONE)
.when(doneCondition)
.perform(doneAction);
return builder.build(getMachineType());
}
}
从实现类可见状态、事件、条件、动作
,在代码中是非常清晰的,且维护在一个类中。
此时的状态机DSL
图如下(通过接口获取)
状态机引擎类
代码语言:javascript复制@Component
public class StateMachineEngine implements InitializingBean {
private Map<String, StateMachineStrategy> stateMachineMap;
@Autowired
private ApplicationContext applicationContext;
/**
* 根据枚举获取状态机实例key
*
* @param stateMachineEnum stateMachineEnum
* @return String
*/
private String getMachine(StateMachineEnum stateMachineEnum) {
return stateMachineMap.get(stateMachineEnum.getCode()).getMachineType();
}
/**
* 根据枚举获取状态机示例,并根据当前状态、事件、上下文,进行状态流转
*
* @param stateMachineEnum stateMachineEnum
* @param auditState auditState
* @param auditEvent auditEvent
* @param auditContext auditContext
* @return AuditState
*/
public AuditState fire(StateMachineEnum stateMachineEnum, AuditState auditState,
AuditEvent auditEvent, AuditContext auditContext) {
StateMachine<AuditState, AuditEvent, AuditContext> stateMachine = StateMachineFactory.get(getMachine(stateMachineEnum));
return stateMachine.fireEvent(auditState, auditEvent, auditContext);
}
/**
* 根据枚举获取状态机示例的状态DSL UML图
*
* @param stateMachineEnum stateMachineEnum
* @return String
*/
public String generateUml(StateMachineEnum stateMachineEnum){
StateMachine<AuditState, AuditEvent, AuditContext> stateMachine = StateMachineFactory.get(getMachine(stateMachineEnum));
return stateMachine.generatePlantUML();
}
/**
* 获取所有实现了接口的状态机
*/
@Override
public void afterPropertiesSet() {
Map<String, StateMachineStrategy> beansOfType = applicationContext.getBeansOfType(StateMachineStrategy.class);
stateMachineMap = Optional.of(beansOfType)
.map(beansOfTypeMap -> beansOfTypeMap.values().stream()
.filter(stateMachineHandler -> StringUtils.hasLength(stateMachineHandler.getMachineType()))
.collect(Collectors.toMap(StateMachineStrategy::getMachineType, Function.identity())))
.orElse(new HashMap<>(8));
}
}
在COLA
中定义了Condtion
和Action
接口
本文的Condition
实现为
/**
* 通用通过/驳回条件
* 覆盖审核正向流程,以及驳回流程
* 已申请->爸爸同意->妈妈统一
* 已申请->爸爸不同意
* 爸爸同意->妈妈不同意
*/
@Component
public class PassOrRejectCondition implements Condition<AuditContext> {
@Override
public boolean isSatisfied(AuditContext context) {
System.out.println(1);
return true;
}
}
代码语言:javascript复制/**
* 已完成条件
*/
@Component
public class DoneCondition implements Condition<AuditContext> {
@Override
public boolean isSatisfied(AuditContext context) {
System.out.println(1);
return true;
}
}
这里简单起见并没有在Condition
上做特殊条件判断,如果需要拦截返回false
便不会执行后续Action
本文的Action
实现为
/**
* 通用审核通过/驳回执行动作
* 覆盖审核正向流程,以及驳回流程
* 已申请->爸爸同意->妈妈同意
* 已申请->爸爸不同意
* 爸爸同意->妈妈不同意
*/
@Component
public class PassOrRejectAction implements Action<AuditState, AuditEvent, AuditContext> {
private static final Logger LOGGER = LoggerFactory.getLogger(PassOrRejectAction.class);
@Autowired
private AuditDao auditDao;
@Override
@Transactional
public void execute(AuditState from, AuditState to, AuditEvent event, AuditContext context) {
LOGGER.info("passOrRejectAction from {}, to {}, on event {}, id:{}", from, to, event, context.getId());
auditDao.updateAuditStatus(to.getCode(), context.getId());
}
}
代码语言:javascript复制/**
* 已完成执行动作
*/
@Component
public class DoneAction implements Action<AuditState, AuditEvent, AuditContext> {
private static final Logger LOGGER = LoggerFactory.getLogger(DoneAction.class);
@Autowired
private AuditDao auditDao;
@Override
public void execute(AuditState from, AuditState to, AuditEvent event, AuditContext context) {
LOGGER.info("doneAction from {}, to {}, on event {}, id:{}", from, to, event, context.getId());
auditDao.updateAuditStatus(to.getCode(), context.getId());
}
}
在Action
中只是进行根据id
更新状态,这一操作
# Controller层
代码语言:javascript复制@RestController
@RequestMapping("/test")
@Slf4j
public class TestController {
@Autowired
private AuditService auditService;
@PostMapping("/audit")
public void audit(@RequestBody @Validated AuditParam auditParam){
AuditContext auditContext = new AuditContext();
BeanUtils.copyProperties(auditParam, auditContext);
auditService.audit(auditContext);
}
@GetMapping("/uml")
public String uml(){
return auditService.uml();
}
}
提供2个接口,一个接口用于触发多级审核状态机,一个接口用于获取状态机内置的UML图
其中AuditParam
为
@Data
public class AuditParam {
/**
* id
*/
private Long id;
/**
* 事件
*/
private Integer auditEvent;
}
# Service层
代码语言:javascript复制@Service
@Slf4j
public class AuditServiceImpl implements AuditService {
@Autowired
private AuditDao auditDao;
@Autowired
private StateMachineEngine stateMachineEngine;
@Override
public void audit(AuditContext auditContext) {
Long id = auditContext.getId();
AuditDTO auditDTO = auditDao.selectById(id);
String auditState = auditDTO.getAuditState();
Integer auditEvent = auditContext.getAuditEvent();
// 获取当前状态和事件
AuditState nowState = AuditState.getEnumsByCode(auditState);
AuditEvent nowEvent = AuditEvent.getEnumsByCode(auditEvent);
// 执行状态机
stateMachineEngine.fire(StateMachineEnum.TEST_MACHINE, nowState, nowEvent, auditContext);
}
@Override
public String uml() {
return stateMachineEngine.generateUml(StateMachineEnum.TEST_MACHINE);
}
}
Service
层首先根据id
查询该审核单在数据库中的状态,通过当前状态和事件获取状态机需要的State
和Event
,同时构建Context
,通过前面构造的状态机引擎执行fire
方法进行状态转化。
# 请求模拟
数据库中初始有一条已申请数据,id
为1
,状态
为APPLY
当Postman
发如下请求时,代表对于id
为1
的数据,进行一次审核,且审核事件为0(同意)
此时控制台打印为
表示正常执行Action
数据库状态变更为
# Q/A
提示
Q: 当状态机内没有定义某个状态转移时,比如此时数据库状态为DONE,请求带上审核事件为同意的参数进来,状态机会发生什么?
笔记
A: 由状态机框架内部处理,此时状态机什么都不会发生,也不会抛出异常
提示
Q: 在状态机的Action和Condition方法上加AOP注解有效吗
笔记
A: 如果你的Action或Condition是匿名内部类的形式,那么事务将无效,这是因为Action和Condition由框架内部直接调用,框架并未交给Spring管理,所以无法产生代理对象执行增强。如果你定义状态机时传递的直接是Action或Condition的实现类,则事务依旧有效。具体经验可查看COLA-statemachine事务失效踩坑 (opens new window)