0%

spring-statemachine入门

spring-statemachine入门

介绍

spring-statemachinespring设计于处理状态的变化,以及定义状态之间的转换规则。

状态机的基本组成

状态机主要由4要素组成:

  • 现态:当前所处的状态。
  • 条件:或者称为“事件”,在状态转换图中使用箭头标识,当满足某个条件(触发某个事件)之后,由一个状态转移到另一个状态。比如下图中“支付”这个箭头。
  • 动作:表示由现态转换到次态之后,需要执行的动作,不是必须的,可以是转换转换后不执行任何动作。比如下图中从[待收货]转换到[已收货]是否设置某个动作。
  • 次态:下一环状态。

​ 比如一个支付下单流程的状态转换图:

状态装换图.png

​ 订单的开始状态为[待支付],当用户支付之后状态流转为[已支付],这个地方是个分支节点,当状态流转到[已支付]后,根据一些业务规则,将状态流转到[待收货]或者[待开票],若是[待收货],通过收货行为,使状态流转到[结束];若是[待开票]状态,通过投递行为,使状态流转到[结束]。

Spring stateMachine

Spring StateMachine是一个基于 Spring框架的状态机框架,使用Spring StateMachine可以方便地在 Java应用中实现状态机功能,并可以与Spring框架的其他功能结合使用。

基本用法

1.添加依赖

我这里使用的是springboot工程,最好先引入官方提供的spring-statemachine-bom,这样基本上不会出现与spring framework版本不对应,出现的一些无法解释的坑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-bom</artifactId>
<version>2.3.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-starter</artifactId>
</dependency>
</dependencies>

2.订单状态枚举类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Getter
public enum StateEnum {

UNPAID("unpaid","待支付"),
PAID("paid","已支付"),
WAITING_FOR_RECEIVE("waitingForReceive","待收货"),
DONE("done","结束"),
INVOICE("invoice", "开票")
;
private final String type;

private final String value;

StateEnum(String type, String value) {
this.type = type;
this.value = value;
}

public static StateEnum getByState(String state) {
for (StateEnum stateEnum : StateEnum.values()) {
if (stateEnum.getType().equals(state)) {
return stateEnum;
}
}
return null;
}
}

3.事件枚举类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Getter
public enum EventEnum {

PAY("pay","支付"),
RECEIVE("receive","收货"),
DELIVERY("delivery","发货")
;

private final String type;

private final String value;

EventEnum(String type, String value) {
this.type = type;
this.value = value;
}
}

4.状态机配置

状态机配置类中包含状态配置、状态转换事件关系配置、监听器配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
@Configuration
// 这里最好手动设置name, 如果不设置, 默认使用stateMachineFactory这个名字, 由于这个类
// 需要注入到spring容器中, 如果需要创建两个不一样的状态机会出现重名的冲突。
@EnableStateMachineFactory(name = "feeStateMachineFactory")
@Slf4j
public class FeeStateMachineConfig extends StateMachineConfigurerAdapter<StateEnum, EventEnum> {

/**
* 配置状态: 初始状态、
*/
@Override
public void configure(StateMachineStateConfigurer<StateEnum, EventEnum> states) throws Exception {
states.withStates()
// 初始状态
.initial(StateEnum.UNPAID)
// 分支节点, 如果存在分支节点, 必须要在这里声明
.choice(StateEnum.PAID)
// 状态的所有值
.states(EnumSet.allOf(StateEnum.class));
}

/**
* 配置状态机监听器
*
* @author xiaocainiaoya
* @date 2023/7/14 16:04:34
* @param config
* @return: void
*/
@Override
public void configure(StateMachineConfigurationConfigurer<StateEnum, EventEnum> config) throws Exception {
config.withConfiguration()
// 设置全局监听器
.listener(listener())
// 设置状态机的ID, 可以认为是状态机的名称,在通过StateMachineFactory.getStateMachine()需要通过这个名称获取
// 在测试的过程中发现, 有些人写的文章里没有设置这个名称, 然后在使用StateMachineFactory.getStateMachine(machineId)
// 中的machineId使用的是数据库中查出的订单ID, 这样的写法在不使用OnTransition相关监听注解是不会有问题,但是如果
// 需要使用到@OnTransition相关注解就会导致无法进入到这个注解标记的方法中。
.machineId("stateMachine")
.autoStartup(false);
}

/**
* 配置状态转换和事件的转换关系
*
* @param transitions
* @throws Exception
*/
@Override
public void configure(StateMachineTransitionConfigurer<StateEnum, EventEnum> transitions) throws Exception {
transitions
.withExternal()
.source(StateEnum.UNPAID)
.target(StateEnum.PAID)
.event(EventEnum.PAY)
// guard()守卫函数,状态转移后进入, 如果返回的是true, 才进入到action方法中。
.guard(new Guard<StateEnum, EventEnum>() {
@Override
public boolean evaluate(StateContext<StateEnum, EventEnum> context) {
log.info("进入订单守卫函数");
return true;
}
})
.action(ctx -> log.info("action状态变更:{} -> {}.", StateEnum.UNPAID.getValue(), StateEnum.PAID.getValue()))
.and()
.withExternal()
.source(StateEnum.WAITING_FOR_RECEIVE)
.target(StateEnum.DONE)
.event(EventEnum.RECEIVE)
.action(ctx -> log.info("action状态变更:{} -> {}.", StateEnum.WAITING_FOR_RECEIVE.getValue(), StateEnum.DONE.getValue()))
.and()
// 表示分支
.withChoice()
.source(StateEnum.PAID)
// 类似于 if(first的第二个参数的方法如果是true)则进入到first第一个参数的状态,并进入first第三个参数的action
// 如果first的第二个参数返回的是false,则进入到last的第一个参数状态,并进入到first第二个参数的atction
.first(StateEnum.INVOICE, ctx -> (boolean) Optional.ofNullable(ctx.getMessage().getHeaders().get("invoice")).orElse(true), ctx -> log.info("进入发票分支"))
.last(StateEnum.WAITING_FOR_RECEIVE, ctx -> log.info("进入收货分支"))
.and()
.withExternal()
.source(StateEnum.INVOICE)
.target(StateEnum.WAITING_FOR_RECEIVE)
.event(EventEnum.DELIVERY)
.action(ctx -> log.info("action状态变更:{} -> {}.", StateEnum.INVOICE.getValue(), StateEnum.WAITING_FOR_RECEIVE.getValue()))
;
}

/**
* 全局监听器
*/
private StateMachineListener<StateEnum, EventEnum> listener() {
return new StateMachineListenerAdapter<StateEnum, EventEnum>() {
/**
* 当状态的转移在configure方法配置中时,会走到该方法。
*/
@Override
public void transition(Transition<StateEnum, EventEnum> transition) {
log.info("listener[{}]状态变更:{} -> {}", transition.getKind().name(),
transition.getSource() == null ? "NULL" : ofNullableState(transition.getSource().getId()),
transition.getTarget() == null ? "NULL" : ofNullableState(transition.getTarget().getId()));
}

/**
* 当发生的状态转移不在configure方法配置中时,会走到该方法,此处打印error日志,方便排查状态转移问题
*/
@Override
public void eventNotAccepted(Message<EventEnum> event) {
log.error("事件未收到: {}", event);
}

private Object ofNullableState(StateEnum s) {
return Optional.ofNullable(s)
.map(StateEnum::getValue)
.orElse(null);
}
};
}
}

4.测试调用

1
2
3
4
stateMachine = feeStateMachineFactory.getStateMachine("stateMachine");
stateMachine.sendEvent(MessageBuilder.withPayload(EventEnum.PAY)
.setHeader(JSON_STR, JSON.toJSONString(feeOrder))
.build())

使用注解方式配置监听器

​ 前面说到,如果需要使用注解方式配置监听器,一定一定要注意machineId的赋值。因为可以通过多种方式来处理状态转移之后的监听,所以并不一定所有人在使用时都会使用这种方式,但是使用这种方式就是一定要注意machineId的赋值问题。(可以在定义配置时就设置action也可以达到状态转移后进入某个函数进行相关处理。)

spring stateMachine中设置的监听,这些监听都有设置对应的注解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public interface StateMachineListener<S,E> {

void stateChanged(State<S,E> from, State<S,E> to);

void stateEntered(State<S,E> state);

void stateExited(State<S,E> state);

void eventNotAccepted(Message<E> event);

void transition(Transition<S, E> transition);

void transitionStarted(Transition<S, E> transition);

void transitionEnded(Transition<S, E> transition);

void stateMachineStarted(StateMachine<S, E> stateMachine);

void stateMachineStopped(StateMachine<S, E> stateMachine);

void stateMachineError(StateMachine<S, E> stateMachine, Exception exception);

void extendedStateChanged(Object key, Object value);

void stateContext(StateContext<S, E> stateContext);

}

可以通过注解的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @author :xiaocainiaoya
* @date : 2023/7/15 09:38
*/
//@Configuration
@WithStateMachine
public class StateMachineEventListener {

@OnTransition(target = "UNPAID")
public void create() {
System.out.println("订单创建");
}

//@StatesOnTransition
@OnTransition(source = "UNPAID", target = "PAID")
public void pay(Message<EventEnum> obj) {
// 获取消息中的订单对象
//Order order = (Order) message.getHeaders().get("order");
// 设置新状态
//order.setStates(States.WAITING_FOR_RECEIVE);
System.out.println("用户支付完毕,状态机反馈信息:");
}
}

持久化

​ 一般来说,我们的处理逻辑为从状态机工程中获取到一个状态机,但是不一定说每一台状态机都是从初始状态开始走,有可能订单目前处在某个状态,但是由于一些原因重启了服务,那么这是如果重新创建状态机那么状态需要从头开始走,这显然不符合逻辑,所以在获取到状态机之后,需要为这台状态机重新赋状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//从状态机工厂获取一个状态机
stateMachine = feeStateMachineFactory.getStateMachine("stateMachine");
stateMachine.stop();
stateMachine.getStateMachineAccessor().doWithAllRegions(sma -> {
//配置状态机拦截器,当状态发生转移时,会走到该拦截器中
sma.addStateMachineInterceptor(new StateMachineInterceptorAdapter<StateEnum, EventEnum>() {
@Override
public void preStateChange(State<StateEnum, EventEnum> state,
Message<EventEnum> message,
Transition<StateEnum, EventEnum> transition,
StateMachine<StateEnum, EventEnum> stateMachine,
StateMachine<StateEnum, EventEnum> rootStateMachine) {
log.info("preStateChange");
FeeOrder result = JSON.parseObject(String.class.cast(message.getHeaders().get(JSON_STR)), FeeOrder.class);
//更新状态机转移后的状态
result.setOrderStatus(state.getId().getType());
}
});
//将状态机的初始状态配置为DB中对应状态
sma.resetStateMachine(new DefaultStateMachineContext<>(stateEnum, null, null, null, null, "stateMachine"));
});
//启动状态机
stateMachine.start();

​ 尽管可以手动对状态机赋状态,但是这种方式也解决不了根本问题,以订单为例子,如果用户在发起创建订单之后,订单处在待支付状态,然后用户一直没有去支付,这时出现需要重启服务器的情况;或者如果部署了多个实例,某一台实例中状态机处在待支付状态,而用户去支付时请求走到了另一台服务器,这时在另一台服务器上状态走到了待开票,如果开票的请求又走到了原本处在待支付状态的服务器上呢?所以这里就变成必须与数据库中的订单状态实时校验?

所以这里引入的持久化。主要针对于故障恢复、可恢复性、长时间运行等情况。

持久化有多种方式,基于内存、基于db、基于redis、基于MongoDB等,这里以redis为例。

1.引入依赖

1
2
3
4
5
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-redis</artifactId>
<version>1.2.14.RELEASE</version>
</dependency>

2.redis配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class StateMachinePersisterConfig {

@Resource
private RedisConnectionFactory redisConnectionFactory;

/**
* Redis持久化配置
*/
@Bean
public RedisStateMachinePersister<StateEnum, EventEnum> persister() {
RedisStateMachineContextRepository<StateEnum, EventEnum> repository
= new RedisStateMachineContextRepository<StateEnum, EventEnum>(redisConnectionFactory);
return new RedisStateMachinePersister<>(new RepositoryStateMachinePersist<>(repository));
}
}

3.使用redis进行持久化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private synchronized boolean sendEvent(Events changeEvent, Order order) {
boolean result = false;
try {
stateMachine.start();
//尝试恢复状态机状态
stateMachineMemPersister.restore(stateMachine, String.valueOf(order.getId()));
Message message = MessageBuilder.withPayload(changeEvent)
.setHeader("order", order).build();
result = stateMachine.sendEvent(message);
//持久化状态机状态
stateMachineMemPersister.persist(stateMachine, String.valueOf(order.getId()));
} catch (Exception e) {
System.out.println("操作失败:" + e.getMessage());
} finally {
stateMachine.stop();
}
return result;
}

小结

spring stateMachine在简单实用之后感觉虽然在一定层度上对状态装换过程的代码编写风格进行了解耦,但是感觉由于spring stateMachine有点过于重量化,导致如果使用不当容易出现一些不可预判、出乎意料之外的问题。就比如之前提到的版本不对应监听注解失效的问题,在使用过程中它也不会报错,仅仅是始终无法进入到对应的注解中;还比如choice分支的问题、状态机不持久化带来的问题、若使用了持久化,持久化带来的问题等。

-------------本文结束感谢您的阅读-------------