1,前言
Workflow(https://en.wikipedia.org/wiki/Workflow)是一个极其常见的业务场景,基本所有行业都能涉及到流程管理上的问题。工作流,个人认为可以等价的理解为状态流(state flow),因为工作流的主要工作就是流程管理或者就是状态转移。如果用状态转移来抽象描述问题的话,基本大多数业务系统都可以状态转移来描述,且不说OA、ERP等软件,在常见交易系统软件里产品管理的流程、在线交易系统里订单的各个状态流程等。
使用状态转移来描述问题的优势是语义简洁、易于图像化描述(计算机专业所熟知的计算理论中的状态机)。日前已有像Spring webflow、jBPM等开源workflow实现,但本文对这些不相关,本文主要是围绕使用Spring Statemachine(http://projects.spring.io/spring-statemachine/),以一个具体应用实例来设计和实现一个简单基本但易拓展、易修改的状态流程。
2,问题描述
在一些网络上的金融产品平台上,我们可以看到各种类型的定期理财产品。这些理财产品实际都是金融公司依据国家的一些监管要求,打包成的一系列金融资产。所以这些资产在平台上正式对外公开募集资金前,实际是需要是需要经过一系列评审流程的。例如假设有一笔资产A,对这笔资产首先需要发起一场沟通会议,然后风控经理做好第一道最重要的风控问题、再是产品经理审批,最后进行评审、表决是否通过、拒绝或待议。
既然是状态转移问题,忽略异常状态和中间状态终止或转移流程,我们通过语义来描述上述问题的正常流程:
1) 状态定义: Meeting("项目沟通会"), RiskManager("风控经理"), ProdManager("产品经理"), Review(“评审"), Pass("通过"), Reject("否决"), Discussing("待议");
2) 状态转移事件:
{from=Initial, to=Meeting, on=LAUNCH_MEETING("发起项目沟通会")},
{from=Meeting, to=RiskManager, on=PASS_MEETING("通过项目沟通会")},
{from=RiskManager, to=ProdManager, on=PASS_RISK_MANAGER("风控经理审核通过")},
{from=ProdManager, to=Review, on=PASS_PROD_MANAGER("产品经理审核通过")},
{from=Review, to=Termination, on=REVIEW_PASS("通过")},
{from=Review, to=Termination, on=REVIEW_REJECT("否决")},
{from=Review, to=Terminaton, on=REVIEW_DISCUSSING("待议")};
图1
问题如果使用状态机描述的话,则如图1所示。
3,领域建模
图2 领域抽象
从图2中可以看出,资产评审的领域核心Entity即是Review。State和Feature是刻画Review状态转移的基本组件,如第2部分语义所描述的。状态转移的发生都是通过FeatureEvent来触发,从FeatureEvent我们可以看到每一个Feature都是对应一个单例的触发Event,每一个FeatureEvent都标明了它将使Review从具体一个状态跳转到另一个状态。Review的构成除了基本user和asset数据外,featureCode和state记录了该Review的状态转移语义。state表明该Review当前所处的状态,featureCode标识了对应的触发状态转移的FeatureEvent。特别的,所有的流程都应当有一个初始的状态,这就是Review#initial()需要完成的工作。
4,编码实现
4.1, 领域核心实现
Review.java
1 import org.springframework.beans.factory.support.StaticListableBeanFactory; 2 import org.springframework.core.task.SyncTaskExecutor; 3 import org.springframework.messaging.Message; 4 import org.springframework.statemachine.StateMachine; 5 import org.springframework.statemachine.config.StateMachineBuilder; 6 import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; 7 import org.springframework.statemachine.listener.StateMachineListenerAdapter; 8 import org.springframework.statemachine.transition.Transition; 9 import org.springframework.util.Assert; 10 11 import java.util.*; 12 13 /** 14 * @author shenjixiaodao 15 */ 16 public class Review { 17 private Long reviewId; 18 private Users user; 19 private String featureCode; 20 private Assets asset; 21 private State state; 22 23 public enum State{ 24 Meeting("项目沟通会"), RiskManager("风控经理审批"), 25 ProdManager("产品经理审批"), Review("评审"), 26 Pass("评审通过"), Reject("否决"), Discussing("待议"); 27 private String desc; 28 State(String desc) { 29 this.desc = desc; 30 } 31 public String getDesc(){return this.desc;} 32 } 33 34 private StateMachine<State, FeatureEvent> machine = null; 35 /** 36 * 触发状态转移动作 37 * @return true:允许状态转移 38 */ 39 public boolean fire(){ 40 return this.fireEvent(FeatureEvent.codeOf(featureCode)); 41 } 42 private boolean fireEvent(FeatureEvent event){ 43 try { 44 if(machine == null) { 45 machine = buildSyncMachine(); 46 machine.addStateListener(new StateMachineListenerAdapter<State, FeatureEvent>() { 47 @Override 48 public void eventNotAccepted(Message<FeatureEvent> msg) { 49 FeatureEvent event = msg.getPayload(); 50 StringBuilder appender = new StringBuilder(); 51 appender.append("【").append(event.featureName()).append("】只能将资产从") 52 .append(event.reviewState()).append("修改为").append(event.nextState()); 53 throw new IllegalArgumentException(appender.toString()); 54 } 55 56 @Override 57 public void transitionEnded(Transition<State, FeatureEvent> transition) { 58 FeatureEvent event = transition.getTrigger().getEvent(); 59 state = event.nextState(); 60 } 61 }); 62 } 63 return machine.sendEvent(event); 64 } catch (Exception e) { 65 throw new RuntimeException(e); 66 } 67 } 68 69 public Review(Integer userId, Integer assetId, String featureCode) { 70 this.user = new Users(); 71 this.user.setId(userId); 72 this.asset = new Assets(); 73 this.asset.setId(assetId); 74 this.featureCode = featureCode; 75 } 76 77 public static Review initial(Integer assetId){ 78 Review review = new Review(); 79 review.asset = new Assets(); 80 review.asset.setId(assetId); 81 review.state = State.Publish; 82 return review; 83 } 84 85 public boolean isInitial(){ 86 return this.reviewId == null && this.state == State.Publish; 87 } 88 89 public void setReviewId(Long reviewId) { 90 this.reviewId = reviewId; 91 for(Attachment attachment:attachments) 92 attachment.setReviewId(reviewId); 93 } 94 95 public FeatureEvent featureEvent(){ 96 if(StringUtils.isEmpty(featureCode)) 97 return null; 98 return FeatureEvent.codeOf(featureCode); 99 } 100 public void featureCode(String featureCode){ 101 this.featureCode = featureCode; 102 } 103 104 public State state() { 105 return state; 106 } 107 108 public Review state(State state) { 109 this.state = state; 110 return this; 111 } 112 113 Review() { 114 //for ORM 115 } 116 /** 117 * 构建线程同步状态机 118 */ 119 private StateMachine<State, FeatureEvent> buildSyncMachine() throws Exception { 120 Assert.notNull(state,"状态不能为空"); 121 StateMachineBuilder.Builder<State, FeatureEvent> builder = StateMachineBuilder.builder(); 122 builder.configureConfiguration().withConfiguration() 123 .taskExecutor(new SyncTaskExecutor()) 124 .beanFactory(new StaticListableBeanFactory()) 125 .autoStartup(true); 126 //配置状态 127 builder.configureStates() 128 .withStates() 129 .initial(state) 130 .states(EnumSet.allOf(State.class)); 131 //配置状态转移 132 StateMachineTransitionConfigurer<State, FeatureEvent> transition = builder.configureTransitions(); 133 for(FeatureEvent event:FeatureEvent.values()) { 134 transition = transition.withExternal() 135 .source(event.reviewState()) 136 .target(event.nextState()) 137 .event(event) 138 .and(); 139 } 140 return builder.build(); 141 } 142 143 }
从Review的状态转移实现主要依赖 buildSyncMachine()方法,在 buildSyncMachine()方法里使用Spring Statemachine(的使用在本文不作描述)实现了第2部分描述的状态转移语义,定义状态和状态转移事件。fireEvent(FeatureEvent event)是触发review发生状态转移的动作,其中主要是实现对拒绝动作和状态正确转移地操作。
4.2,调用接口设计
ReviewServiceImpl.java
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; /** * @author shenjixiaodao */ @Service public class ReviewServiceImpl implements ReviewService { @Autowired private ReviewRepository repository; @Transactional public void review(Review review) { // FIXME: 2017/7/10 检查资产是否存在 Review lastReview = repository.findLastUpdated(review.asset().getId()); boolean accept = review.state(lastReview.state()).fire(); FeatureEvent event = review.featureEvent(); if(!accept) { StringBuilder appender = new StringBuilder(); appender.append("【").append(event.code()).append("】只能将资产从【") .append(event.reviewState().getDesc()).append("】修改为【") .append(event.nextState().getDesc()).append("】"); throw new IllegalArgumentException(appender.toString()); } if(!lastReview.isInitial()) { Assert.notNull(lastReview.reviewId(),"当前状态的评审ID为空"); //记录状态迁移信息 lastReview.featureCode(event.code()); repository.update(lastReview); } //进入新的状态 repository.save(review); } }
如上ReviewServiceImpl.java源码所示,定义了一个ReviewService#review(Review)接口来执行所有评审动作。从review(Review)的实现源码看,在触发状态转移之前,我需要从数据库中恢复Review当前所处的状态。最后如果状态迁移成功,则更新状态迁移记录,并进入新的评审状态。