Java有限状态机FSM(快速使用篇)

2,864 阅读4分钟

「这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战

前言

Java有限状态机FSM(基础篇)一文中,笔者介绍了有限状态机的概念、应用及其几种实现方式。

开源世界有几款优秀的有限状态机实现,比如 SquirrelStateless4j等,本文将对一款优秀的开源的Java有限状态机的使用进行介绍,并给出一些示例代码。

什么是 squirrel-foundation

其官方github地址是 hekailiang/squirrel: squirrel-foundation

其自我介绍如下

Just like the squirrel, a smallagilesmartalert and cute animal, squirrel-foundation is aimed to provide a lightweight, highly flexible and extensiblediagnosableeasy use and type safe Java state machine implementation for enterprise usage.

其致力于提供一个轻量化的、高灵活性和扩展性,便于纠错、使用以及类型安全的企业级Java状态机实现

要使用它,需要引入maven依赖如下

<dependency>
    <groupId>org.squirrelframework</groupId>
    <artifactId>squirrel-foundation</artifactId>
    <version>0.3.8</version>
</dependency>

快速使用

  1. 定义状态枚举

状态机是进行状态的转换,因此需要对其所拥有的所有状态进行定义

这里以一个商品的简单状态举例,定义如下

/**
 * 商品状态枚举
 * @author st4rlight
 * Created on 2022-02-04
 */
public enum CommodityState {
    WAIT_AUDIT(1, "待审核"),
    WAIT_MODIFY(2, "待修改"),
    OFF_SHELF(3, "未上架"),
    ON_SHELF(4, "已上架");
}
  1. 定义事件枚举

状态机是由事件进行驱动,使得其从一个状态变换到另一个状态,因此需要定义所有的变换事件

同样以商品的简单事件举例,定义如下

/**
 * 商品事件枚举
 * @author st4rlight
 * Created on 2022-02-04
 */
public enum CommodityEvent {
    AUDIT_PASS(1, "审核通过"),
    EDIT_INFO(2, "编辑信息"),
    UP_SHELF(3, "上架"),
    DOWN_SHELF(4, "下架");
}
  1. 定义操作的上下文

状态转换时,会包含一些额外的参数信息,可以用于做日志记录,或者根据不同的条件进行不同的转换等,这些额外信息需要传入

同样以商品状态变换的上下文举例,定义如下

/**
 * 商品状态机上下文定义
 * @author st4rlight
 * Created on 2022-02-04
 */
public class CommodityContext {
    // 商品信息
    private Commodity commodity;
    // 操作人员
    private String operator;
}
  1. 伴生字符串常量类

定义状态机时,需要指定在什么事件下,使得状态机从哪个状态变换到哪个状态,特别在使用注解进行定义时,其需要的参数是字符串常量,直接使用枚举的 toString() 会报错,因此需要额外定义一个常量类,便于使用

但是这样子会导致多了一个依赖,需要注意在修改枚举名称时,同时也在常量类中野进行修改

示例定义如下

/**
 * 商品状态机枚举常量
 * @author st4rlight
 * Created on 2022-02-04
 */
public class StateMachineConstant {
    // 状态部分常量
    public static final String WAIT_AUDIT = "WAIT_AUDIT";
    // ......
    
    // 事件部分常量
    public static final String AUDIT_PASS = "AUDIT_PASS";
}

后续在定义状态机时,通过 import static 进行引入,这样子代码不长,也使得代码清晰,避免直接使用字符串进行定义。

  1. 定义状态机

由于状态机特别灵活,可以通过注解也可以通过流式api进行定义,同时也可以在事件或者状态上定义切面,因此这里仅介绍笔者的最佳实践

以商品状态机为例,定义如下

// 这里仅给出一个示例,不全写

/**
 * 商品状态机
 * @author st4rlight
 * Created on 2022-02-04
 */
@Transitions({
    @Transit(from = WAIT_AUDIT, to = OFF_SHELF, on = AUDIT_PASS)
})
public class CommodityFSM
    extends AbstractStateMachine<CommodityFSM, CommodityState, CommodityEvent, CommodityContext> {
}

一些使用建议

  • 避免使用过重地使用状态机,一般仅将状态机用于做状态转换(即类似于当成一个工具类使用),输入参数和事件,得到次态,并将状态机的变换直接通过注解方式写在状态机定义中,在什么事件下使得从哪个状态变换到哪个状态,一目了然
  • 在状态较多时,避免在状态机中定义切面事件,过重地使用状态机会导致后期维护上的困难,除非一些特别明显必定触发且不会发生改变的事件可以在切面事件中进行操作,否则不建议使用
  1. 定义状态机工具类
/**
 * 商品状态机工具类
 * @author st4rlight
 * Created on 2022-02-04
 */
public class CommodityFSMUtil {
    // 定义builder
    private static StateMachineBuilder<CommodityFSM, CommodityState, CommodityEvent, CommodityContext> builder =
            StateMachineBuilderFactory.create(
                    CommodityFSM.class, CommodityState.class, CommodityEvent.class, CommodityContext.class);

    // 静态工具方法,获取下一个状态
    public static CommodityState getNextState(CommodityState fromState, CommodityEvent event, CommodityContext context) {
        StateMachineConfiguration conf = StateMachineConfiguration
                .create()
                .enableDebugMode(true)
                .enableAutoStart(true);
        CommodityFSM sm = builder.newStateMachine(fromState, conf);

        try {
            sm.fire(event, context);
            return sm.getCurrentState();
        } catch (Exception ex) {
            // 异常处理
        }
    }
}

后话

下一篇将对 squirrel 状态机做详细介绍