当前位置:   article > 正文

EasyStateMachine(ESM),一款不需要依赖Spring的状态机,支持代码和json配置文件的方式初始化状态机_statemachine json

statemachine json

项目G地址

https://github.com/NoticeVengus/EasyStateMachineicon-default.png?t=LA92https://github.com/NoticeVengus/EasyStateMachine

EasyStateMachine

EasyStateMachine,以下简称ESM,是一款不需要依赖Spring状态机,支持代码和json配置文件的方式初始化状态机, 并通过注解形式触发Action。

Version版本

versiondatedetail
1.0.02021.11.21初始版本

Dependencies 引入依赖

nameversiondetail
lombok1.18.20实现注解式构造和成员变量配置
fastjson1.2.76序列化及反序列化参数
slf4j2.0.0-alpha5日志格式化输出
commons-logging1.2日志工具包
commons-lang33.12.0语言工具包
commons-collection44.4集合类工具包

Example 例子

1.Requirement 需求

Jon是一名热爱旅游的小伙子,今天开始他的北上广之旅。
开着心爱的小轿车,跟着导航,准备出发!
每到一座城市,Jon都会买上当地的特色美食,和下一个目的地的Holo线下好友分享。
以下是他的旅游目的地城市和轿车行进线路图:

Image text

我们将用ESM来维护他的这趟旅程,告诉Jon往哪个方向开,会到达哪一座城市,而且不要忘了带上当地美食!

2.Property 配置文件

ESM提供Util工具类,通过工具类可以读取特定规则的json配置文件,这样就不需要在代码中维护负责的状态变更逻辑了。
但在写配置文件之前,还需要定义好状态机必备的两个要素,即State和Event。
在本次旅程中,State为Jon当前身处的城市,Event为轿车行进的方向。
在ESM中需要给每一个State和Event定义一个Integer类型的唯一编码,当然,同样的编码在State和Event中是可以重复的。

State定义如下:

codedetail
0北京
1上海
2广州

Event定义如下:

codedetail
0往北方开
1往东方开
2往西方开
3往南方开

定义好编号后,就可以配置json文件了,新建statemachind.json,添加配置,如下节选部分配置:

  1. {
  2. "title": "旅行计划",
  3. "stateRelationList": [{
  4. "beginId": 0,
  5. "targetId": 1,
  6. "eventId": 3,
  7. "action": "arrive|weather",
  8. "extMap": {
  9. "gift": "北京片皮鸭",
  10. "title": "北京 -> 往南开 -> 上海"
  11. }
  12. }, {
  13. "beginId": 1,
  14. "targetId": 2,
  15. "eventId": 3,
  16. "action": "arrive",
  17. "extMap": {
  18. "gift": "上海老字号糕点",
  19. "title": "上海 -> 往南开 -> 广州"
  20. }
  21. }]
  22. }

配置说明请参考以下表格。
请注意,Jon在extMap中通过gift声明了他的伴手礼,这样就能在action中获取gift并与好友分享:

paramfunction
title标题,用来声明该配置文件的作用
beginId起始状态ID编号
targetId目标状态ID编号
eventId驱动事件ID编号
action状态变更触发的action名,多个action使用竖线拼接
extMap拓展信息,这些数据将会在action触发的时候被响应的方法获取,用于差异化传参

3.Enum 定义枚举

ESM接收分别实现EsmStateInterfaceEsmEventInterface接口的State和Event枚举,本次旅程需要定义以下枚举:

CityEnum,即Jon现在身处的城市,为State状态:

  1. public enum CityEnum implements EsmStateInterface {
  2. PEKING(0, "北京"),
  3. SHANGHAI(1, "上海"),
  4. GUANGZHOU(2, "广州"),
  5. ;
  6. private Integer code;
  7. private String desc;
  8. CityEnum(Integer code, String desc) {
  9. this.code = code;
  10. this.desc = desc;
  11. }
  12. @Override
  13. public Integer getCode() {
  14. return code;
  15. }
  16. public static CityEnum codeOf(Integer code) {
  17. return Arrays.stream(CityEnum.values()).filter(item -> item.getCode() == code).findFirst().orElseGet(null);
  18. }
  19. }

DriveEnum,即Jon的小轿车行进的方向,为Event事件:

  1. public enum DriveEnum implements EsmEventInterface {
  2. NORTH(0, "往北方开"),
  3. EAST(1, "往东方开"),
  4. WEST(2, "往西方开"),
  5. SOUTH(3, "往南方开"),
  6. ;
  7. private Integer code;
  8. private String desc;
  9. DriveEnum(Integer code, String desc) {
  10. this.code = code;
  11. this.desc = desc;
  12. }
  13. @Override
  14. public Integer getCode() {
  15. return code;
  16. }
  17. public static DriveEnum codeOf(Integer code) {
  18. return Arrays.stream(DriveEnum.values()).filter(item -> item.getCode() == code).findFirst().orElseGet(null);
  19. }
  20. }

上述两个接口都需要实现 Integer getCode() 方法,它让ESM明确知道枚举中的code是哪个字段,这样能用于标识State和Event。
除此之外,我们还定义了通过code获取枚举的静态方法,通过该方法可以告诉ESM如何通过code转换得到相应的枚举,这个方法在后续的例子中会用到。

4.Action 定义Action

如上文所述,作为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字段中需要用到。

配置完成后,如下代码所示:

  1. @EsmHandlerService
  2. public class DialectAction {
  3. @EsmHandler("arrive")
  4. public void cantoneseSpeech(EsmState<CityEnum> sourceEsmState, EsmState<CityEnum> targetEsmState, Map<String, Object> paramMap) {
  5. String gift = MapUtils.getString(paramMap, "gift");
  6. log.info("从[{}]来到[{}],顺便从[{}]带了手信[{}]",
  7. sourceEsmState.getData().getDesc(),
  8. targetEsmState.getData().getDesc(),
  9. sourceEsmState.getData().getDesc(),
  10. gift);
  11. }
  12. @EsmHandler("weather")
  13. public void weatherSpeech(EsmState<CityEnum> sourceEsmState, EsmState<CityEnum> targetEsmState, Map<String, Object> paramMap) {
  14. log.info("这里也太热了吧");
  15. }
  16. }

5.Usage 使用

在业务逻辑中,需先对状态进行初始化,这应该在你的工程启动的时候执行。
ESM通过 EsmService 提供服务,该类依次接收Event和State枚举泛型。
如下所示,通过 EsmInitUtil.initStateMachineFromFile 方法可以指定上述的配置文件路径,并根据配置文件初始化ESM, 获取初始化后的 EsmService 服务实例。

  1. // 通过配置初始化状态机
  2. EsmService<DriveEnum, CityEnum> esmService = EsmInitUtil.initStateMachineFromFile(new EsmTranslateInterface() {
  3. @Override
  4. public EsmStateInterface onStateInitialize(Integer code) {
  5. return CityEnum.codeOf(code);
  6. }
  7. @Override
  8. public EsmEventInterface onEventInitialize(Integer code) {
  9. return DriveEnum.codeOf(code);
  10. }
  11. }, ExampleMainTest.class.getResource("/").getPath() + "statemachine.json");

该方法需要接收 EsmTranslateInterface 接口,在接口的抽象方法中,需要实现通过code获取State和Event枚举的逻辑,这里就能用到上述枚举中声明的静态方法。

5.1指定当前状态,执行事件,获取下一个状态

EsmService.setCurrentState 配置当前状态;
EsmService.next(EsmEventInterface) 传入Event事件,返回下一个节点信息,并触发Action;
通过 EsmState.getData 可以获取State对应的状态枚举。

  1. public void nextActionTest() {
  2. // 配置当前节点
  3. esmService.setCurrentState(CityEnum.SHANGHAI);
  4. try {
  5. EsmState<CityEnum> esmState;
  6. // 触发event和action操作
  7. esmState = esmService.next(DriveEnum.SOUTH);
  8. Assert.assertEquals(esmState.getData(), CityEnum.GUANGZHOU);
  9. log.info("一路向北");
  10. esmState = esmService.next(DriveEnum.NORTH);
  11. Assert.assertEquals(esmState.getData(), CityEnum.SHANGHAI);
  12. log.info("一路再向北");
  13. esmState = esmService.next(DriveEnum.NORTH);
  14. Assert.assertEquals(esmState.getData(), CityEnum.PEKING);
  15. } catch (Exception e) {
  16. log.error("执行异常", e);
  17. }
  18. }

您可以在EasyStateMachineExample的 net.nathanye.esm.example.ExampleMainTest.nextActionTest 中执行该测试用例,Jon将会开始他的旅程并给好友带上精心挑选的当地特色美食。
显然,Jon有点受不了广东的炎热天气。

  1. [net.nathanye.esm.service.util.EsmInitUtil] - 正在通过[/D:/Program/EasyStateMachineGit/EasyStateMachineExample/target/test-classes/statemachine.json]配置初始化状态机
  2. [net.nathanye.esm.service.service.EsmListener] - 开始扫描状态机注解方法处理器
  3. [net.nathanye.esm.service.service.EsmListener] - 正在处理[net.nathanye.esm.example.action.DialectAction]类的状态机处理方法
  4. [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]
  5. [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]
  6. [net.nathanye.esm.service.service.EsmListener] - 完成扫描状态机注解方法处理器
  7. [net.nathanye.esm.service.service.EsmService] - [3(SOUTH)] = [0(PEKING)] -> [1(SHANGHAI)]
  8. [net.nathanye.esm.service.service.EsmService] - [3(SOUTH)] = [1(SHANGHAI)] -> [2(GUANGZHOU)]
  9. [net.nathanye.esm.service.service.EsmService] - [0(NORTH)] = [1(SHANGHAI)] -> [0(PEKING)]
  10. [net.nathanye.esm.service.service.EsmService] - [0(NORTH)] = [2(GUANGZHOU)] -> [1(SHANGHAI)]
  11. [net.nathanye.esm.example.action.DialectAction] - 从[上海]来到[广州],顺便从[上海]带了手信[上海老字号糕点]
  12. [net.nathanye.esm.example.action.DialectAction] - 这里也太热了吧
  13. [net.nathanye.esm.example.ExampleMainTest] - 一路向北
  14. [net.nathanye.esm.example.action.DialectAction] - 从[广州]来到[上海],顺便从[广州]带了手信[艇仔粥]
  15. [net.nathanye.esm.example.ExampleMainTest] - 一路再向北
  16. [net.nathanye.esm.example.action.DialectAction] - 从[上海]来到[北京],顺便从[上海]带了手信[上海老字号糕点]

5.2如果ESM迷路了,将抛出异常

执行 net.nathanye.esm.example.ExampleMainTest.exceptionActionTest 用例,可怜的Jon将会迷路。

  1. INFO [net.nathanye.esm.example.ExampleMainTest] - 继续一路向北
  2. ERROR [net.nathanye.esm.service.service.EsmService] - State machine process error
  3. 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)]
  4. ERROR [net.nathanye.esm.example.ExampleMainTest] - 执行异常,当前状态为:[PEKING]
  5. java.lang.Exception: 无法找到下一个节点状态,state:PEKING, event:NORTH

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/黑客灵魂/article/detail/811392
推荐阅读
相关标签
  

闽ICP备14008679号