用于Spring Boot项目的简单状态机


在状态机(Ref#1)上有很多文章和开源项目,可以在Google或GitHub上进行搜索。Spring团队本身提供了一个状态机框架(Ref#2)。但是,我发现这些框架不容易自定义。此外,在我需要的地方添加日志并引发自定义异常并不容易。因此,我创建了状态机的简单实现,可以轻松地将其集成到Spring Boot应用程序中。

状态机的想法是定义一组状态转换,其中每个转换都受事件影响。例如,维基百科举例说明了具有锁定和解锁状态的旋转门,该状态受事件“硬币”和“推送”的影响。当发生“硬币”事件时,旋转栅门的状态从“已锁定”转变为“未锁定”。当发生“推送”事件时,它会从“解锁”转换为“锁定”。状态机强制执行以下操作:当旋转闸门处于“锁定”状态时,“推送”事件无效。同样,当旋转门处于“解锁”状态时,“硬币”事件无效。

本文提出的状态机框架基于状态转换表表示,如下所示:

QQ图片20210416191952.png

拟议的框架包括以下核心组成部分:

  1. ProcessState —将各种状态配置为Java Enum类。

2.- ProcessEvent 将上述状态转换配置为Java Enum类。

  1. StateTransitionsManager —响应事件。如果事件是事件前,则将处理转发到处理器,或者如果事件是事件后,则将状态更改为最终状态。如果需要,它还会设置默认状态。

  2. Processor —执行相应流程状态转换步骤的业务规则。

下面列出了相应的Java组件:

//Enum implements this marker interface
public interface ProcessState {
}
//Enum implements this interface
public interface ProcessEvent {
    public abstract Class<? extends Processor> nextStepProcessor(ProcessEvent event);   
    public abstract ProcessState nextState(ProcessEvent event);
}
//events handler
public interface StateTransitionsManager {
    public ProcessData processEvent(ProcessData data) throws ProcessException;
}
//enforces initialization of the state where needed
public abstract class AbstractStateTransitionsManager implements StateTransitionsManager {
    protected abstract ProcessData initializeState(ProcessData data) throws ProcessException;
    protected abstract ProcessData processStateTransition(ProcessData data) throws ProcessException;

    @Override
    public ProcessData processEvent(ProcessData data) throws ProcessException {
    initializeState(data);
        return processStateTransition(data);
    }
}
//executes the business rules 
//needed for this state transition step
public interface Processor {
    public ProcessData process(ProcessData data) throws ProcessException;
}
//
public interface ProcessData {
    public ProcessEvent getEvent();
}
//
public class ProcessException extends Exception {
    private static final long serialVersionUID = 1L;

    public ProcessException(String message) {
        super(message);
    }

    public ProcessException(String message, Throwable e) {
        super(message, e);
    }
}

用于在线订单处理应用程序的上述状态机框架的示例实现涉及以下类:

​/**  
 * DEFAULT    -  submit -> orderProcessor()   -> orderCreated   -> PMTPENDING
 * PMTPENDING -  pay    -> paymentProcessor() -> paymentError   -> PMTPENDING
 * PMTPENDING -  pay    -> paymentProcessor() -> paymentSuccess -> COMPLETED 
 */
public enum OrderState implements ProcessState {
    Default,
    PaymentPending,    
    Completed;
}

/**  
 * DEFAULT    -  submit -> orderProcessor()   -> orderCreated   -> PMTPENDING
 * PMTPENDING -  pay    -> paymentProcessor() -> paymentError   -> PMTPENDING
 * PMTPENDING -  pay    -> paymentProcessor() -> paymentSuccess -> COMPLETED 
 */
public enum OrderEvent implements ProcessEvent {

    submit {
        @Override
        public Class<? extends Processor> nextStepProcessor(ProcessEvent event) {
                return OrderProcessor.class;
        }

        /**
         * This event has no effect on state so return current state
         */
        @Override
        public ProcessState nextState(ProcessEvent event) {
                return OrderState.Default;
        }

    },
    orderCreated {
    /**
     * This event does not trigger any process
     * So return null 
     */
        @Override
        public Class<? extends Processor> nextStepProcessor(ProcessEvent event) {
            return null;
        }

        @Override
        public ProcessState nextState(ProcessEvent event) {
                return OrderState.PaymentPending;
        }

    },
    pay {
        @Override
        public Class<? extends Processor> nextStepProcessor(ProcessEvent event) {
                return PaymentProcessor.class;
        }

        /**
         * This event has no effect on state so return current state
         */
        @Override
        public ProcessState nextState(ProcessEvent event) {
                return OrderState.PaymentPending;
        }
    },
    paymentSuccess {
    /**
     * This event does not trigger any process
     * So return null 
     */
        @Override
        public Class<? extends Processor> nextStepProcessor(ProcessEvent event) {
            return null;
        }
        @Override
        public ProcessState nextState(ProcessEvent event) {
                return OrderState.Completed;
        }
    },
    paymentError {
    /**
     * This event does not trigger any process
     * So return null 
     */
        @Override
        public Class<? extends Processor> nextStepProcessor(ProcessEvent event) {
            return null;
        }

        @Override
        public ProcessState nextState(ProcessEvent event) {
                return OrderState.PaymentPending;
        }
    };
}

/**
 * This class manages various state transitions 
 * based on the event
 * The superclass AbstractStateTransitionsManager
 * calls the two methods initializeState and 
 * processStateTransition in that order
 */
@RequiredArgsConstructor
@Slf4j
@Service
public class OrderStateTransitionsManager extends AbstractStateTransitionsManager {

    private final ApplicationContext context;
    private final OrderDbService dbService;

    @Override
    protected ProcessData processStateTransition(ProcessData sdata) throws ProcessException {

        OrderData data = (OrderData) sdata;

        try {
            log.info("Pre-event: " + data.getEvent().toString());
            data = (OrderData) this.context.getBean(data.getEvent().nextStepProcessor(data.getEvent())).process(data);
            log.info("Post-event: " + data.getEvent().toString());
            dbService.getStates().put(data.getOrderId(), (OrderState)data.getEvent().nextState(data.getEvent()));
            log.info("Final state: " + dbService.getStates().get(data.getOrderId()).name());
            log.info("??*************************************");

        } catch (OrderException e) {
            log.info("Post-event: " + ((OrderEvent) data.getEvent()).name());
            dbService.getStates().put(data.getOrderId(), (OrderState)data.getEvent().nextState(data.getEvent()));
            log.info("Final state: " + dbService.getStates().get(data.getOrderId()).name());
            log.info("??*************************************");
            throw new OrderException(((OrderEvent) data.getEvent()).name(), e);
        }
        return data;
    }

    private OrderData checkStateForReturningCustomers(OrderData data) throws OrderException {
        // returning customers must have a state
        if (data.getOrderId() != null) {
            if (this.dbService.getStates().get(data.getOrderId()) == null) {
                throw new OrderException("No state exists for orderId=" + data.getOrderId());
            } else if (this.dbService.getStates().get(data.getOrderId()) == OrderState.Completed) {
                throw new OrderException("Order is completed for orderId=" + data.getOrderId());
            } else {
                log.info("Initial state: " + dbService.getStates().get(data.getOrderId()).name());
            }
        }
        return data;
    }

    @Override
    protected ProcessData initializeState(ProcessData sdata) throws OrderException {

        OrderData data = (OrderData) sdata;

        if (data.getOrderId() != null) {
            return checkStateForReturningCustomers(data);
        }

        UUID orderId = UUID.randomUUID();
        data.setOrderId(orderId);
        dbService.getStates().put(orderId, (OrderState) OrderState.Default);

        log.info("Initial state: " + dbService.getStates().get(data.getOrderId()).name());
        return data;
    }

    public ConcurrentHashMap<UUID, OrderState> getStates() {
        return dbService.getStates();
    }
}

//persists state of the data
//here we are using HashMap for illustration purposes
@Service
public class OrderDbService {

    private final ConcurrentHashMap<UUID, OrderState> states;

    public OrderDbService() {
        this.states = new ConcurrentHashMap<UUID, OrderState>();
    }

    public ConcurrentHashMap<UUID, OrderState> getStates() {
        return states;
    }
}

@NoArgsConstructor
@AllArgsConstructor
@Setter @Getter
@Builder
public class OrderData implements ProcessData {
private double payment;
private ProcessEvent event;
private UUID orderId;

@Override
public ProcessEvent getEvent() {
return this.event;
}
}

@Service
public class OrderProcessor implements Processor {
    @Override
    public ProcessData process(ProcessData data) throws ProcessException{   
        ((OrderData)data).setEvent(OrderEvent.orderCreated); 
        return data;
    }
}

@Service
public class PaymentProcessor implements Processor {
    @Override
    public ProcessData process(ProcessData data) throws ProcessException {
        if(((OrderData)data).getPayment() < 1.00) {
            ((OrderData)data).setEvent(OrderEvent.paymentError);
            throw new PaymentException(OrderEvent.paymentError.name());
        } else {
            ((OrderData)data).setEvent(OrderEvent.paymentSuccess);
        }
        return data;
    }
}

@RequiredArgsConstructor
@RestController
public class OrderController {
    private final OrderStateTransitionsManager stateTrasitionsManager;

    @GetMapping("/order/cart")
    public String handleOrderPayment( 
            @RequestParam double payment,
            @RequestParam UUID orderId) throws Exception {

        OrderData data = new OrderData();
    data.setPayment(payment);
    data.setOrderId(orderId);
    data.setEvent(OrderEvent.pay);
    data = (OrderData)stateTrasitionsManager.processEvent(data);

        return ((OrderEvent)data.getEvent()).name();
    }

    @ExceptionHandler(value=OrderException.class)
    public String handleOrderException(OrderException e) {
        return e.getMessage();
    }

    @GetMapping("/order")
    public String handleOrderSubmit() throws ProcessException {

        OrderData data = new OrderData();
        data.setEvent(OrderEvent.submit);
        data = (OrderData)stateTrasitionsManager.processEvent(data);

        return ((OrderEvent)data.getEvent()).name() + ", orderId = " + data.getOrderId();
    }
}


@SpringBootApplication
public class StateMachineApplication {
    public static void main(String[] args) {
        SpringApplication.run(StateMachineApplication.class, args);
    }
}

完整的源代码也可以在GitHub上找到。

请注意,实现此框架的第一步是创建状态转换表。要运行此示例,请将源 导入到STS之类的IDE中,并作为Spring Boot应用程序运行。可以执行以下两个API来测试上述实现:

GET http://localhost:8080/order

GET http://localhost:8080/order/cart?payment=123&orderId=123

为了在浏览器中进行快速测试,两个API均实现为GET。

测试#1:当 /order 调用API时,将返回诸如“创建订单,orderId = 123”的响应,并显示控制台日志:

Initial state: Default Pre-event: submit Post-event: orderCreated Final state: PaymentPending

测试2:如果 /order/cart 调用API时出现付款错误,例如: /order/cart?payment=0&orderId=123 调用了API,则返回响应:“ paymentError”,并且控制台日志显示: Initial state: PaymentPending Pre-event: pay Post-event: paymentError Final state: PaymentPending

测试#3:当 /order/cart?payment=123&orderId=123 API调用,就像一个回应:“paymentSuccess”返回和控制台日志显示: Initial state: PaymentPending Pre-event: pay Post-event: paymentSuccess Final state: Completed

测试#4:付款成功处理后,如果 /order/cart?payment=123&orderId=123 再次调用,则返回响应:“ orderId = 123的订单已完成”,状态保持不变。

结论 呈现的状态机应易于定制,并集成到面向流程/工作流的Spring Boot项目中。希望你喜欢!


原文链接:https://codingdict.com/