java有限状态机探讨

java有限状态机探讨

前言

今天跟大家分享一个关于"状态机"的话题。状态属性在我们的现实生活中无处不在。比如经典的电商场景会有一系列的订单状态(待支付、待发货、已发货、超时、关闭);某个活动会有活动状态(待提交、审核中、审核成功、审核拒绝、已失效、带参与、已参与、未参与)等等。上述场景有一个共同问题:根据不同触发条件执行不同处理动作最后落地不同的状态。示例代码如下:

java 复制代码
Integer status=0;
    if(condition1){
        status=1;
    }else if(condition2){
        status=2;
    }else if(condition3){
        status=3;
    }else if(condition4){
        status=4;
    }

那我们最容易能想到的自然是if-else方案。那if-else方案会有什么问题呢?

  • 维护成本增加(复杂的业务流程,if.else代码几乎无法维护)
  • 可扩展性(随着业务的发展,业务过程也需要变更及扩展,但if.else代码段已经无法支持)
  • 可读性(没有可读性,变更风险特别大,可能会牵一发而动全身,线上事故层出不穷)
  • 高耦合(其他业务逻辑可能也会跟if-else代码块耦合在一起,带来更多的问题)

关于状态机

状态机是有限状态自动机的简称。有限^1^状态机(英语:finite-state machine,缩写:FSM)是表示有限个状态以及在这些状态之间的转移和动作等行为的数学计算模型。

各个状态机的方案

枚举状态机

Java中的枚举是一个定义了一系列常量的特殊类(隐式继承自class java.lang.Enum)。枚举类型因为自身的线程安全性保障和高可读性特性,是简单状态机的首选。

  • Java枚举有一个比较有趣的特性即它允许为实例编写方法,从而为每个实例赋予其行为。实现也很简单,定义一个抽象的方法即可,这样每个实例必须强制重写该方法
java 复制代码
public enum State {
    TYPE_A {
        @Override
        State getNextState(String checkcondition) {
            System.out.println("接受条件到下一状态, 条件 = " + checkcondition);
            returnTYPE_B;
        }
    },
    TYPE_B {
        @Override
        State getNextState(String checkcondition) {
            System.out.println("接受条件到下一状态, 条件 = " + checkcondition);
            return TYPE_C;
        }
    },
    TYPE_C {
        @Override
        State getNextState(String checkcondition) {
            System.out.println("接受条件到下一状态, 条件 = " + checkcondition);
            return FINAL;
        }
    },
    FINAL {
        @Override
        State getNextState(String checkcondition) {
            System.out.println("接受条件到下一状态, 条件 = " + checkcondition);
            return this;
        }
    };
	//根据条件获取下一状态
    abstract State getNextState(String checkcondition);
}

状态模式实现状态机

状态模式是23中设计模式中一种,当一个对象内在状态改变时允许其改变行为,这个对象看起来像改变了其类. 结构类图如下:

  • 环境类(Context)角色:也称为上下文,它定义了客户端需要的接口,内部维护一个当前状态,并负责具体状态的切换。
  • 抽象状态(State)角色:定义一个接口,用以封装环境对象中的特定状态所对应的行为,可以有一个或多个行为。
  • 具体状态(Concrete State)角色:实现抽象状态所对应的行为,并且在需要的情况下进行状态切换。

定义一个State接口,它可以有N个实现类,每个实现类需重写接口State定义的handle方法。它还有一个Context上下文类,内部持有一个State对象引用,外部状态发生改变(构造器内传入不同实现类),最终实现类自身行为动作也接着改变(实现类调用其自身的handle方法)。

arduino 复制代码
//存在的问题:
若一个事物有多个状态,就会导致子类太多了,产生类膨胀。

自定义状态机

自己写适合业务需求的有限状态机

自定义状态机1
  • 比如有此业务的状态流转逻辑:
java 复制代码
//定义事件枚举
enum EventEnum {
    /**
     * 初始化到主播池
     */
    INIT,
    /**
     * 点击邀请
     */
    CLICK_INVITATION,
    /**
     * 点击活动页面
     */
    CLICK_ACTIVITY,
    /**
     * 点击申请签约
     */
    CLICK_APPLY_SIGN,
    /**
     * 执行了某个定时任务
     */
    EXEC_JOB,
    ;
}
java 复制代码
//定义状态枚举
public enum StateEnum {
    INVITED_ING(1, "待邀约"),
    CONSULTED_ING(2, "待查阅"),
    PARTICIPATE_ING(3, "待参与"),
    SIGN_ING(4, "待签约"),
    SIGN_DONE(5, "已签约"),
    NOT_PARTICIPATING(6, "未参与"),
    INVALID(7,"失效"),
    ;
    private Integer code;
    private String desc;
}
java 复制代码
//状态handle接口
interface IStateHandle<T, R> {
	//具体操作逻辑方法
    R handle(T t);
}
java 复制代码
//通用的操作方法
class CommonHandle implements IStateHandle<CommonStateDto, String> {
    @Override
    public String handle(CommonStateDto s) {
        System.out.println(String.format("接受到【%s】,处理通用的业务逻辑中...", s));
        NewStateMachine newStateMachine = new NewStateMachine();
        StateEnum state = newStateMachine.getNext(StateEnum.PARTICIPATE_ING, EventEnum.EXEC_JOB);
        System.out.println(state);
        return "通用的流程处理完成";
    }
}
//待签约处理逻辑
class SignIngHandle implements IStateHandle<CommonStateDto, String> {
    @Override
    public String handle(CommonStateDto s) {
        System.out.println(String.format("接受到【%s】,待签约状态处理业务逻辑中...", s));
        return "流程处理完成";
    }
}
java 复制代码
class StateProcess {
	//现态
    private StateEnum from;
    //目标状态
    private StateEnum to;
    //操作事件
    private EventEnum event;
    //操作handle类
    private IStateHandle stateHandle;
}
java 复制代码
//初始化状态机
private HashBasedTable<StateEnum, EventEnum, SopExec> hashBasedTable = HashBasedTable.create();
    //初始化流程状态
    {
        List<StateProcess> stateProcesses = init();
        stateProcesses.forEach(item -> {
            SopExec sopExec = new SopExec();
            sopExec.setNextState(item.getTo());
            sopExec.setStateHandle(item.getStateHandle());
            hashBasedTable.put(item.getFrom(), item.getEvent(), sopExec);
        });
    }
    abstract List<StateProcess> init();
    
List<StateProcess> init() {
        return Arrays.asList(
                StateProcess.builder()
                        .from(StateEnum.INVITED_ING)
                        .event(EventEnum.CLICK_INVITATION)
                        .stateHandle(new CommonHandle())
                        .to(StateEnum.CONSULTED_ING)
                        .build(),
                StateProcess.builder()
                        .from(StateEnum.CONSULTED_ING)
                        .event(EventEnum.CLICK_ACTIVITY)
                        .stateHandle(new CommonHandle())
                        .to(StateEnum.PARTICIPATE_ING)
                        .build(),
                StateProcess.builder()
                        .from(StateEnum.PARTICIPATE_ING)
                        .event(EventEnum.EXEC_JOB)
                        .stateHandle(new SignIngHandle())
                        .build()
        );
    }
自定义状态机2-客户回访
  • 比如有此业务的状态流转逻辑:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wgAcx3JM-1671693216921)(null)

java 复制代码
//操作事件枚举
@AllArgsConstructor
@Getter
@Slf4j
public enum EventTypeEnum {
    OPT_1(Arrays.asList(1), 1, 2, "待分配点击分配"),
    OPT_2(Arrays.asList(1), 2, 12, "待分配点击不处理"),
    OPT_3(Arrays.asList(2, 3, 4, 5, 6, 7, 8, 9), 3, 12, "已分配点击不处理"),
    OPT_4(Arrays.asList(2, 4, 5), 4, 4, "待处理-未建联页签-跟进中-联系失败/处理中"),
    OPT_5(Arrays.asList(2, 4, 5), 5, 5, "待处理-未建联页签-跟进中-联系成功(工单)/处理中"),
    OPT_6(Arrays.asList(2, 3, 4, 5), 6, 7, "待处理-处理完成-可继续进行开播/处理中"),
    OPT_7(Arrays.asList(2, 3, 4, 5), 7, 8, "待处理-处理完成-无法进行开播/处理中"),
    OPT_8(Arrays.asList(2, 3, 4, 5), 8, 6, "待处理-处理完成-联系主播失败/处理中"),
    ;

    /**
     * 当前状态
     * {@link VisitStatusEnum}
     */
    private List<Integer> code;
    /**
     * 事件类型
     */
    private Integer eventType;
    /**
     * 下一个状态
     */
    private Integer nextStatus;
    /**
     * 描述
     */
    private String desc;

    /**
     * 根据当前状态和当前事件操作 获取下一状态值 (带校验
     *
     * @param code
     * @param eventType
     * @return
     */
    public static Integer getNextStatusCheck(Integer code, Integer eventType) {
        if (ObjectUtil.isNull(code)) {
            throw new BusinessRuntimeException("当前状态为空");
        }
        EventTypeEnum nextStatusEnum = getNextStatusNotCheck(eventType);
        if (nextStatusEnum == null) {
            throw new BusinessRuntimeException("无此事件操作");
        }
        if (!nextStatusEnum.getCode().contains(code)) {
            log.info(MessageFormat.format("当前状态:{0},当前事件操作:{1}", code, eventType));
            throw new BusinessRuntimeException("当前状态不允许操作此事件");
        }
        return nextStatusEnum.getNextStatus();
    }
java 复制代码
//回访状态枚举
public enum VisitStatusEnum {
    /**
     * 具体方法见{@link ZbReturnStatusService}
     */
    DEFAULT(0, "未知", ""),
    //IOutAnchorService#notStartRoomsData 接口触发状态变更
    TO_BE_ALLOCATED(1, "待分配", ""),
    TODO_NOT_CONTACT(2, "待处理-未建联", "todoNotContact"),
    //定时任务触发状态变更
    TODO_NOT_RECALLED(3, "待处理-开播未召回", ""),
    ING_NOT_CONTACT(4, "处理中-建联失败", "ingNotContact"),
    ING_ORDER(5, "处理中-工单", "ingOrder"),
    DONE_FAILED(6, "已处理-建联失败", "doneFailed"),
    DONE_TO_CONFIRM(7, "已处理-待确认开播", "doneToConfirm"),
    DONE_NOT_COME(8, "已处理-无法开播", "doneNotCome"),
    //定时任务 从房间中台获取最近开播时间 来更新此状态
    DONE_COME(9, "已处理-已召回开播", "doneCome"),
    //定时任务   从已处理-待召回 页签中对应21天的数据更新到此状态
    DONE_NOT_RECALLED(11, "已处理-未召回", "doneNotRecalled"),
    NOT_HANDLE(12, "不处理", "notHandle"),
    ;
    /**
     * 编号对应 数据库中值
     */
    private Integer code;
    /**
     * 页面标签的名字
     */
    private String name;
    /**
     * 状态变更操作方法名
     */
    private String method;
java 复制代码
		//根据当前状态和当前事件操作 获取下一状态值 (带校验
        Integer nextStatus = EventTypeEnum.getNextStatusCheck(currentStatus, param.getEventType());
        //根据下一状态查询对应操作方法枚举
        VisitStatusEnum statusEnum = VisitStatusEnum.of(nextStatus);
        //执行ZbReturnStatusService类中对应方法
        redisLockUtils.execLock(StringUtils.join(Constants.RETURN_VISIT_CHANGE_STATUS_KEY,param.getBusinessId())
                    ,"回访状态变更",500L,() -> VisitStatusEnum.doInvoke(statusEnum, param, finalZbReturnVisit));
        

开源框架实现

目前开源的状态机实现方案有spring-statemachine、squirrel-foundation、cola-component-statemachine等。

perl 复制代码
前面两个状态机普通使用下来存在两个问题:
太复杂(因为基本囊括了以上列举的所有功能,功能是强大了,但也搞得体积过于庞大、臃肿、很重。很多功能实际生产场景中根本用不到。(支持的高阶功能有:状态的嵌套(substate),状态的并行(parallel,fork,join)、子状态机等等。)
性能差(这些状态机都是有状态的,有状态意味着多线程并发情况下如果是单个实例就容易出现线程安全问题。在如今的普遍分布式多线程环境中,你就不得不每次一个请求就创建一个状态机实例。但问题来了一旦碰到某些状态机它的构建过程很复杂,如果当下QPS又很高话,往往会造成系统的性能瓶颈。
  • 前面两个开源框架太复杂

    他们的优点是功能很完备,缺点也是功能很完备

  • 开源状态机性能差

    因为有状态,所以不是线程安全的,每次使用不得不重新build一个新的状态机实例

鉴于复杂性和性能的考虑,张建飞-Frank他们团队研发并开源了一个无状态的状态机引擎,简洁的仅支持状态流转的状态机,不需要支持嵌套,并行等高级玩法;并且本身是无状态的.

cola状态机领域模型
  1. State:状态
  2. Event:事件,状态由事件触发,引起变化
  3. Transition:流转,表示从一个状态到另一个状态
  4. External Transition:外部流转,两个不同状态之间的流转
  5. Internal Transition:内部流转,同一个状态之间的流转
  6. Condition:条件,表示是否允许到达某个状态
  7. Action:动作,到达某个状态之后,可以做什么
  8. StateMachine:状态机
  • 核心语义模型

一个状态机(StateMachine)包含多个状态(State)。一个状态(State)包含多个流转(Transition),一个Transition各包含一个Condition和Action。状态State分源状态(Source)和目标状态(Target)。源状态响应一个事件后,满足一定触发条件,经过流转,执行Action动作,最后返回Target状态.

状态机的实现简单,同样,他的使用也不难。如下面的代码所示,它展现了cola状态机支持的全部三种transition方式。

java 复制代码
StateMachineBuilder<States, Events, Context> builder = StateMachineBuilderFactory.create();
        //external transition
        builder.externalTransition()
                .from(States.STATE1)
                .to(States.STATE2)
                .on(Events.EVENT1)
                .when(checkCondition())
                .perform(doAction());

        //internal transition
        builder.internalTransition()
                .within(States.STATE2)
                .on(Events.INTERNAL_EVENT)
                .when(checkCondition())
                .perform(doAction());

        //external transitions
        builder.externalTransitions()
                .fromAmong(States.STATE1, States.STATE2, States.STATE3)
                .to(States.STATE4)
                .on(Events.EVENT4)
                .when(checkCondition())
                .perform(doAction());

        builder.build(machineId);
        StateMachine<States, Events, Context> stateMachine = StateMachineFactory.get(machineId);
        stateMachine.showStateMachine();

cola模型的核心代码

kotlin 复制代码
class TransitionBuilderImpl<S,E,C> implements ExternalTransitionBuilder<S,E,C>, InternalTransitionBuilder<S,E,C>, From<S,E,C>, On<S,E,C>, To<S,E,C> {
    //状态机
    final Map<S, State<S, E, C>> stateMap;
    //原状态
    private State<S, E, C> source;
    //目标状态
    protected State<S, E, C> target;
    //流转
    private Transition<S, E, C> transition;
     //流转类型
    final TransitionType transitionType;
...
    // 此时只是把from的state新增到stateMap中,返回结果赋给本地变量source 然后返回当前对象
    @Override
    public From<S, E, C> from(S stateId) {
        source = StateHelper.getState(stateMap, stateId);
        return this;
    }
 
    // 此时只是把to的state新增到stateMap中,返回结果赋给本地变量target
    @Override
    public To<S, E, C> to(S stateId) {
        target = StateHelper.getState(stateMap, stateId);
        return this;
    }
  //Transition是一个具体做事的流转,其中包含事件,目标状态和流转类型
  //看下面源码就是一个事件对应Transition
  @Override
    public On<S, E, C> on(E event) {
        transition = source.addTransition(event, target, transitionType);
        return this;
    }
//该执行流转的条件
@Override
    public When<S, E, C> when(Condition<C> condition) {
        transition.setCondition(condition);
        return this;
    }
//设置transition的action
 @Override
    public void perform(Action<S, E, C> action) {
        transition.setAction(action);
    }
...
}

build的时候发现from后面只能.to这种方式实现其实是连贯接口实现的(fluent interfaces)

通过这种Fluent Interface的方式,我们确保了Fluent调用的顺序,如下图所示,在externalTransition的后面你只能调用from,在from的后面你只能调用to,从而保证了状态机构建的语义正确性和连贯性。

举一个普通的状态流转过程分析:

java 复制代码
	@Test
    public void testExternalNormal(){
        //生成一个状态机build
        StateMachineBuilder<States, Events, Context> builder = StateMachineBuilderFactory.create();
        //设置一个外部状态转移类型的build,并设置from\to\on\when\perform
        builder.externalTransition()
                .from(States.STATE1)
                .to(States.STATE2)
                .on(Events.EVENT1)
                .when(checkCondition())
                .perform(doAction());
        //设置状态机id和ready,并在StateMachineFactory中的stateMachineMap进行注册
        StateMachine<States, Events, Context> stateMachine = builder.build(MACHINE_ID);
        //触发状态机
        States target = stateMachine.fireEvent(States.STATE1, Events.EVENT1, Context.builder().entityId("123465").operator("xingtian").build());
        Assert.assertEquals(States.STATE2, target);
    }
java 复制代码
//fireEvent方法
    public S fireEvent(S sourceStateId, E event, C ctx) {
        isReady();
        //根据sourceStateId找到符合条件的Transition
        Transition<S, E, C> transition = routeTransition(sourceStateId, event, ctx);
 
        if (transition == null) {
            Debugger.debug("There is no Transition for " + event);
            failCallback.onFail(sourceStateId, event, ctx);
            return sourceStateId;
        }
        //找到transition后执行transit方法(最终执行Action后返回目标State)
        return transition.transit(ctx, false).getId();
    }
kotlin 复制代码
//transit方法
    public State<S, E, C> transit(C ctx, boolean checkCondition) {
        Debugger.debug("Do transition: "+this);
        this.verify();
        //checkCondition为false或不指定when触发条件亦或匹配when触发条件;都将执行自定义的perform函数
        if (!checkCondition || condition == null || condition.isSatisfied(ctx)) {
            //如果自定义的perform函数有指定,将执行perform函数
            if(action != null){
                action.execute(source.getId(), target.getId(), event, ctx);
            }
            return target;
        }
        Debugger.debug("Condition is not satisfied, stay at the "+source+" state ");
        return source;
    }
xml 复制代码
<dependency>
    <groupId>com.alibaba.cola</groupId>
    <artifactId>cola-component-statemachine</artifactId>
    <version>4.3.1</version>
</dependency>
scss 复制代码
@Configuration
public class StateMachineRegist {
    private final String STATE_MACHINE_ID="stateMachineId";
    /**
     * 构建状态机实例
     */
    @Bean
    public StateMachine<ApplyStatusEnum, Event, LeaveContext> stateMachine() {
 
        StateMachineBuilder<States, Events, Context> builder = StateMachineBuilderFactory.create();
        builder.externalTransition()
                .from(States.STATE1)
                .to(States.STATE2)
                .on(Events.EVENT1)
                .when(checkCondition())
                .perform(doAction());

        builder.internalTransition()
                .within(States.STATE2)
                .on(Events.INTERNAL_EVENT)
                .when(checkCondition())
                .perform(doAction());

        builder.externalTransition()
                .from(States.STATE2)
                .to(States.STATE1)
                .on(Events.EVENT2)
                .when(checkCondition())
                .perform(doAction());

        builder.externalTransition()
                .from(States.STATE1)
                .to(States.STATE3)
                .on(Events.EVENT3)
                .when(checkCondition())
                .perform(doAction());

        builder.externalTransitions()
                .fromAmong(States.STATE1, States.STATE2, States.STATE3)
                .to(States.STATE4)
                .on(Events.EVENT4)
                .when(checkCondition())
                .perform(doAction());

        return builder.build(STATE_MACHINE_ID);

    }
}
 
 
StateMachine<States, Events, Context> stateMachine = StateMachineFactory.get(STATE_MACHINE_ID);
States target = stateMachine.fireEvent(States.STATE1, Events.EVENT1, new Context());
  • 状态机利弊

增加了代码量和固有的展示形式 但可维护性可读性可扩展性有一定提高

实现一个状态机引擎,教你看清DSL的本质:blog.csdn.net/significant...
github传送门

Footnotes

  1. 关于有限的解释:也就是被描述的事物的状态的数量是有限的,例如开关的状态只有"开"和"关"两个;灯的状态只有"亮"和"灭"等等\] [↩](#↩ "#user-content-fnref-1")

相关推荐
NE_STOP8 分钟前
SpringBoot--简单入门
java·spring
hqxstudying35 分钟前
Java创建型模式---原型模式
java·开发语言·设计模式·代码规范
Dcs1 小时前
VSCode等多款主流 IDE 爆出安全漏洞!插件“伪装认证”可执行恶意命令!
java
保持学习ing1 小时前
day1--项目搭建and内容管理模块
java·数据库·后端·docker·虚拟机
京东云开发者1 小时前
Java的SPI机制详解
java
超级小忍2 小时前
服务端向客户端主动推送数据的几种方法(Spring Boot 环境)
java·spring boot·后端
程序无bug2 小时前
Spring IoC注解式开发无敌详细(细节丰富)
java·后端
小莫分享2 小时前
Java Lombok 入门
java
程序无bug2 小时前
Spring 对于事务上的应用的详细说明
java·后端
食亨技术团队2 小时前
被忽略的 SAAS 生命线:操作日志有多重要
java·后端