我是靠谱客的博主 暴躁黑米,这篇文章主要介绍线上系统引入Spring状态机实践什么是状态机为什么要使用状态机如何引入状态机状态机的实践应用,现在分享给大家,希望可以做个参考。

什么是状态机

在某个起始状态下,当满足某个条件时,将状态转换到另一个状态的机制。状态机就是实现这一机制的控制元件。状态机的概念也经常出现在电气元件中。

而在软件开发中,状态机指的是一种在某个状态下,由某个事件触发,并将状态转移到目标状态的一个模块。

为什么要使用状态机

状态机解决的痛点在于,当某个业务流程很长,且很复杂时,对于状态的流转的维护成本将膨胀到不可接受的程度。而将状态的流转从业务代码中剥离出来,让开发过程中更加专注于业务处理,就成为了项目急需解决的问题。

状态机的引入,让程序开发不再需要关注复杂繁琐的状态迁移,只需要关注在当前状态及事件下,需要处理的业务逻辑。而状态的迁移交给状态机去完成即可。
简单的例子:一个订单创建后,处于待支付状态。在未使用状态机的情况下,在开发支付的逻辑时,需要关注订单的当前状态是否是待支付,以确保幂等;同时需要关注支付的过程;然后需要关注支付完成后,订单的目标状态。目标状态会有很多种,比如实体货物订单,支付完成后可能要进入待发货状态;而虚拟货物支付完成后,可能就直接进入了待使用状态。在不使用状态机的情况下,支付模块还需要关注订单的下一个状态。

在使用状态机的情况下,开发支付逻辑时,只需要关注支付的业务逻辑,完成支付后,告诉状态机,支付完成即可。状态机会做状态的前置校验,未满足状态时,不会调用业务代码。同时,状态机配置了各种情况下,订单的目标状态。只要支付模块告诉状态机,支付完成,状态机就会按照配置好的状态迁移,将订单转移到指定的状态去。

如何引入状态机

这里以Spring状态机为例,演示如何在项目中使用状态机。

引入状态机包含两种情况,一种是在项目设计的时候,就明确系统需要使用状态机。然而,在敏捷开发的项目里,一般在项目刚起步时,为了能快速迭代,不引入状态机。当业务达到一定的复杂程度,状态维护成本变大时,考虑引入状态机。

下面介绍状态机的几个落地方案。
在pom文件中引入状态机

复制代码
1
2
3
4
5
6
7
<!--状态机--> <dependency> <groupId>org.springframework.statemachine</groupId> <artifactId>spring-statemachine-starter</artifactId> <version>2.0.1.RELEASE</version> </dependency>

下面以一个简单的订单流程为例:
定义订单的状态结点和事件

复制代码
1
2
3
4
5
6
7
8
9
10
11
public enum OrderEvents { PAY, // 支付 RECEIVE // 收货 } public enum OrderStates { UNPAID, // 待支付 WAITING_FOR_RECEIVE, // 待收货 DONE; // 结束 }

构建状态机

复制代码
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
@Component public class OrderStateMachineBuilder { final static String MACHINEID = "orderMachine"; private final static String INSTALL_MACHINE_ID = "InstallOrderMachine"; /** * 构建状态机 * * @param beanFactory * @return * @throws Exception */ public StateMachine<OrderStates, OrderEvents> build(BeanFactory beanFactory, OrderStates orderState) throws Exception { StateMachineBuilder.Builder<OrderStates, OrderEvents> builder = StateMachineBuilder.builder(); System.out.println("构建订单状态机"); builder.configureConfiguration() .withConfiguration() .machineId(MACHINEID) .beanFactory(beanFactory); builder.configureStates() .withStates() .initial(orderState) .states(EnumSet.allOf(OrderStates.class)); builder.configureTransitions() .withExternal() .source(OrderStates.UNPAID).target(OrderStates.WAITING_FOR_RECEIVE) .event(OrderEvents.PAY) .and() .withExternal() .source(OrderStates.WAITING_FOR_RECEIVE).target(OrderStates.DONE) .event(OrderEvents.RECEIVE); return builder.build(); } }

在业务代码中使用状态机

复制代码
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
@RestController @RequestMapping("/statemachine") public class StateMachineController { @Resource private OrderStateMachineBuilder orderStateMachineBuilder; @Resource private BeanFactory beanFactory; @GetMapping("/testOrderState") public void testOrderState(String orderId) throws Exception { StateMachine<OrderStates, OrderEvents> stateMachine = orderStateMachineBuilder.build(beanFactory, OrderStates.UNPAID); System.out.println(stateMachine.getId()); //用message传递数据 Order order = new Order(orderId, "547568678", "广东省深圳市", "13435465465", "RECEIVE"); // 创建流程 stateMachine.start(); // 触发PAY事件 Message<OrderEvents> message = MessageBuilder.withPayload(OrderEvents.PAY).setHeader("order", order).setHeader("otherObj", "otherObjValue").build(); stateMachine.sendEvent(message); // 触发RECEIVE事件 Message<OrderEvents> message2 = MessageBuilder.withPayload(OrderEvents.RECEIVE).setHeader("order", order).setHeader("otherObj", "otherObjValue").build(); // stateMachine.sendEvent(OrderEvents.RECEIVE); stateMachine.sendEvent(message2); // 获取最终状态 System.out.println("最终状态:" + stateMachine.getState().getId()); } }

注册事件的业务代码

复制代码
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
@WithStateMachine(id="orderMachine") public class OrderEventConfig { private Logger logger = LoggerFactory.getLogger(getClass()); /** * 当前状态UNPAID */ @OnTransition(target = "UNPAID") public void create() { logger.info("---订单创建,待支付---"); } /** * UNPAID->WAITING_FOR_RECEIVE 执行的动作 */ @OnTransition(source = "UNPAID", target = "WAITING_FOR_RECEIVE") public void pay(Message<OrderEvents> message) { System.out.println("传递的参数:" + message.getHeaders().get("order")); logger.info("---用户完成支付,待收货---"); } /** * WAITING_FOR_RECEIVE->DONE 执行的动作 */ @OnTransition(source = "WAITING_FOR_RECEIVE", target = "DONE") public void receive(Message<OrderEvents> message) { System.out.println("传递的参数:" + message.getHeaders().get("order")); System.out.println("传递的参数:" + message.getHeaders().get("otherObj")); logger.info("---用户已收货,订单完成---"); } }

这里展示了状态机的基本用法。但是在实际应用中,状态机的状态不是一口气走完的,这就意味着状态需要持久化。关于状态机的持久化,有两个方向的方案,一个是持久化状态,这个状态在业务系统中,本身也用得到。在每次需要调度状态的时候,按照持久化的状态,构建新的状态机。另外一个是持久化状态机,直接把状态机持久化,下次用的时候,直接取出状态机调度,而不必重新构建状态机。
持久化状态和持久化状态机没有必然的哪个比较好,看业务的使用场景来决定选择哪种持久化方式。

  • 通过实现StateMachinePersist接口,持久化状态机
    这里使用的是持久化到mysql数据库中,同理,也可以持久化到redis,mangoDB等。
复制代码
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
/** * 在mysql中持久化状态机 */ @Component public class InMysqlStateMachinePersist implements StateMachinePersist<OrderStates, OrderEvents, Order> { @Resource private OrderStatemachineDao orderStatemachineDao; @Override public void write(StateMachineContext<OrderStates, OrderEvents> context, Order order) { byte[] contextStream = StateMachineSerializeDeserializeUtils.serialize(context); OrderStatemachine record = new OrderStatemachine(); record.setBusId(Integer.valueOf(order.getId())); record.setGmtCreate(new Date()); record.setGmtModified(new Date()); record.setCreateBy(order.getUserId()); record.setMachineId(context.getId()); record.setMachineState(contextStream); OrderStatemachine orderStatemachine = orderStatemachineDao.selectBySelective(Integer.valueOf(order.getId()), record.getMachineId()); if (Objects.isNull(orderStatemachine)) { orderStatemachineDao.insert(record); } else { record.setId(orderStatemachine.getId()); orderStatemachineDao.updateByPrimaryKey(record); } } @Override public StateMachineContext<OrderStates, OrderEvents> read(Order order) { StateMachineContext<OrderStates, OrderEvents> context = null; OrderStatemachine orderStatemachine = orderStatemachineDao.selectBySelective(Integer.valueOf(order.getId()), OrderStateMachineBuilder.MACHINEID); if (Objects.nonNull(orderStatemachine)) { context = StateMachineSerializeDeserializeUtils.deserialize(orderStatemachine.getMachineState()); } return context; } }

在业务代码中调用持久化的写入和读取。

复制代码
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
@RestController @RequestMapping("/statemachine") public class StateMachineController { @Resource private OrderStateMachineBuilder orderStateMachineBuilder; @Resource(name = "orderMysqlPersister") private StateMachinePersister<OrderStates, OrderEvents, Order> persister; @Resource private BeanFactory beanFactory; @RequestMapping("/testOrderPersister") public void testOrderPersister(Integer id) throws Exception { // 创建状态机 StateMachine<OrderStates, OrderEvents> stateMachine = orderStateMachineBuilder.build(beanFactory,OrderStates.UNPAID); stateMachine.start(); Order order = new Order(); order.setId(String.valueOf(id)); //发送PAY事件 Message<OrderEvents> message = MessageBuilder.withPayload(OrderEvents.PAY).setHeader("order", order).build(); stateMachine.sendEvent(message); //持久化stateMachine persister.persist(stateMachine, order); } @RequestMapping("/testOrderRestore") public void testOrderRestore(Integer id) throws Exception { // 创建状态机 StateMachine<OrderStates, OrderEvents> stateMachine = orderStateMachineBuilder.build(beanFactory,OrderStates.UNPAID); //订单 Order order = new Order(); order.setId(String.valueOf(id)); persister.restore(stateMachine, order); //查看恢复后状态机的状态 System.out.println("恢复后的状态:" + stateMachine.getState().getId()); } }
  • 持久化状态
    持久化状态,则需要将状态值写入到枚举值当中。如下所示:
复制代码
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
public enum OrderStatus { UNPAID(0, "待支付"), //待支付 WAITING_FOR_RECEIVE(1, "待收货"), // 待收货 DONE(2, "结束"); // 结束 private Integer code; private String desc; OrderStatus(int code, String desc) { this.code = code; this.desc = desc; } public static String getDescByCode(Integer code) { if (code == null) { return StaticConstants.EMPTY_STR; } for (OrderStatus codeEnum : values()) { if (codeEnum.code.equals(code)) { return codeEnum.desc; } } return StaticConstants.EMPTY_STR; } public static OrderStatus getStateByCode(Integer code) { if (code == null) { return null; } for (OrderStatus codeEnum : values()) { if (codeEnum.code.equals(code)) { return codeEnum; } } return null; } public Integer getCode() { return code; } public String getDesc() { return desc; } }

然后在业务代码中实时生成状态机。

*此处省略了dao层代码。

复制代码
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
@RestController @RequestMapping("/statemachine") public class StateMachineController { @Resource private OrderStateMachineBuilder orderStateMachineBuilder; @Resource private BeanFactory beanFactory; @Resource private OrderStateMachineDao orderStateMachineDao; @GetMapping("/testOrderState") public void testOrderState(String orderId) throws Exception { Order order = new Order(orderId, "547568678", "广东省深圳市", "13435465465", "RECEIVE"); Integer stateCode = orderStateMachineDao.getStateByOrderId(orderId); OrderStates states = OrderStatus.getStateByCode(stateCode); // 创建状态机,并设置当前状态 StateMachine<OrderStates, OrderEvents> stateMachine = orderStateMachineBuilder.build(beanFactory, states); System.out.println(stateMachine.getId()); //用message传递数据 // 创建流程 stateMachine.start(); // 触发PAY事件 Message<OrderEvents> message = MessageBuilder.withPayload(OrderEvents.PAY).setHeader("order", order).setHeader("otherObj", "otherObjValue").build(); stateMachine.sendEvent(message); // 触发RECEIVE事件 Message<OrderEvents> message2 = MessageBuilder.withPayload(OrderEvents.RECEIVE).setHeader("order", order).setHeader("otherObj", "otherObjValue").build(); // stateMachine.sendEvent(OrderEvents.RECEIVE); stateMachine.sendEvent(message2); // 获取最终状态 System.out.println("最终状态:" + stateMachine.getState().getId()); // 持久化结果状态 Integer retStates = stateMachine.getState().getId().getCode(); orderStateMachineDao.updateStateByOrderId(orderId,retStates); } }

状态机的实践应用

状态机已经设计并且实现好了,那么怎么在项目中实践呢?以上实例中,包含了业务代码的填写。然而,对于已经上线的系统,没办法对所有的请求去做改造,对于这种情况,可以使用Spring切面,来实现状态机的嵌入。这里给出一个简单示例。

首先声明一个注解StateMachineAnn

复制代码
1
2
3
4
5
6
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface StateMachineAnn { }

然后创建一个aop切面StateMachineAop

复制代码
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
@Component @Aspect public class StateMachineAop { @Resource private OrderStateMachineBuilder orderStateMachineBuilder; @Resource private BeanFactory beanFactory; @Resource private OrderStateMachineDao orderStateMachineDao; private static final Logger LOGGER = LoggerFactory.getLogger(StateMachineAop.class); /** * 创建切面 * 扫描的包地址,根据自己的包名做相应的变更 */ @Pointcut("execution(* com.test.domain.order.service.*.*.*(..))") private void stateMachinePointcut() { // Do nothing } @Around("stateMachinePointcut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature ms = (MethodSignature) joinPoint.getSignature(); Method targetMethod = ms.getMethod(); LOGGER.info("state machine target method : {}", targetMethod.getName()); // 如果方法上面没有@StateMachineAnn注解,则直接执行方法 if (!targetMethod.isAnnotationPresent(StateMachineAnn.class)) { // 直接执行方法 return joinPoint.proceed(); } Object[] args = joinPoint.getArgs(); OrderEvents event = null; String orderId = null; if (args == null || args.length < 1) { String msg = "state machine params error!"; LOGGER.error(msg); throw new HermesRuntimeException(msg); } for(Object arg : args){ if (arg instanceof OrderEvents) { event = (OrderEvents)arg; }else if ( arg instanceof Order){ orderId = ((Order) arg).getId(); } } LOGGER.info("state machine method params: workOrderId-{},event-{}",orderId,event); if(event == null || orderId == null){ String msg = "state machine params error!"; LOGGER.error(msg); throw new HermesRuntimeException(msg); } try { // 查询当前状态 Integer stateCode = orderStateMachineDao.getStateByOrderId(orderId);; OrderStates state = OrderStates.getStateByCode(stateCode); if(state == null){ String msg = "state machine current state error!"; LOGGER.error(msg); throw new HermesRuntimeException(msg); } LOGGER.info("state machine current state : {}",state.getDesc()); // 生成状态机 StateMachine<OrderStates, OrderEvents> stateMachine = orderStateMachineBuilder.build(beanFactory, state); // 执行事件 // 创建流程 stateMachine.start(); // 触发事件 Message<OrderEvents> message = MessageBuilder.withPayload(event).setHeader("orderId", orderId).build(); stateMachine.sendEvent(message); // 查询状态结果 OrderStates retState = stateMachine.getState().getId(); stateMachine.stop(); // 如果状态变更成功,则执行目标方法 if(retState != null && !retState.getCode().equals(stateCode)){ LOGGER.info("state machine result state : {}",retState.getDesc()); // 查询结果状态吗 Integer retStateCode = retState.getCode(); // 执行目标方法 Object retObj = joinPoint.proceed(); // 更新状态 orderStateMachineDao.updateStateByOrderId(orderId,retStateCode); return retObj; }else { // 否则报错,并终止方法。 String msg = "state machine result state error!"; LOGGER.error(msg); throw new HermesRuntimeException(msg); } } catch (Throwable throwable) { // 报错,并终止方法。 String msg = "state machine process error!"; LOGGER.error(msg,throwable); throw new HermesRuntimeException(msg); } } }

然后在业务service层中,相应的方法上面添加StateMachineAnn注解

复制代码
1
2
3
4
5
6
7
8
9
10
11
@Service public class InstallMachineAopTest { @StateMachineAnn public void testInstallMachine(InstallOrderEvents event, WorkOrderIdVo paramVo){ // Do business statement. System.out.println("state change ."); } }

通过切面的方式,就可以避免修改以前的业务代码,并且引入状态机。需要修改的就是将原来维护状态的代码去掉即可。

以上就是对于状态机使用实践的一个示例。也是我在项目中使用状态机的一点心得。

最后

以上就是暴躁黑米最近收集整理的关于线上系统引入Spring状态机实践什么是状态机为什么要使用状态机如何引入状态机状态机的实践应用的全部内容,更多相关线上系统引入Spring状态机实践什么是状态机为什么要使用状态机如何引入状态机状态机内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(68)

评论列表共有 0 条评论

立即
投稿
返回
顶部