spring statemachine-传递参数的message与持久化

1.传递参数的message

   在企业开发中,数据在不同的业务间传输是最常见的工作,所以虽然我们的主架构是用的状态机,也就是从流程状态的角度来看待这个项目,但在具体业务中,每个状态的转变中会牵涉到各类业务,这些业务有些需要收到状态机变化的通知,需要把状态值传递给业务类和业务方法,同样的,在处理状态变化是,也需要获取业务数据,方便不同的业务在同一个状态变化环节做各自的业务,下面我们就讲下这个数据在spring statemachine里面的传递。

   这次我们的顺序变一下,由外部传入一个订单号到controller开始:

@RequestMapping("/testOrderState")
    public void testOrderState(String orderId) throws Exception {

        StateMachine<OrderStates, OrderEvents> 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<OrderEvents> 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<T> {

    /**
     * Return the message payload.
     */
    T getPayload();

    /**
     * Return message headers for the message (never {@code null} but may be empty).
     */
    MessageHeaders getHeaders();

}

它由两个部分组成,看图就知道了,和代码里面是一致的

image

   在spring statemachine里面,我们把状态塞到message的payload里面,然后把需要传递的业务数据(例子里面就是order对象)塞到header里面。创建message用的是messagebuilder,看它的名字就知道是专门创建message的。

Message<OrderEvents> 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<OrderEvents> message) {
        System.out.println("传递的参数:" + message.getHeaders().get("order"));
        logger.info("---用户已收货,订单完成---");
    }

首先,receive方法的参数由之前的为空:

public void receive() {
        logger.info("---用户已收货,订单完成---");
}

   改成了Message<OrderEvents> message,这样就能从message的getHeaders里面取到传递过来的数据对象了。

   另外如果我们需要传递多个数据对象怎么办呢,比如我们在实际业务中,除了传订单数据,可能还需要把商品数据,或者支付结果数据也传过来,那么也容易,我们还是从controller里面开始:

Message<OrderEvents> 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<OrderStates, OrderEvents, String> {

    private Map<String, StateMachineContext<OrderStates, OrderEvents>> map = new HashMap<String, StateMachineContext<OrderStates,OrderEvents>>();
    
    @Override
    public void write(StateMachineContext<OrderStates, OrderEvents> context, String contextObj) throws Exception {
        map.put(contextObj, context);
    }

    @Override
    public StateMachineContext<OrderStates, OrderEvents> 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<OrderStates, OrderEvents, String> 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<S, E, T> {
    void write(StateMachineContext<S, E> context, T contextObj) throws Exception;
    StateMachineContext<S, E> read(T contextObj) throws Exception;
}
package org.springframework.statemachine.persist;
import org.springframework.statemachine.StateMachine;
public interface StateMachinePersister<S, E, T> {
    void persist(StateMachine<S, E> stateMachine, T contextObj) throws Exception;
    StateMachine<S, E> restore(StateMachine<S, E> stateMachine, T contextObj) throws Exception;
}

这两个接口名字很类似,很容易搞混,但下面的是有er的,包名也不同的。StateMachinePersister是可以直接保存StateMachine对象的,所以我们需要先实现上面的StateMachinePersist,然后再一个Config类里面转换成下面的StateMachinePersister,转换的代码就在上面的PersistConfig类里。

然后我们就能在controller里面使用了

@Resource(name="orderMemoryPersister")
private StateMachinePersister<OrderStates, OrderEvents, String> orderMemorypersister;

......

//保存状态机
@RequestMapping("/testMemoryPersister")
    public void tesMemorytPersister(String id) throws Exception {
        StateMachine<OrderStates, OrderEvents> 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<OrderStates, OrderEvents> stateMachine = orderStateMachineBuilder.build(beanFactory);
        orderMemorypersister.restore(stateMachine, id);
        System.out.println("恢复状态机后的状态为:" + stateMachine.getState().getId());
    }

2、持久化到redis

  真正的业务中,一般都是多台机分布式运行,所以如果状态机只能保存在本地内容,就不能用在分布式应用上了。spring提供了一个方便的办法,使用redis解决这个问题。让我们看看怎么弄。
   pom文件引入spring-statemachine-redis

<!-- redis持久化状态机 -->
        <dependency>
            <groupId>org.springframework.statemachine</groupId>
            <artifactId>spring-statemachine-redis</artifactId>
            <version>1.2.9.RELEASE</version>
        </dependency>

在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<OrderStates, OrderEvents> redisPersister() {
        return new RedisStateMachinePersister<>(redisPersist());
    }

    /**
     * 通过redisConnectionFactory创建StateMachinePersist
     * 
     * @return
     */
    public StateMachinePersist<OrderStates, OrderEvents,String> redisPersist() {
        RedisStateMachineContextRepository<OrderStates, OrderEvents> repository = new RedisStateMachineContextRepository<>(redisConnectionFactory);
        return new RepositoryStateMachinePersist<>(repository);
    }
    
}

这个套路和上面保存到本地内存是一样一样的,先生成一个StateMachinePersist,这里是通过RedisConnectionFactory生成RepositoryStateMachinePersist,然后再包装输出StateMachinePersister,这里是RedisStateMachinePersister。然后就可以愉快的在controller里面看怎么用了

@Resource(name="orderRedisPersister")
private StateMachinePersister<OrderStates, OrderEvents, String> orderRedisPersister;

......

@RequestMapping("/testRedisPersister")
    public void testRedisPersister(String id) throws Exception {
        StateMachine<OrderStates, OrderEvents> stateMachine = orderStateMachineBuilder.build(beanFactory);
        stateMachine.start();
        Order order = new Order();
        order.setId(id);
        //发送PAY事件
        Message<OrderEvents> 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<OrderStates, OrderEvents> 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<OrderStates, OrderEvents, Order> {

    @Override
    public void write(StateMachineContext<OrderStates, OrderEvents> context, Order contextObj) throws Exception {
        //这里不做任何持久化工作
    }

    @Override
    public StateMachineContext<OrderStates, OrderEvents> read(Order contextObj) throws Exception {
        StateMachineContext<OrderStates, OrderEvents> result = new DefaultStateMachineContext<OrderStates, OrderEvents>(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<OrderStates, OrderEvents, Order> orderPersister() {
        return new DefaultStateMachinePersister<OrderStates, OrderEvents, Order>(orderStateMachinePersist);
    }
}

现在问题来了,不持久化的持久化类是为啥呢,主要就是为了取一个任何状态节点的状态机,方便继续往下执行,请看controller

@RestController
@RequestMapping("/statemachine")
public class StateMachineController {

    @Resource(name="orderPersister")
    private StateMachinePersister<OrderStates, OrderEvents, Order> persister;
    
    @RequestMapping("/testOrderRestore")
    public void testOrderRestore(String id) throws Exception {
        StateMachine<OrderStates, OrderEvents> 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指定状态的老司机状态机了,在这里,持久化不是本意,让状态机能够随时抓换到任意状态节点才是目的。在实际的企业开发中,不可能所有情况都是从头到尾的按状态流程来,会有很多意外,比如历史数据,故障重启后的遗留流程......,所以这种可以任意调节状态的才是我们需要的状态机。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,258评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,335评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,225评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,126评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,140评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,098评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,018评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,857评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,298评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,518评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,400评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,993评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,638评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,661评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352