1.传递参数的message
在企业开发中,数据在不同的业务间传输是最常见的工作,所以虽然我们的主架构是用的状态机,也就是从流程状态的角度来看待这个项目,但在具体业务中,每个状态的转变中会牵涉到各类业务,这些业务有些需要收到状态机变化的通知,需要把状态值传递给业务类和业务方法,同样的,在处理状态变化是,也需要获取业务数据,方便不同的业务在同一个状态变化环节做各自的业务,下面我们就讲下这个数据在spring statemachine里面的传递。
这次我们的顺序变一下,由外部传入一个订单号到controller开始:
@RequestMapping("/testOrderState")
public void testOrderState(String orderId) throws Exception {
StateMachine stateMachine = orderStateMachineBuilder.build(beanFactory);
System.out.println(stateMachine.getId());
// 创建流程
stateMachine.start();
// 触发PAY事件
stateMachine.sendEvent(OrderEvents.PAY);
// 触发RECEIVE事件
Order order = new Order(orderId, "547568678", "广东省深圳市", "13435465465", "RECEIVE");
Message message = MessageBuilder.withPayload(OrderEvents.RECEIVE).setHeader("order", order).build();
stateMachine.sendEvent(message);
// 获取最终状态
System.out.println("最终状态:" + stateMachine.getState().getId());
}
controller收到request请求的参数,orderId,然后状态机依次触发事件,到触发RECEIVE事件的时候,我们新建了一个Order,并把orderId塞进去了,其实更多的情况应该是我们拿到orderId,然后查询数据库,得到order数据对象,这里为了简化代码,就新建一个啦。
然后就是真正的主角登场了,Message。它其实不是spirng statemachine专属的,它是spring里面通用的一种消息工具,看它的源代码:
package org.springframework.messaging;
public interface Message {
/**
* Return the message payload.
*/
T getPayload();
/**
* Return message headers for the message (never {@code null} but may be empty).
*/
MessageHeaders getHeaders();
}
它由两个部分组成,看图就知道了,和代码里面是一致的
在spring statemachine里面,我们把状态塞到message的payload里面,然后把需要传递的业务数据(例子里面就是order对象)塞到header里面。创建message用的是messagebuilder,看它的名字就知道是专门创建message的。
Message message = MessageBuilder.withPayload(OrderEvents.RECEIVE).setHeader("order", order).build();
stateMachine.sendEvent(message);
创建了message后,状态机sendEvent就可以不只是传一个event,可以组合event(OrderEvents.RECEIVE)和数据内容(order)一起发送给状态机变化的处理类eventconfig了。让我们看eventConfig的处理:
/**
* WAITING_FOR_RECEIVE->DONE 执行的动作
*/
@OnTransition(source = "WAITING_FOR_RECEIVE", target = "DONE")
public void receive(Message message) {
System.out.println("传递的参数:" + message.getHeaders().get("order"));
logger.info("---用户已收货,订单完成---");
}
首先,receive方法的参数由之前的为空:
public void receive() {
logger.info("---用户已收货,订单完成---");
}
改成了Message
另外如果我们需要传递多个数据对象怎么办呢,比如我们在实际业务中,除了传订单数据,可能还需要把商品数据,或者支付结果数据也传过来,那么也容易,我们还是从controller里面开始:
Message message = MessageBuilder.withPayload(OrderEvents.RECEIVE).setHeader("order", order).setHeader("otherobj", "otherobjvalue").build();
在后面继续setHeader就好了,然后到eventConfig里面:
System.out.println("传递的参数:" + message.getHeaders().get("order"));
System.out.println("传递的参数:" + message.getHeaders().get("otherObj"));
运行后看日志:
传递的参数:Order [id=null, userId=547568678, address=广东省深圳市, phoneNum=13435465465, state=RECEIVE]
传递的参数:otherObjValue
可知两个的数据都传递到了eventConfig里面了,这个就实现了多个数据对象的同时传递。
到这里为止,状态机通过message对象就和其他的业务代码做到了数据连接。其实这个很关键,只有做到和其他业务的数据传递,才能算的上真正的可用。
2.持久化
目前为止,我们都是从状态流程的开始阶段创建一个状态机,然后一路走下去。但在实际业务中,状态机可能需要在某个环节停留,等待其他业务的触发,然后再继续下面的流程。比如订单,可能在支付环节需要等待一个剁手的用户隔天再下单,所以这里面涉及到一个创建的状态机该何去何从的问题。在spring statemachine中,给出来的办法就是保存起来,到需要的时候取出来用。
1、持久化到本地内存
import java.util.HashMap;
import java.util.Map;
import org.springframework.statemachine.StateMachineContext;
import org.springframework.statemachine.StateMachinePersist;
import org.springframework.stereotype.Component;
/**
* 在内存中持久化状态机
*/
@Component
public class InMemoryStateMachinePersist implements StateMachinePersist {
private Map> map = new HashMap>();
@Override
public void write(StateMachineContext context, String contextObj) throws Exception {
map.put(contextObj, context);
}
@Override
public StateMachineContext read(String contextObj) throws Exception {
return map.get(contextObj);
}
}
这个接口非常简单,就是write和read,但他保存的对象是StateMachineContext,不是StateMachine,所以我们还不能直接用它,需要配置一下。
import org.springframework.statemachine.StateMachinePersist;
......
import org.springframework.statemachine.persist.StateMachinePersister;
@Configuration
public class PersistConfig {
@Autowired
private InMemoryStateMachinePersist inMemoryStateMachinePersist;
/**
* 注入StateMachinePersister对象
*
* @return
*/
@Bean(name="orderMemoryPersister")
public StateMachinePersister getPersister() {
return new DefaultStateMachinePersister<>(inMemoryStateMachinePersist);
}
}
这里有个坑,InMemoryStateMachinePersist 实现的接口是
org.springframework.statemachine.StateMachinePersist
,但在PersistConfig 里面,getPersister()方法返回的值类型是StateMachinePersister类型,看着很像,但并不是上面的这个接口,而是org.springframework.statemachine.persist.StateMachinePersister
接口,为了表示这个坑对我的伤害,我要强调一下两个接口:
package org.springframework.statemachine;
public interface StateMachinePersist {
void write(StateMachineContext context, T contextObj) throws Exception;
StateMachineContext read(T contextObj) throws Exception;
}
package org.springframework.statemachine.persist;
import org.springframework.statemachine.StateMachine;
public interface StateMachinePersister {
void persist(StateMachine stateMachine, T contextObj) throws Exception;
StateMachine restore(StateMachine stateMachine, T contextObj) throws Exception;
}
这两个接口名字很类似,很容易搞混,但下面的是有er的,包名也不同的。StateMachinePersister是可以直接保存StateMachine对象的,所以我们需要先实现上面的StateMachinePersist,然后再一个Config类里面转换成下面的StateMachinePersister,转换的代码就在上面的PersistConfig类里。
然后我们就能在controller里面使用了
@Resource(name="orderMemoryPersister")
private StateMachinePersister orderMemorypersister;
......
//保存状态机
@RequestMapping("/testMemoryPersister")
public void tesMemorytPersister(String id) throws Exception {
StateMachine stateMachine = orderStateMachineBuilder.build(beanFactory);
stateMachine.start();
//发送PAY事件
stateMachine.sendEvent(OrderEvents.PAY);
Order order = new Order();
order.setId(id);
//持久化stateMachine
orderMemorypersister.persist(stateMachine, order.getId());
}
//取出状态机
@RequestMapping("/testMemoryPersisterRestore")
public void testMemoryRestore(String id) throws Exception {
StateMachine stateMachine = orderStateMachineBuilder.build(beanFactory);
orderMemorypersister.restore(stateMachine, id);
System.out.println("恢复状态机后的状态为:" + stateMachine.getState().getId());
}
2、持久化到redis
真正的业务中,一般都是多台机分布式运行,所以如果状态机只能保存在本地内容,就不能用在分布式应用上了。spring提供了一个方便的办法,使用redis解决这个问题。让我们看看怎么弄。
pom文件引入spring-statemachine-redis
org.springframework.statemachine
spring-statemachine-redis
1.2.9.RELEASE
在springboot配置文件里面加上redis参数,我这是application.properties
# REDIS (RedisProperties)
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=localhost
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=0
保证配置的redis开启并能用,我们继续。回到我们熟悉的PersistConfig
@Configuration
public class PersistConfig {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
/**
* 注入RedisStateMachinePersister对象
*
* @return
*/
@Bean(name = "orderRedisPersister")
public RedisStateMachinePersister redisPersister() {
return new RedisStateMachinePersister<>(redisPersist());
}
/**
* 通过redisConnectionFactory创建StateMachinePersist
*
* @return
*/
public StateMachinePersist redisPersist() {
RedisStateMachineContextRepository repository = new RedisStateMachineContextRepository<>(redisConnectionFactory);
return new RepositoryStateMachinePersist<>(repository);
}
}
这个套路和上面保存到本地内存是一样一样的,先生成一个StateMachinePersist,这里是通过RedisConnectionFactory生成RepositoryStateMachinePersist,然后再包装输出StateMachinePersister,这里是RedisStateMachinePersister。然后就可以愉快的在controller里面看怎么用了
@Resource(name="orderRedisPersister")
private StateMachinePersister orderRedisPersister;
......
@RequestMapping("/testRedisPersister")
public void testRedisPersister(String id) throws Exception {
StateMachine stateMachine = orderStateMachineBuilder.build(beanFactory);
stateMachine.start();
Order order = new Order();
order.setId(id);
//发送PAY事件
Message message = MessageBuilder.withPayload(OrderEvents.PAY).setHeader("order", order).build();
stateMachine.sendEvent(message);
//持久化stateMachine
orderRedisPersister.persist(stateMachine, order.getId());
}
@RequestMapping("/testRedisPersisterRestore")
public void testRestore(String id) throws Exception {
StateMachine stateMachine = orderStateMachineBuilder.build(beanFactory);
orderRedisPersister.restore(stateMachine, id);
System.out.println("恢复状态机后的状态为:" + stateMachine.getState().getId());
}
执行完redis保存statemachine后,大家可以自己在redis客户端查看以下,是不是有内容保存进去了。
3.伪持久化和中间段的状态机
我们设想一个业务场景,就比如订单吧,我们一般的设计都会把订单状态存到订单表里面,其他的业务信息也都有表保存,而状态机的主要作用其实是规范整个订单业务流程的状态和事件,所以状态机要不要保存真的不重要,我们只需要从订单表里面把状态取出来,知道当前是什么状态,然后伴随着业务继续流浪到下一个状态节点就好了。我们先实现一个StateMachinePersist,因为我不想真的持久化,所以就敷衍一下,持久化是什么,啥也不干。
import org.springframework.statemachine.StateMachineContext;
import org.springframework.statemachine.StateMachinePersist;
import org.springframework.statemachine.support.DefaultStateMachineContext;
import org.springframework.stereotype.Component;
@Component
public class OrderStateMachinePersist implements StateMachinePersist {
@Override
public void write(StateMachineContext context, Order contextObj) throws Exception {
//这里不做任何持久化工作
}
@Override
public StateMachineContext read(Order contextObj) throws Exception {
StateMachineContext result = new DefaultStateMachineContext(OrderStates.valueOf(contextObj.getState()),
null, null, null, null, "orderMachine");
return result;
}
}
然后在PersistConfig里面转换成StateMachinePersister
@Configuration
public class PersistConfig {
@Autowired
private OrderStateMachinePersist orderStateMachinePersist;
@Bean(name="orderPersister")
public StateMachinePersister orderPersister() {
return new DefaultStateMachinePersister(orderStateMachinePersist);
}
}
现在问题来了,不持久化的持久化类是为啥呢,主要就是为了取一个任何状态节点的状态机,方便继续往下执行,请看controller
@RestController
@RequestMapping("/statemachine")
public class StateMachineController {
@Resource(name="orderPersister")
private StateMachinePersister persister;
@RequestMapping("/testOrderRestore")
public void testOrderRestore(String id) throws Exception {
StateMachine stateMachine = orderStateMachineBuilder.build(beanFactory);
//订单
Order order = new Order();
order.setId(id);
order.setState(OrderStates.WAITING_FOR_RECEIVE.toString());
//恢复
persister.restore(stateMachine, order);
//查看恢复后状态机的状态
System.out.println("恢复后的状态:" + stateMachine.getState().getId());
}
}
看到没有,用builder建了一个新的状态机,用restore过了一手,就已经是一个到达order指定状态的老司机状态机了,在这里,持久化不是本意,让状态机能够随时抓换到任意状态节点才是目的。在实际的企业开发中,不可能所有情况都是从头到尾的按状态流程来,会有很多意外,比如历史数据,故障重启后的遗留流程......,所以这种可以任意调节状态的才是我们需要的状态机。