EasyStateMachine(ESM),一款不需要依赖Spring的状态机,支持代码和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,添加配置,如下节选部分配置:

{
    "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并与好友分享:

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

3.Enum 定义枚举

ESM接收分别实现EsmStateInterfaceEsmEventInterface接口的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转换得到相应的枚举,这个方法在后续的例子中会用到。

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字段中需要用到。

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

@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("这里也太热了吧");
    }

}

5.Usage 使用

在业务逻辑中,需先对状态进行初始化,这应该在你的工程启动的时候执行。
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

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值