赞
踩
https://github.com/NoticeVengus/EasyStateMachinehttps://github.com/NoticeVengus/EasyStateMachine
EasyStateMachine,以下简称ESM,是一款不需要依赖Spring的状态机,支持代码和json配置文件的方式初始化状态机, 并通过注解形式触发Action。
version | date | detail |
---|---|---|
1.0.0 | 2021.11.21 | 初始版本 |
name | version | detail |
---|---|---|
lombok | 1.18.20 | 实现注解式构造和成员变量配置 |
fastjson | 1.2.76 | 序列化及反序列化参数 |
slf4j | 2.0.0-alpha5 | 日志格式化输出 |
commons-logging | 1.2 | 日志工具包 |
commons-lang3 | 3.12.0 | 语言工具包 |
commons-collection4 | 4.4 | 集合类工具包 |
Jon是一名热爱旅游的小伙子,今天开始他的北上广之旅。
开着心爱的小轿车,跟着导航,准备出发!
每到一座城市,Jon都会买上当地的特色美食,和下一个目的地的Holo线下好友分享。
以下是他的旅游目的地城市和轿车行进线路图:
我们将用ESM来维护他的这趟旅程,告诉Jon往哪个方向开,会到达哪一座城市,而且不要忘了带上当地美食!
ESM提供Util工具类,通过工具类可以读取特定规则的json配置文件,这样就不需要在代码中维护负责的状态变更逻辑了。
但在写配置文件之前,还需要定义好状态机必备的两个要素,即State和Event。
在本次旅程中,State为Jon当前身处的城市,Event为轿车行进的方向。
在ESM中需要给每一个State和Event定义一个Integer类型的唯一编码,当然,同样的编码在State和Event中是可以重复的。
State定义如下:
code | detail |
---|---|
0 | 北京 |
1 | 上海 |
2 | 广州 |
Event定义如下:
code | detail |
---|---|
0 | 往北方开 |
1 | 往东方开 |
2 | 往西方开 |
3 | 往南方开 |
定义好编号后,就可以配置json文件了,新建statemachind.json,添加配置,如下节选部分配置:
- {
- "title": "旅行计划",
- "stateRelationList": [{
- "beginId": 0,
- "targetId": 1,
- "eventId": 3,
- "action": "arrive|weather",
- "extMap": {
- "gift": "北京片皮鸭",
- "title": "北京 -> 往南开 -> 上海"
- }
- }, {
- "beginId": 1,
- "targetId": 2,
- "eventId": 3,
- "action": "arrive",
- "extMap": {
- "gift": "上海老字号糕点",
- "title": "上海 -> 往南开 -> 广州"
- }
- }]
- }
配置说明请参考以下表格。
请注意,Jon在extMap中通过gift声明了他的伴手礼,这样就能在action中获取gift并与好友分享:
param | function |
---|---|
title | 标题,用来声明该配置文件的作用 |
beginId | 起始状态ID编号 |
targetId | 目标状态ID编号 |
eventId | 驱动事件ID编号 |
action | 状态变更触发的action名,多个action使用竖线拼接 |
extMap | 拓展信息,这些数据将会在action触发的时候被响应的方法获取,用于差异化传参 |
ESM接收分别实现EsmStateInterface和EsmEventInterface接口的State和Event枚举,本次旅程需要定义以下枚举:
CityEnum,即Jon现在身处的城市,为State状态:
- public enum CityEnum implements EsmStateInterface {
-
- PEKING(0, "北京"),
- SHANGHAI(1, "上海"),
- GUANGZHOU(2, "广州"),
- ;
-
- private Integer code;
- private String desc;
-
- CityEnum(Integer code, String desc) {
- this.code = code;
- this.desc = desc;
- }
-
- @Override
- public Integer getCode() {
- return code;
- }
-
- public static CityEnum codeOf(Integer code) {
- return Arrays.stream(CityEnum.values()).filter(item -> item.getCode() == code).findFirst().orElseGet(null);
- }
-
- }
DriveEnum,即Jon的小轿车行进的方向,为Event事件:
- public enum DriveEnum implements EsmEventInterface {
-
- NORTH(0, "往北方开"),
- EAST(1, "往东方开"),
- WEST(2, "往西方开"),
- SOUTH(3, "往南方开"),
- ;
-
- private Integer code;
- private String desc;
-
- DriveEnum(Integer code, String desc) {
- this.code = code;
- this.desc = desc;
- }
-
- @Override
- public Integer getCode() {
- return code;
- }
-
- public static DriveEnum codeOf(Integer code) {
- return Arrays.stream(DriveEnum.values()).filter(item -> item.getCode() == code).findFirst().orElseGet(null);
- }
-
- }
上述两个接口都需要实现 Integer getCode() 方法,它让ESM明确知道枚举中的code是哪个字段,这样能用于标识State和Event。
除此之外,我们还定义了通过code获取枚举的静态方法,通过该方法可以告诉ESM如何通过code转换得到相应的枚举,这个方法在后续的例子中会用到。
如上文所述,作为Holo的爱好者,Jon在到达一座新城市后都会和他的线上战友们来一场聚会,这时候伴手礼是必不可少的。
接下来就定义Jon的伴手礼姿势,他会在聚会中给好友们惊喜。
在ESM中,Action通过反射的方式被执行,我们需要做的事情如下:
4.1定义Action执行类
ESM会在初始化时扫描当前主程序类加载所加载的所有类,在感知到 @EsmHandlerService 注解后,才会进一步解析该类的action处理方法。
因此,除了定义action处理方法外,不要忘了给类加上 @EsmHandlerService 注解。
4.2定义Action处理方法
ESM在State流转时,会在满足条件的情况下调用Action方法处理。
需要定义一个public方法,并标记 @EsmHandler 方法注解,此外,入参必须为 EsmState, EsmState, Map ,否则初始化会报错并退出主程序。
在 @EsmHandler 注解中,我们需要声明该Action的名称,这在上述配置文件的action字段中需要用到。
配置完成后,如下代码所示:
- @EsmHandlerService
- public class DialectAction {
-
- @EsmHandler("arrive")
- public void cantoneseSpeech(EsmState<CityEnum> sourceEsmState, EsmState<CityEnum> targetEsmState, Map<String, Object> paramMap) {
- String gift = MapUtils.getString(paramMap, "gift");
- log.info("从[{}]来到[{}],顺便从[{}]带了手信[{}]",
- sourceEsmState.getData().getDesc(),
- targetEsmState.getData().getDesc(),
- sourceEsmState.getData().getDesc(),
- gift);
- }
-
- @EsmHandler("weather")
- public void weatherSpeech(EsmState<CityEnum> sourceEsmState, EsmState<CityEnum> targetEsmState, Map<String, Object> paramMap) {
- log.info("这里也太热了吧");
- }
-
- }
在业务逻辑中,需先对状态进行初始化,这应该在你的工程启动的时候执行。
ESM通过 EsmService 提供服务,该类依次接收Event和State枚举泛型。
如下所示,通过 EsmInitUtil.initStateMachineFromFile 方法可以指定上述的配置文件路径,并根据配置文件初始化ESM, 获取初始化后的 EsmService 服务实例。
- // 通过配置初始化状态机
- EsmService<DriveEnum, CityEnum> esmService = EsmInitUtil.initStateMachineFromFile(new EsmTranslateInterface() {
- @Override
- public EsmStateInterface onStateInitialize(Integer code) {
- return CityEnum.codeOf(code);
- }
-
- @Override
- public EsmEventInterface onEventInitialize(Integer code) {
- return DriveEnum.codeOf(code);
- }
- }, ExampleMainTest.class.getResource("/").getPath() + "statemachine.json");
该方法需要接收 EsmTranslateInterface 接口,在接口的抽象方法中,需要实现通过code获取State和Event枚举的逻辑,这里就能用到上述枚举中声明的静态方法。
5.1指定当前状态,执行事件,获取下一个状态
EsmService.setCurrentState 配置当前状态;
EsmService.next(EsmEventInterface) 传入Event事件,返回下一个节点信息,并触发Action;
通过 EsmState.getData 可以获取State对应的状态枚举。
- public void nextActionTest() {
- // 配置当前节点
- esmService.setCurrentState(CityEnum.SHANGHAI);
- try {
- EsmState<CityEnum> esmState;
- // 触发event和action操作
- esmState = esmService.next(DriveEnum.SOUTH);
- Assert.assertEquals(esmState.getData(), CityEnum.GUANGZHOU);
- log.info("一路向北");
- esmState = esmService.next(DriveEnum.NORTH);
- Assert.assertEquals(esmState.getData(), CityEnum.SHANGHAI);
- log.info("一路再向北");
- esmState = esmService.next(DriveEnum.NORTH);
- Assert.assertEquals(esmState.getData(), CityEnum.PEKING);
- } catch (Exception e) {
- log.error("执行异常", e);
- }
- }
您可以在EasyStateMachineExample的 net.nathanye.esm.example.ExampleMainTest.nextActionTest 中执行该测试用例,Jon将会开始他的旅程并给好友带上精心挑选的当地特色美食。
显然,Jon有点受不了广东的炎热天气。
- [net.nathanye.esm.service.util.EsmInitUtil] - 正在通过[/D:/Program/EasyStateMachineGit/EasyStateMachineExample/target/test-classes/statemachine.json]配置初始化状态机
- [net.nathanye.esm.service.service.EsmListener] - 开始扫描状态机注解方法处理器
- [net.nathanye.esm.service.service.EsmListener] - 正在处理[net.nathanye.esm.example.action.DialectAction]类的状态机处理方法
- [net.nathanye.esm.service.service.EsmListener] - 已发现[net.nathanye.esm.example.action.DialectAction]状态机处理类的[weatherSpeech]处理方法,入参:[class net.nathanye.esm.service.model.EsmState, class net.nathanye.esm.service.model.EsmState, interface java.util.Map]
- [net.nathanye.esm.service.service.EsmListener] - 已发现[net.nathanye.esm.example.action.DialectAction]状态机处理类的[cantoneseSpeech]处理方法,入参:[class net.nathanye.esm.service.model.EsmState, class net.nathanye.esm.service.model.EsmState, interface java.util.Map]
- [net.nathanye.esm.service.service.EsmListener] - 完成扫描状态机注解方法处理器
- [net.nathanye.esm.service.service.EsmService] - [3(SOUTH)] = [0(PEKING)] -> [1(SHANGHAI)]
- [net.nathanye.esm.service.service.EsmService] - [3(SOUTH)] = [1(SHANGHAI)] -> [2(GUANGZHOU)]
- [net.nathanye.esm.service.service.EsmService] - [0(NORTH)] = [1(SHANGHAI)] -> [0(PEKING)]
- [net.nathanye.esm.service.service.EsmService] - [0(NORTH)] = [2(GUANGZHOU)] -> [1(SHANGHAI)]
- [net.nathanye.esm.example.action.DialectAction] - 从[上海]来到[广州],顺便从[上海]带了手信[上海老字号糕点]
- [net.nathanye.esm.example.action.DialectAction] - 这里也太热了吧
- [net.nathanye.esm.example.ExampleMainTest] - 一路向北
- [net.nathanye.esm.example.action.DialectAction] - 从[广州]来到[上海],顺便从[广州]带了手信[艇仔粥]
- [net.nathanye.esm.example.ExampleMainTest] - 一路再向北
- [net.nathanye.esm.example.action.DialectAction] - 从[上海]来到[北京],顺便从[上海]带了手信[上海老字号糕点]
5.2如果ESM迷路了,将抛出异常
执行 net.nathanye.esm.example.ExampleMainTest.exceptionActionTest 用例,可怜的Jon将会迷路。
- INFO [net.nathanye.esm.example.ExampleMainTest] - 继续一路向北
- ERROR [net.nathanye.esm.service.service.EsmService] - State machine process error
- java.lang.Exception: Can not find next ID state of [EsmState(id=0, data=PEKING, extMap=null, action=null, nodeIdMap={EsmEvent(eventId=3, eventObject=SOUTH)=EsmState.NextNodeObject(nodeId=1, extMap={gift=北京片皮鸭, title=北京 -> 往南开 -> 上海}, action=arrive)})] by Event[EsmEvent(eventId=0, eventObject=NORTH)]
- ERROR [net.nathanye.esm.example.ExampleMainTest] - 执行异常,当前状态为:[PEKING]
- java.lang.Exception: 无法找到下一个节点状态,state:PEKING, event:NORTH
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。