1 状况机简介
1.1 界说
咱们先来给出状况机的根本界说。一句话:
状况机是有限状况主动机的简称,是实践事物运转规矩笼统而成的一个数学模型。
先来解释什么是“状况”( State )。实践事物是有不同状况的,例如一个主动门,就有 open 和 closed 两种状况。咱们一般所说的状况机是有限状况机,也便是被描述的事物的状况的数量是有限个,例如主动门的状况便是两个 open 和 closed 。
状况机,也便是 State Machine ,不是指一台实践机器,而是指一个数学模型。说白了,一般便是指一张状况转换图。例如,依据主动门的运转规矩,咱们能够笼统出下面这么一个图。
主动门有两个状况,open 和 closed ,closed 状况下,假如读取开门信号,那么状况就会切换为 open 。open 状况下假如读取关门信号,状况就会切换为 closed 。
状况机的全称是有限状况主动机,主动两个字也是包括重要意义的。给定一个状况机,同时给定它的当时状况以及输入,那么输出状况时能够明晰的运算出来的。例如关于主动门,给定初始状况 closed ,给定输入“开门”,那么下一个状况时能够运算出来的。
这样状况机的根本界说咱们就介绍完毕了。重复一下:状况机是有限状况主动机的简称,是实践事物运转规矩笼统而成的一个数学模型。
1.2 四大概念
下面来给出状况机的四大概念。
第一个是 State ,状况。一个状况机至少要包括两个状况。例如上面主动门的比方,有 open 和 closed 两个状况。
第二个是 Event ,事情。事情便是执行某个操作的触发条件或者口令。关于主动门,“按下开门按钮”便是一个事情。
第三个是 Action ,动作。事情发生今后要执行动作。例如事情是“按开门按钮”,动作是“开门”。编程的时分,一个 Action 一般就对应一个函数。
第四个是 Transition ,改换。也便是从一个状况改变为另一个状况。例如“开门过程”便是一个改换。
2 DSL
2.1 DSL
DSL是一种东西,它的中心价值在于,它供给了一种手段,能够更加明晰地就体系某部分的意图进行交流。
这种明晰并非仅仅审美追求。一段代码越简略看懂,就越简略发现错误,也就越简略对体系进行修改。因而,咱们鼓励变量名要有意义,文档要写清楚,代码结构要写明晰。根据同样的理由,咱们应该也鼓励选用DSL。
依照界说来说,DSL是针对某一特定范畴,具有受限表达性的一种计算机程序设计言语。
这一界说包括3个关键元素:
言语性(language nature):DSL是一种程序设计言语,因而它有必要具有连贯的表达才能——不管是一个表达式仍是多个表达式组合在一起。
受限的表达性(limited expressiveness):通用程序设计言语供给广泛的才能:支撑各种数据、控制,以及笼统结构。这些才能很有用,但也会让言语难于学习和运用。DSL只支撑特定范畴所需求特性的最小集。运用DSL,无法构建一个完好的体系,相反,却能够处理体系某一方面的问题。
针对范畴(domain focus):只有在一个明晰的小范畴下,这种才能有限的言语才会有用。这个范畴才使得这种言语值得运用。
比方正则表达式,/\d{3}-\d{3}-\d{4}/便是一个典型的DSL,处理的是字符串匹配这个特定范畴的问题。
2.2 DSL的分类
依照类型,DSL能够分为三类:内部DSL(Internal DSL)、外部DSL(External DSL)、以及言语工作台(Language Workbench)。
Internal DSL是一种通用言语的特定用法。用内部DSL写成的脚本是一段合法的程序,可是它具有特定的风格,而且只用到了言语的一部分特性,用于处理整个体系一个小方面的问题。 用这种DSL写出的程序有一种自界说言语的风格,与其所运用的宿主言语有所区别。例如咱们的状况机便是Internal DSL,它不支撑脚本装备,运用的时分仍是Java言语,但并不妨碍它也是DSL。
builder.externalTransition()
.from(States.STATE1)
.to(States.STATE2)
.on(Events.EVENT1)
.when(checkCondition())
.perform(doAction());
External DSL是一种“不同于运用体系首要运用言语”的言语。外部DSL一般选用自界说语法,不过选择其他言语的语法也很常见(XML便是一个常见选 择)。比方像Struts和Hibernate这样的体系所运用的XML装备文件。
Workbench是一个专用的IDE,简略点说,工作台是DSL的产品化和可视化形态。
三个类别DSL早年往后是有一种递进联系,Internal DSL最简略,完成本钱也低,可是不支撑“外部装备”。Workbench不仅完成了装备化,还完成了可视化,可是完成本钱也最高。他们的联系如下图所示:
2.3 DSL示例
2.3.1 内部DSL示例
HTML: 经过自然言语编写
在Groovy中,经过DSL能够用易读的写法生成XML
def s = new StringWriter()
def xml = new MarkupBuilder(s)
xml.html{
head{
title("Hello - DSL")
script(ahref:"https://xxxx.com/vue.js")
meta(author:"marui116")
}
body{
p("JD-ILT-ITMS")
}
}
println s.toString()
最终将生成
<html>
<head>
<title>Hello - DSL</title>
<script ahref='https://xxxx.com/vue.js' />
<meta author='marui116' />
</head>
<body>
<p>JD-ILT-ITMS</p>
</body>
</html>
MarkupBuilder的效果说明:
A helper class for creating XML or HTML markup. The builder supports various 'pretty printed' formats.
Example:
new MarkupBuilder().root {
a( a1:'one' ) {
b { mkp.yield( '3 < 5' ) }
c( a2:'two', 'blah' )
}
}
Will print the following to System.out:
<root>
<a a1='one'>
<b>3 < 5</b>
<c a2='two'>blah</c>
</a>
</root>
这儿相关于Java这样的动态言语,最为不同的便是xml.html这个并不存在的办法竟然能够经过编译并运转,它内部重写了invokeMethod办法,并进行闭包遍历,少写了许多POJO目标,功率更高。
2.3.2 外部DSL
以plantUML为例,外部DSL不受限于宿主言语的语法,对用户很友爱,尤其是关于不明白宿主言语语法的用户。但外部DSL的自界说语法需求有配套的语法剖析器。常见的语法剖析器有:YACC、ANTLR等。
github.com/plantuml/pl…
plantuml.com/zh/
2.3.3 DSL & DDD(范畴驱动)
DDD和DSL的交融有三点:面向范畴、模型的拼装办法、分层架构演进。DSL 能够看作是在范畴模型之上的一层外壳,能够显著增强范畴模型的才能。
它的价值首要有两个,一是提升了开发人员的生产力,二是增进了开发人员与范畴专家的交流。外部 DSL 便是对范畴模型的一种拼装办法。
3 状况机完成的调研
3.1 Spring Statemachine
官网:spring.io/projects/sp…
源码:github.com/spring-proj…
API:docs.spring.io/spring-stat…
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 integration to associate beans with a state machine.(Spring IOC集成将bean与状况机关联起来)
Spring StateMachine供给了papyrus的Eclipse Plugin,用来辅助构建状况机。
更多Eclipse建模插件可拜见文档:docs.spring.io/spring-stat…
Spring状况机的装备、界说、事情、状况扩展、上下文集成、安全性、错误处理等,能够参看如下文档:
docs.spring.io/spring-stat…
3.2 COLA状况机DSL完成
COLA 是 Clean Object-Oriented and Layered Architecture的缩写,代表“整洁面向目标分层架构”。 现在COLA已经发展到COLA v4。COLA供给了一个DDD落地的处理方案,其间包括了一个开源、简略、轻量、功能极高的状况机DSL完成,处理事务中的状况流通问题。
COLA状况机组件完成一个仅支撑简略状况流通的状况机,该状况机的中心概念如下图所示,首要包括:
1.State:状况
2.Event:事情,状况由事情触发,引起改变
3.Transition:流通,表明从一个状况到另一个状况
4.External Transition:外部流通,两个不同状况之间的流通
5.Internal Transition:内部流通,同一个状况之间的流通
6.Condition:条件,表明是否答应到达某个状况
7.Action:动作,到达某个状况之后,能够做什么
8.StateMachine:状况机
整个状况机的中心语义模型(Semantic Model):
4 状况机DEMO
4.1 Spring状况机示例
代码地址:xingyun.jd.com/codingRoot/…
例如,开始节点为SI、完毕节点为SF,开始节点后续有S1、S2、S3三个节点的简略状况机。
Spring Boot项目需引进Spring状况机组件。
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-core</artifactId>
<version>3.2.0</version>
</dependency>
4.1.1 结构状况机
@Configuration
@EnableStateMachine
@Slf4j
public class SimpleStateMachineConfiguration extends StateMachineConfigurerAdapter<String, String> {
/**
* 界说初始节点、完毕节点和状况节点
* @param states the {@link StateMachineStateConfigurer}
* @throws Exception
*/
@Override
public void configure(StateMachineStateConfigurer<String, String> states) throws Exception {
states.withStates()
.initial("SI")
.end("SF")
.states(new HashSet<String>(Arrays.asList("S1", "S2", "S3")));
}
/**
* 装备状况节点的流向和事情
* @param transitions the {@link StateMachineTransitionConfigurer}
* @throws Exception
*/
@Override
public void configure(StateMachineTransitionConfigurer<String, String> transitions) throws Exception {
transitions.withExternal()
.source("SI").target("S1").event("E1").action(initAction())
.and()
.withExternal()
.source("S1").target("S2").event("E2").action(s1Action())
.and()
.withExternal()
.source("S2").target("SF").event("end");
}
/**
* 初始节点到S1
* @return
*/
@Bean
public Action<String, String> initAction() {
return ctx -> log.info("Init Action -- DO: {}", ctx.getTarget().getId());
}
/**
* S1到S2
* @return
*/
@Bean
public Action<String, String> s1Action() {
return ctx -> log.info("S1 Action -- DO: {}", ctx.getTarget().getId());
}
}
4.1.2 状况机状况监听器
@Component
@Slf4j
public class StateMachineListener extends StateMachineListenerAdapter<String, String> {
@Override
public void stateChanged(State from, State to) {
log.info("Transitioned from {} to {}", from == null ? "none" : from.getId(), to.getId());
}
}
4.1.3 状况机装备
@Configuration
@Slf4j
public class StateMachineConfig implements WebMvcConfigurer {
@Resource
private StateMachine<String, String> stateMachine;
@Resource
private StateMachineListener stateMachineListener;
@PostConstruct
public void init() {
stateMachine.addStateListener(stateMachineListener);
}
}
4.1.4 接口示例
4.1.4.1 获取状况机状况列表
@RequestMapping("info")
public String info() {
return StringUtils.collectionToDelimitedString(
stateMachine.getStates()
.stream()
.map(State::getId)
.collect(Collectors.toList()),
",");
}
4.1.4.2 状况机敞开
在对Spring状况机进行事情操作之前,有必要先敞开状况机
@GetMapping("start")
public String start() {
stateMachine.startReactively().block();
return state();
}
4.1.4.3 事情操作
@PostMapping("event")
public String event(@RequestParam(name = "event") String event) {
Message<String> message = MessageBuilder.withPayload(event).build();
return stateMachine.sendEvent(Mono.just(message)).blockLast().getMessage().getPayload();
}
4.1.4.4 获取状况机当时状况
@GetMapping("state")
public String state() {
return Mono.defer(() -> Mono.justOrEmpty(stateMachine.getState().getId())).block();
}
4.1.4.5 一次状况转换的控制台输出
: Completed initialization in 0 ms
: Transitioned from none to SI
: Init Action -- DO: S1
: Transitioned from SI to S1
: S1 Action -- DO: S2
: Transitioned from S1 to S2
: Transitioned from S2 to SF
能够看到,状况从none到SI开始节点,再到S1、S2,然后S2经过E2事情到SF完毕节点。
4.2 COLA状况机示例
代码地址:xingyun.jd.com/codingRoot/…
例如:iTMS中的运送需求单的状况现在有:待分配、已分配、运送中、部分妥投、悉数妥投、悉数拒收、已撤销。
4.2.1 结构状况机
com.jd.ilt.component.statemachine.demo.component.statemachine.TransNeedStateMachine
StateMachineBuilder<TransNeedStatusEnum, TransNeedEventEnum, Context> builder = StateMachineBuilderFactory.create();
// 接单后,运送需求单生成运送规划单
builder.externalTransition()
.from(None)
.to(UN_ASSIGN_CARRIER)
.on(Create_Event)
.when(checkCondition())
.perform(doAction());
// 运送规划单生成调度单,调度单绑定服务商
builder.externalTransition()
.from(UN_ASSIGN_CARRIER)
.to(UN_ASSIGN_CAR)
.on(Assign_Carrier_Event)
.when(checkCondition())
.perform(doAction());
// 服务商分配车辆、司机
builder.externalTransition()
.from(UN_ASSIGN_CAR)
.to(ASSIGNED_CAR)
.on(Assign_Car_Event)
.when(checkCondition())
.perform(doAction());
// 货品揽收
builder.externalTransition()
.from(ASSIGNED_CAR)
.to(PICKUPED)
.on(Trans_Job_Status_Change_Event)
.when(checkCondition())
.perform(doAction());
// 揽收货品更新到运送中
builder.externalTransition()
.from(ASSIGNED_CAR)
.to(IN_TRANSIT)
.on(Trans_Job_Status_Change_Event)
.when(checkCondition())
.perform(doAction());
// 运送中更新到过海关
builder.externalTransition()
.from(IN_TRANSIT)
.to(PASS_CUSTOMS)
.on(Trans_Job_Status_Change_Event)
// 查看是否需求过海关
.when(isTransNeedPassCustoms())
.perform(doAction());
// 妥投
builder.externalTransition()
.from(PASS_CUSTOMS)
.to(ALL_DELIVERIED)
.on(All_Delivery_Event)
.when(checkCondition())
.perform(doAction());
// 车辆揽收、运送、过海关的运送状况,都能够直接更新到妥投
Stream.of(PICKUPED, IN_TRANSIT, PASS_CUSTOMS)
.forEach(status ->
builder.externalTransition()
.from(status)
.to(ALL_DELIVERIED)
.on(Trans_Job_Status_Change_Event)
.when(checkCondition())
.perform(doAction())
);
// 待分配、待派车、已派车可撤销
Stream.of(UN_ASSIGN_CARRIER, UN_ASSIGN_CAR, ASSIGNED_CAR)
.forEach(status ->
builder.externalTransition()
.from(status)
.to(CANCELED)
.on(Order_Cancel_Event)
.when(checkCondition())
.perform(doAction())
);
// 妥投、和撤销可完毕归档
Stream.of(ALL_DELIVERIED, CANCELED)
.forEach(status ->
builder.externalTransition()
.from(status)
.to(FINISH)
.on(Order_Finish)
.when(checkCondition())
.perform(doAction())
);
stateMachine = builder.build("TransNeedStatusMachine");
从代码中,能够方便的扩展状况和对应的事情,状况机主动进行事务状况的流通。生成的状况流通图如下所示:
@startuml
None --> UN_ASSIGN_CARRIER : Create_Event
UN_ASSIGN_CARRIER --> UN_ASSIGN_CAR : Assign_Carrier_Event
UN_ASSIGN_CAR --> ASSIGNED_CAR : Assign_Car_Event
ASSIGNED_CAR --> CANCELED : Order_Cancel_Event
ASSIGNED_CAR --> PICKUPED : Trans_Job_Status_Change_Event
ASSIGNED_CAR --> IN_TRANSIT : Trans_Job_Status_Change_Event
IN_TRANSIT --> PASS_CUSTOMS : Trans_Job_Status_Change_Event
PASS_CUSTOMS --> ALL_DELIVERIED : Trans_Job_Status_Change_Event
PASS_CUSTOMS --> ALL_DELIVERIED : All_Delivery_Event
IN_TRANSIT --> ALL_DELIVERIED : Trans_Job_Status_Change_Event
ALL_DELIVERIED --> FINISH : Order_Finis
UN_ASSIGN_CAR --> CANCELED : Order_Cancel_Event
UN_ASSIGN_CARRIER --> CANCELED : Order_Cancel_Event
PICKUPED --> ALL_DELIVERIED : Trans_Job_Status_Change_Event
CANCELED --> FINISH : Order_Finis
@enduml
4.2.2 状况机事情处理
/**
* 一种是经过Event来进行事情分发,不同Event经过EventBus走不同的事情呼应
* 另一种是在结构状况机时,直接装备不同的Action
* @return
*/
private Action<TransNeedStatusEnum, TransNeedEventEnum, Context> doAction() {
log.info("do action");
return (from, to, event, ctx) -> {
log.info(ctx.getUserName()+" is operating trans need bill "+ctx.getTransNeedId()+" from:"+from+" to:"+to+" on:"+event);
if (from != None) {
TransNeed transNeed = ctx.getTransNeed();
transNeed.setStatus(to.name());
transNeed.setUpdateTime(LocalDateTime.now());
transNeedService.update(transNeed);
}
eventBusService.invokeEvent(event, ctx);
};
}
Event和EventBus简略Demo示例:
/**
* @author marui116
* @version 1.0.0
* @className TransNeedAssignCarrierEvent
* @description TODO
* @date 2023/3/28 11:08
*/
@Component
@EventAnnonation(event = TransNeedEventEnum.Assign_Carrier_Event)
@Slf4j
public class TransNeedAssignCarrierEvent implements EventComponent {
@Override
public void invokeEvent(Context context) {
log.info("分配了服务商,给服务商发邮件和短信,让服务商组织");
}
}
/**
* @author marui116
* @version 1.0.0
* @className TransNeedAssignCarEvent
* @description TODO
* @date 2023/3/28 11:05
*/
@Component
@EventAnnonation(event = TransNeedEventEnum.Assign_Car_Event)
@Slf4j
public class TransNeedAssignCarEvent implements EventComponent {
@Override
public void invokeEvent(Context context) {
log.info("分配了车辆信息,给运单中心发送车辆信息");
}
}
/**
* @author marui116
* @version 1.0.0
* @className EventServiceImpl
* @description TODO
* @date 2023/3/28 10:57
*/
@Service
public class EventBusServiceImpl implements EventBusService {
@Resource
private ApplicationContextUtil applicationContextUtil;
private Map<TransNeedEventEnum, EventComponent> eventComponentMap = new ConcurrentHashMap<>();
@PostConstruct
private void init() {
ApplicationContext context = applicationContextUtil.getApplicationContext();
Map<String, EventComponent> eventBeanMap = context.getBeansOfType(EventComponent.class);
eventBeanMap.values().forEach(event -> {
if (event.getClass().isAnnotationPresent(EventAnnonation.class)) {
EventAnnonation eventAnnonation = event.getClass().getAnnotation(EventAnnonation.class);
eventComponentMap.put(eventAnnonation.event(), event);
}
});
}
@Override
public void invokeEvent(TransNeedEventEnum eventEnum, Context context) {
if (eventComponentMap.containsKey(eventEnum)) {
eventComponentMap.get(eventEnum).invokeEvent(context);
}
}
}
4.2.3 状况机上下文
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Context {
private String userName;
private Long transNeedId;
private TransNeed transNeed;
}
4.2.4 状况枚举
public enum TransNeedStatusEnum {
/**
* 开始状况
*/
None,
/**
* 待分配陆运服务商
*/
UN_ASSIGN_CARRIER,
/**
* 待分配车辆和司机
*/
UN_ASSIGN_CAR,
/**
* 订单已处理,已组织司机提货
*/
ASSIGNED_CAR,
/**
* 已完成提货
*/
PICKUPED,
/**
* 运送中
*/
IN_TRANSIT,
/**
* 已经过内地海关
*/
PASS_CUSTOMS,
/**
* 您的货品部分妥投部分投递失利
*/
PARTIAL_DELIVERIED,
/**
* 您的货品妥投
*/
ALL_DELIVERIED,
/**
* 您的货品被拒收
*/
ALL_REJECTED,
/**
* 托付订单被撤销
*/
CANCELED,
/**
* 单据完毕归档
*/
FINISH;
}
4.2.5 事情枚举
public enum TransNeedEventEnum {
// 体系事情
Create_Event,
Normal_Update_Event,
/**
* 分配服务商事情
*/
Assign_Carrier_Event,
/**
* 派车事情
*/
Assign_Car_Event,
// 车辆使命(trans_jbo)执行修改调度单(trans_task)状况的事情
Trans_Job_Status_Change_Event,
// 派送事情
Partial_Delivery_Event,
All_Delivery_Event,
Partial_Reject_Event,
All_Reject_Event,
// 调度单中的使命单撤销事情
Order_Cancel_Event,
// 单据完毕
Order_Finish;
public boolean isSystemEvent() {
return this == Create_Event ||
this == Normal_Update_Event;
}
}
4.2.6 接口Demo
4.2.6.1 创立需求单
/**
* 接单
* @return
*/
@RequestMapping("/start/{fsNo}/{remark}")
public Context start(@PathVariable("fsNo") String fsNo, @PathVariable("remark") String remark) {
Context context = contextService.getContext();
Object newStatus = stateMachine.getStateMachine().fireEvent(TransNeedStatusEnum.None, TransNeedEventEnum.Create_Event, context);
TransNeed transNeed = transNeedService.createTransNeed(fsNo, remark, newStatus.toString());
context.setTransNeed(transNeed);
context.setTransNeedId(transNeed.getId());
return context;
}
4.2.6.2 分配服务商
/**
* 运送规划单生成调度单,调度单绑定服务商
*/
@RequestMapping("/assignCarrier/{id}")
public Context assignCarrier(@PathVariable("id") Long id) {
Context context = contextService.getContext(id);
TransNeedStatusEnum prevStatus = TransNeedStatusEnum.valueOf(context.getTransNeed().getStatus());
stateMachine.getStateMachine().fireEvent(prevStatus, TransNeedEventEnum.Assign_Carrier_Event, context);
return context;
}
4.2.6.3 分配车辆
@RequestMapping("/assignCar/{id}")
public Context assignCar(@PathVariable("id") Long id) {
Context context = contextService.getContext(id);
TransNeedStatusEnum prevStatus = TransNeedStatusEnum.valueOf(context.getTransNeed().getStatus());
log.info("trans need id: {}, prev status: {}", id, prevStatus);
stateMachine.getStateMachine().fireEvent(prevStatus, TransNeedEventEnum.Assign_Car_Event, context);
return context;
}
5 状况机比照
维度\组件 | Spring StateMachine | COLA StateMachine |
---|---|---|
API调用 | 运用Reactive的Mono、Flux办法进行API调用 | 同步的API调用,假如有需求也能够将办法经过MQ、守时使命、线程池做成异步的 |
代码量 | core包284个接口和类 | 36个接口和类 |
生态 | 非常丰富 | 无 |
定制化难度 | 困难 | 简略 |
代码更新状况 | 将近1年没有更新 | 半年前 |
综上,假如是直接运用状况机的组件库,能够考虑运用Spring的状况机,假如是要渐进式的运用状况机,逐步依照自己的需求去定制化状况机以满足事务需求,主张运用COLA的状况机。
6 iTMS运用状况机的方案
iTMS预备渐进式的运用COLA的状况机组件,先轻量级运用状况机进行运送相关域的状况改变,后续依照DDD的状况和事情的剖析,运用CQRS的设计形式对命令做封装,调用状况机进行事务流通。
作者:京东物流 马瑞
来历:京东云开发者社区 自猿其说Tech