状态机的介绍和使用 | 京东物流技术团队
1 状态机简介
1.1 定义
我们先来给出状态机的基本定义。一句话:
状态机是有限状态自动机的简称,是现实事物运行规则抽象而成的一个数学模型。
先来解释什么是“状态”( State )。现实事物是有不同状态的,例如一个自动门,就有 open 和 closed 两种状态。我们通常所说的状态机是有限状态机,也就是被描述的事物的状态的数量是有限个,例如自动门的状态就是两个 open 和 closed 。
状态机,也就是 State Machine ,不是指一台实际机器,而是指一个数学模型。说白了,一般就是指一张状态转换图。例如,根据自动门的运行规则,我们可以抽象出下面这么一个图。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OREHZkVK-1690871182117)(https://mp.toutiao.com/mp/agw/article_material/open_image/get?code=NDkwNTM1ZGViMWU4ZGI2YWQwYmI3MjFkMzY5ZGIwNWEsMTY5MDg1NDI4NjYzMQ==)]
自动门有两个状态,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不仅实现了配置化,还实现了可视化,但是实现成本也最高。他们的关系如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p5LuyXRN-1690871182119)(https://mp.toutiao.com/mp/agw/article_material/open_image/get?code=OGNmZjdjN2M1YmZjNDY1ODM1ODRkMmI0MTFiYzE1ZGUsMTY5MDg1NDI4NjYzMQ==)]
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等。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6KmM4F7i-1690871182121)(https://mp.toutiao.com/mp/agw/article_material/open_image/get?code=YjMyY2I5N2VlYzZkYmJlODFjN2I2ODE2OGNiZGNlNWMsMTY5MDg1NDI4NjYzMQ==)]
https://github.com/plantuml/plantuml
https://plantuml.com/zh/
2.3.3 DSL & DDD(领域驱动)
DDD和DSL的融合有三点:面向领域、模型的组装方式、分层架构演进。DSL 可以看作是在领域模型之上的一层外壳,可以显著增强领域模型的能力。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0NrEdOi4-1690871182122)(https://mp.toutiao.com/mp/agw/article_material/open_image/get?code=M2RhZTMwZGM2NDBkM2Y4YzY4Y2RjZmRjZmY2ZmE2OWEsMTY5MDg1NDI4NjYzMQ==)]
它的价值主要有两个,一是提升了开发人员的生产力,二是增进了开发人员与领域专家的沟通。外部 DSL 就是对领域模型的一种组装方式。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3jn36rd9-1690871182123)(https://mp.toutiao.com/mp/agw/article_material/open_image/get?code=MDc4NzE4M2FhMDkxMzI0Nzg1ZWNkOTRiYzgzN2VmYjAsMTY5MDg1NDI4NjYzMQ==)]
3 状态机实现的调研
3.1 Spring Statemachine
官网:https://spring.io/projects/spring-statemachine#learn
源码:https://github.com/spring-projects/spring-statemachine
API:https://docs.spring.io/spring-statemachine/docs/3.2.0/api/
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,用来辅助构建状态机。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GwU7UYE5-1690871182124)(https://mp.toutiao.com/mp/agw/article_material/open_image/get?code=NTBjYTg5YjVkNzAyNjJmYmM3ZGVkZDkzN2FmZGM2Y2EsMTY5MDg1NDI4NjYzMQ==)]
更多Eclipse建模插件可参见文档:https://docs.spring.io/spring-statemachine/docs/3.2.0/reference/#sm-papyrus
Spring状态机的配置、定义、事件、状态扩展、上下文集成、安全性、错误处理等,可以参看如下文档:
https://docs.spring.io/spring-statemachine/docs/3.2.0/reference/#statemachine
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:状态机
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LPasYjz1-1690871182125)(https://mp.toutiao.com/mp/agw/article_material/open_image/get?code=MzE5YjdmNDUyZDZiNDU3M2FkZDljOTQ4OGIxOWIzODMsMTY5MDg1NDI4NjYzMQ==)]
整个状态机的核心语义模型(Semantic Model):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VY2cvXr1-1690871182126)(https://mp.toutiao.com/mp/agw/article_material/open_image/get?code=ZDAyNDMxOGM0YTdlYjNjNDMxOGNmOWFmMmMwYThiYzQsMTY5MDg1NDI4NjYzMQ==)]
4 状态机DEMO
4.1 Spring状态机示例
代码地址:http://xingyun.jd.com/codingRoot/ilt/spring-statemachine-demo/
例如,起始节点为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*/@Overridepublic 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*/@Overridepublic 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*/@Beanpublic Action<String, String> initAction() {return ctx -> log.info("Init Action -- DO: {}", ctx.getTarget().getId());}/*** S1到S2* @return*/@Beanpublic 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> {@Overridepublic 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 {@Resourceprivate StateMachine<String, String> stateMachine;@Resourceprivate StateMachineListener stateMachineListener;@PostConstructpublic 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状态机示例
代码地址:http://xingyun.jd.com/codingRoot/ilt/ilt-component-statemachine/
例如: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 {@Overridepublic 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 {@Overridepublic 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 {@Resourceprivate ApplicationContextUtil applicationContextUtil;private Map<TransNeedEventEnum, EventComponent> eventComponentMap = new ConcurrentHashMap<>();@PostConstructprivate 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);}});}@Overridepublic 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
相关文章:
状态机的介绍和使用 | 京东物流技术团队
1 状态机简介 1.1 定义 我们先来给出状态机的基本定义。一句话: 状态机是有限状态自动机的简称,是现实事物运行规则抽象而成的一个数学模型。 先来解释什么是“状态”( State )。现实事物是有不同状态的,例如一个自…...

tinkerCAD案例:32. 使用对齐工具构建喷泉
tinkerCAD案例:32. 使用对齐工具构建喷泉 In this lesson, you will practice the basics in Tinkercad, such as move, rotate, and scale. You will also learn how to use the Align Tool. 在本课中,您将练习 Tinkercad 中的基础知识,例如…...

一起学数据结构(2)——线性表及线性表顺序实现
目录 1. 什么是数据结构: 1.1 数据结构的研究内容: 1.2 数据结构的基本概念: 1.2.1 逻辑结构: 1.2.2 存储结构: 2. 线性表: 2.1 线性表的基本定义: 2.2 线性表的运用: 3 .线性…...

mqtt协议流程图
转载于...

7、单元测试--测试RestFul 接口
单元测试–测试RestFul 接口 – 测试用例类使用SpringBootTest(webEnvironment WebEnvironment.RANDOM_PORT)修饰。 – 测试用例类会接收容器依赖注入TestRestTemplate这个实例变量。 – 测试方法可通过TestRestTemplate来调用RESTful接口的方法。 测试用例应该定义在和被测…...

国家留学基金委(CSC)|发布2024年创新型人才国际合作培养项目实施办法
2023年7月28日,国家留学基金委(CSC)发布了《2024年创新型人才国际合作培养项目实施办法》,在此知识人网小编做全文转载。详细信息请参见https://www.csc.edu.cn/chuguo/s/2648。 2024年创新型人才国际合作培养项目实施办法 第一章…...

找好听的配乐、BGM就上这6个网站,免费商用。
推荐几个音乐素材网站给你,各种类似、风格的都有,而且免费下载,还可以商用,建议收藏起来~ 菜鸟图库 https://www.sucai999.com/audio.html?vNTYxMjky 站内有上千首音效素材,网络流行的音效素材这里都能找到…...

【前端知识】React 基础巩固(三十五)——ReduxToolKit (RTK)
React 基础巩固(三十五)——ReduxToolKit (RTK) 一、RTK介绍 Redux Tool Kit (RTK)是官方推荐的编写Redux逻辑的方法,旨在成为编写Redux逻辑的标准方式,从而解决上面提到的问题。 RTK的核心API主要有如下几个: confi…...

android Android Studio Giraffe | 2022.3.1 版本Lombok不兼容 解决方案
android Android Studio Giraffe | 2022.3.1 版本Lombok不兼容 解决方案 1.查看当前的android studio 版本 Android Studio Giraffe | 2022.3.1 Build #AI-223.8836.35.2231.10406996, built on June 29, 2023 2.打开 idea 官网下载页面 idea下载历史版本 找到对应的版本编号…...
前端框架学习-基础前后端分离
前端知识栈 前端三要素:HTML、CSS、JS HTML 是前端的一个结构层,HTML相当于一个房子的框架,可类比于毛坯房只有一个结构。CSS 是前端的一个样式层,有了CSS的装饰,相当于房子有了装修。JS 是前端的一个行为层ÿ…...

数据中心电子电气设备常见的五种地线种类和做法
数据中心机房计算机系统的集成化程度很高,其正常工作对环境的要求很严格。接地,是指电力系统和电气装置的中性点、电气设备的外露导电部分和装置外导电部分经由导体与大地相连。其作用主要是防止人身遭受电击、设备和线路遭受损坏、预防火灾和防止雷击、…...

重学C++系列之STL库
一、什么是STL库 STL是“Standard Template Library”的缩写,中文翻译为“标准模板库”。CSTL是一套功能强大的C模板类,提供了通用的模板类和函数,这些模板类和函数可以实现多种流行和常用的算法和数据结构,如字符串操作、链表、队…...
JAVA SE -- 第十四天
(全部来自“韩顺平教育”) 泛型 一、泛型 1、基本介绍 ①泛型又称参数类型,是Jdk5.0出现的新特性,解决数据类型的安全性问题 ②在类声明或实例化时只要指定好需要的具体的类型即可 ③Java泛型可以保证如果程序在编译时没有发…...

微信小程序监测版本更新
在index.js里面 不放到app.js里面是因为有登录页面,在登录页面显示更新不太友好 onShow() {const updateManager wx.getUpdateManager()// 请求完新版本信息的回调updateManager.onCheckForUpdate(res > {if (res.hasUpdate) {// 新版本下载成功updateManage…...

《Java面向对象程序设计》学习笔记——第 2 章 基本数据类型、数组和枚举类型
专栏:《Java面向对象程序设计》学习笔记...

TDesign中后台管理系统-访问后端服务
目录 1 修改后端服务地址2 解决跨域问题3 动态获取菜单4 测试后端接口5 前后端联调总结 目前我们已经搭建了TDesign的前端和express的后端,目前是两个独立的应用。通常我们需要把前后端集成在一起,TDesign已经配置了相关的信息,只需要修改后端…...

【机器学习】Gradient Descent for Logistic Regression
Gradient Descent for Logistic Regression 1. 数据集(多变量)2. 逻辑梯度下降3. 梯度下降的实现及代码描述3.1 计算梯度3.2 梯度下降 4. 数据集(单变量)附录 导入所需的库 import copy, math import numpy as np %matplotlib wi…...

ElasticSearch基础篇-Java API操作
ElasticSearch基础-Java API操作 演示代码 创建连接 POM依赖 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:sch…...
解决uniapp的tabBar使用iconfont图标显示方块
今天要写个uniapp的移动端项目,底部tabBar需要添加图标,以往都是以图片的形式引入,但是考虑到不同甲方的主题色也不会相同,使用图片的话,后期变换主题色并不友好,所以和UI商量之后,决定使用icon…...

UE4/5C++多线程插件制作(0.简介)
目录 插件介绍 插件效果 插件使用 English 插件介绍 该插件制作,将从零开始,由一个空白插件一点点的制作,从写一个效果到封装,层层封装插件,简单粗暴的对插件进行了制作: 插件效果 更多的是在cpp中去…...

XML Group端口详解
在XML数据映射过程中,经常需要对数据进行分组聚合操作。例如,当处理包含多个物料明细的XML文件时,可能需要将相同物料号的明细归为一组,或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码,增加了开…...
React Native在HarmonyOS 5.0阅读类应用开发中的实践
一、技术选型背景 随着HarmonyOS 5.0对Web兼容层的增强,React Native作为跨平台框架可通过重新编译ArkTS组件实现85%以上的代码复用率。阅读类应用具有UI复杂度低、数据流清晰的特点。 二、核心实现方案 1. 环境配置 (1)使用React Native…...

2.Vue编写一个app
1.src中重要的组成 1.1main.ts // 引入createApp用于创建应用 import { createApp } from "vue"; // 引用App根组件 import App from ./App.vue;createApp(App).mount(#app)1.2 App.vue 其中要写三种标签 <template> <!--html--> </template>…...
vue3 字体颜色设置的多种方式
在Vue 3中设置字体颜色可以通过多种方式实现,这取决于你是想在组件内部直接设置,还是在CSS/SCSS/LESS等样式文件中定义。以下是几种常见的方法: 1. 内联样式 你可以直接在模板中使用style绑定来设置字体颜色。 <template><div :s…...

Python爬虫(一):爬虫伪装
一、网站防爬机制概述 在当今互联网环境中,具有一定规模或盈利性质的网站几乎都实施了各种防爬措施。这些措施主要分为两大类: 身份验证机制:直接将未经授权的爬虫阻挡在外反爬技术体系:通过各种技术手段增加爬虫获取数据的难度…...
【AI学习】三、AI算法中的向量
在人工智能(AI)算法中,向量(Vector)是一种将现实世界中的数据(如图像、文本、音频等)转化为计算机可处理的数值型特征表示的工具。它是连接人类认知(如语义、视觉特征)与…...

c#开发AI模型对话
AI模型 前面已经介绍了一般AI模型本地部署,直接调用现成的模型数据。这里主要讲述讲接口集成到我们自己的程序中使用方式。 微软提供了ML.NET来开发和使用AI模型,但是目前国内可能使用不多,至少实践例子很少看见。开发训练模型就不介绍了&am…...
在鸿蒙HarmonyOS 5中使用DevEco Studio实现录音机应用
1. 项目配置与权限设置 1.1 配置module.json5 {"module": {"requestPermissions": [{"name": "ohos.permission.MICROPHONE","reason": "录音需要麦克风权限"},{"name": "ohos.permission.WRITE…...

网络编程(UDP编程)
思维导图 UDP基础编程(单播) 1.流程图 服务器:短信的接收方 创建套接字 (socket)-----------------------------------------》有手机指定网络信息-----------------------------------------------》有号码绑定套接字 (bind)--------------…...

什么是Ansible Jinja2
理解 Ansible Jinja2 模板 Ansible 是一款功能强大的开源自动化工具,可让您无缝地管理和配置系统。Ansible 的一大亮点是它使用 Jinja2 模板,允许您根据变量数据动态生成文件、配置设置和脚本。本文将向您介绍 Ansible 中的 Jinja2 模板,并通…...