java implementation of finite state machine (FSM)

java implementation of finite state machine (FSM)

1. Introduction to finite state machine

Finite state machine, also known as FSM(Finite State Machine), is in a state in the finite state set at any time. When it obtains an input character, it will transition from the current state to another state, or remain in the current state. The necessary conditions for the establishment of finite state machine are:

  1. An object has a set of mutually exclusive states (or the life cycle of the object), and this set of states can cover the whole process from the creation to the extinction of the object.
  2. When a signal (or event) is passed to an object, the state of the object can change from the current state to another state, or keep the state unchanged.
  3. The state is limited.

As shown in the legend, the traffic light can only light one color at a time. The control program can define three different events. Each event defines the starting color and target color of the traffic light. We don't need to operate the traffic light switch directly. We just need to send events in a certain order, and we can accurately control the work of the traffic light, In fact, the working standard of FSM is a green light control.

As mentioned above, FSM generally consists of the following four parts:

  1. Object state enumeration.
  2. Object event enumeration, and specify the start state and target state of the event.
  3. Event logic body, which is used to handle business logic caused by state change.
  4. Event registration factory class, the only entry for FSM.

2. Analyze FSM from requirements

When we get a similar demand, how should we start? Let's take the FSM implementation of a simple mall order as an example to explain from demand analysis to code implementation:

First, we should extract the specific status of an order from the requirements (I only list the positive status of a simple order here).

Order status: to be paid, to be shipped, to be received, cancelled, completed

After listing all the statuses, draw all the statuses in the drawing tool. Analyze whether each status can be converted to other statuses. For example, analyze the status to be paid. Users can make payment events from the status to be paid, and the status to be paid will be converted to the status to be shipped. Users can also cancel payment from the status to be paid, and the status to be paid will be converted to the order cancellation status. Follow this idea, use one-way arrows to list all events and give each event a name.

Order event: order event, payment event, shipment event, payment cancellation event, and receipt event

Finally, the state flow diagram shown in the figure below is formed

3. java implementation of FSM

As mentioned above, we declare the four parts of FSM:

3.1. Object state enumeration class

Enumerate all possible states of the order.

public enum OrderStatusEnum {
    Unpaid("Unpaid", "To be paid"),
    UnShipping("UnShipping", "To be shipped"),
    UnReceiving("UnReceiving", "Goods to be received"),

    Canceled("Canceled", "Cancelled"),
    Finished("Finished", "Completed");

    private final String code;
    private final String desc;

    OrderStatusEnum(String code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    public String getCode() {
        return code;
    }

    public String getDesc() {
        return desc;
    }
}

3.2. Object event enumeration class

In order to save trouble, I associate events and states here, or I can take them out separately to make a configuration package.

public enum OrderEventEnum {
    CreateOrder("CreateOrder", "Order event", null, OrderStatusEnum.Unpaid),
    Payment("Payment", "Payment event", OrderStatusEnum.Unpaid, OrderStatusEnum.UnShipping),
    Shipping("Shipping", "Shipment event", OrderStatusEnum.UnShipping, OrderStatusEnum.UnReceiving),
    Receiving("Receiving", "Receiving event", OrderStatusEnum.UnReceiving, OrderStatusEnum.Finished),
    CancelPayment("CancelPayment", "Payment cancellation event", OrderStatusEnum.Unpaid, OrderStatusEnum.Canceled);

    private final String code;
    private final String desc;
    private final OrderStatusEnum sourceOrderStatus;
    private final OrderStatusEnum targetOrderStatus;

    OrderEventEnum(String code, String desc, OrderStatusEnum sourceOrderStatus, OrderStatusEnum targetOrderStatus) {
        this.code = code;
        this.desc = desc;
        this.sourceOrderStatus = sourceOrderStatus;
        this.targetOrderStatus = targetOrderStatus;
    }

    public String getCode() {
        return code;
    }

    public String getDesc() {
        return desc;
    }

    public OrderStatusEnum getSourceOrderStatus() {
        return sourceOrderStatus;
    }

    public OrderStatusEnum getTargetOrderStatus() {
        return targetOrderStatus;
    }
}

3.3. Event logic

First, declare a logic base class, which defines the event process data cache object, event condition verification method, event post-processing method and event business logic processing virtual method (which needs to be implemented by the business itself).

@Slf4j
public abstract class BaseOrderFsmProcessor {
    private static final Map<Long, Object> FSM_DATA_MAP = new ConcurrentHashMap<>();

    /**
     * Execute business logic
     *
     * @param orderId Order ID
     * @param event   Event type
     * @return Success or failure
     */
    public boolean fireProcess(Long orderId, OrderEventEnum event) throws Exception {
        log.info("OrderFSM_start FSM event: orderId={}", orderId);
        if (!checkRule(orderId, event)) {
            log.warn("OrderFSM_If the conditions are not met, the execution is refused FSM event: orderId={}", orderId);
            return true;
        }
        boolean b = process(orderId, event);
        log.info("OrderFSM_end FSM event: orderId={}", orderId);
        postHandler(orderId, event);
        return b;
    }

    /**
     * Business logic implementation class
     *
     * @param orderId Order ID
     * @param event   Event type
     * @return Success or failure
     * @throws Exception ex
     */
    public abstract boolean process(Long orderId, OrderEventEnum event) throws Exception;

    /**
     * Verify whether the conditions are met and execute the current parent process
     *
     * @param orderId Order ID
     * @param event   Event type
     * @return Success or failure
     */
    public boolean checkRule(Long orderId, OrderEventEnum event) throws Exception {
        return true;
    }

    /**
     * Post processing logic
     *
     * @param orderId Order ID
     * @param event   Event type
     */
    public void postHandler(Long orderId, OrderEventEnum event) throws Exception {

    }

    /**
     * Obtain event process data according to process ID
     *
     * @param orderId Order ID
     * @return Event process data
     */
    public static Object getFsmData(Long orderId) {
        return FSM_DATA_MAP.remove(orderId);
    }

    /**
     * Set event process data according to process ID
     *
     * @param orderId Order ID
     * @param obj     Event process data
     */
    public static void setFsmData(Long orderId, Object obj) {
        FSM_DATA_MAP.put(orderId, obj);
    }

}

Then follow up the base class to derive the processing methods corresponding to different events. An event should declare a corresponding method:

@Slf4j
public class CreateOrderProcessor extends BaseOrderFsmProcessor {

    @Override
    public boolean process(Long orderId, OrderEventEnum event) throws Exception {
        log.info("Business logic in execution:className={},event={}", getClass().getSimpleName(), event.name());
        //TODO simulation business logic
        TimeUnit.MILLISECONDS.sleep(1000);
        setFsmData(orderId, Thread.currentThread().getName());

        log.info("Business logic execution completed");
        return true;
    }

    @Override
    public boolean checkRule(Long orderId, OrderEventEnum event) throws Exception {
        log.info("Execution condition check passed");
        return true;
    }

    @Override
    public void postHandler(Long orderId, OrderEventEnum event) throws Exception {
        log.info("Execute post-processing logic");
        // TODO changes the status of orderId to event getTargetOrderStatus()
    }
}

3.4. Event registration factory class

The factory class encapsulates the unique entry of single instance FSM execution and the entry of intermediate data of query setting process.

@Slf4j
public class OrderFsmManager {

    private final Map<OrderEventEnum, BaseOrderFsmProcessor> orderProcessorMap = new HashMap<>();
    private volatile static OrderFsmManager orderFsmManager;

    private OrderFsmManager() {
        orderProcessorMap.put(OrderEventEnum.CreateOrder, new CreateOrderProcessor());
        orderProcessorMap.put(OrderEventEnum.Payment, new PaymentFsmProcessor());
    }

    /**
     * Get fsm instance
     */
    public static OrderFsmManager getInstance() {
        if (orderFsmManager == null) {
            synchronized (OrderFsmManager.class) {
                if (orderFsmManager == null) {
                    orderFsmManager = new OrderFsmManager();
                }
            }
        }
        return orderFsmManager;
    }

    /**
     * Start executing fsm event
     *
     * @param orderId Order ID
     * @param event   Event type
     * @return Success or failure
     */
    public boolean fireProcess(Long orderId, OrderEventEnum event) throws Exception {
        if (!orderProcessorMap.containsKey(event)) {
            throw new Exception(String.format("MediaProcessFSM No events matched: orderId=%s,currentOrderEvent=%s"
                    , orderId, event));
        }
        return orderProcessorMap.get(event).fireProcess(orderId, event);
    }

    /**
     * Obtain event process data according to process ID
     *
     * @param orderId Order ID
     * @return Event process data
     */
    public Object getFsmData(Long orderId) {
        return BaseOrderFsmProcessor.getFsmData(orderId);
    }

    /**
     * Set event process data according to process ID
     *
     * @param orderId Order ID
     * @param obj     Event process data
     */
    public void setFsmData(Long orderId, Object obj) {
        BaseOrderFsmProcessor.setFsmData(orderId, obj);
    }
}

3.5. Simply do a test

	public static void main(String[] args) throws Exception {
        OrderFsmManager orderFsmManager = OrderFsmManager.getInstance();
//        boolean b1 = orderFsmManager.fireProcess(1L, OrderEventEnum.CreateOrder);
//        boolean b2 = orderFsmManager.fireProcess(2L, OrderEventEnum.Payment);
//        System.out.println(String.format("orderId=%s,data=%s",1, orderFsmManager.getFsmData(1L)));
//        System.out.println(String.format("orderId=%s,data=%s",2, orderFsmManager.getFsmData(2L)));

        for (int i = 0; i < 3; i++) {
            int finalI = i;
            new Thread(() -> {
                String threadName = "thread-" + finalI;
                Thread.currentThread().setName(threadName);

                try {
                    if (finalI%2==0){
                        boolean b = orderFsmManager.fireProcess((long) finalI, OrderEventEnum.CreateOrder);
                    }else {
                        boolean b = orderFsmManager.fireProcess((long) finalI, OrderEventEnum.Payment);
                    }

                    System.out.println(String.format("threadName=%s,data=%s",threadName, orderFsmManager.getFsmData((long) finalI)));
                } catch (Exception exception) {
                    exception.printStackTrace();
                }
            }).start();
        }


    }
11:17:39.213 [thread-2] INFO org.yc.test.fsm4.processor.BaseOrderFsmProcessor - OrderFSM_start FSM event: orderId=2
11:17:39.221 [thread-2] INFO org.yc.test.fsm4.processor.CreateOrderProcessor - Execution condition check passed
11:17:39.221 [thread-2] INFO org.yc.test.fsm4.processor.CreateOrderProcessor - Business logic in execution:className=CreateOrderProcessor,event=CreateOrder
11:17:39.213 [thread-1] INFO org.yc.test.fsm4.processor.BaseOrderFsmProcessor - OrderFSM_start FSM event: orderId=1
11:17:39.213 [thread-0] INFO org.yc.test.fsm4.processor.BaseOrderFsmProcessor - OrderFSM_start FSM event: orderId=0
11:17:39.223 [thread-0] INFO org.yc.test.fsm4.processor.CreateOrderProcessor - Execution condition check passed
11:17:39.223 [thread-1] INFO org.yc.test.fsm4.processor.PaymentFsmProcessor - Execution condition check passed
11:17:39.223 [thread-0] INFO org.yc.test.fsm4.processor.CreateOrderProcessor - Business logic in execution:className=CreateOrderProcessor,event=CreateOrder
11:17:39.223 [thread-1] INFO org.yc.test.fsm4.processor.PaymentFsmProcessor - Business logic in execution:className=PaymentFsmProcessor,event=Payment
11:17:40.223 [thread-1] INFO org.yc.test.fsm4.processor.PaymentFsmProcessor - Business logic execution completed
11:17:40.223 [thread-1] INFO org.yc.test.fsm4.processor.BaseOrderFsmProcessor - OrderFSM_end FSM event: orderId=1
11:17:40.223 [thread-1] INFO org.yc.test.fsm4.processor.PaymentFsmProcessor - Execute post-processing logic
11:17:40.228 [thread-2] INFO org.yc.test.fsm4.processor.CreateOrderProcessor - Business logic execution completed
11:17:40.228 [thread-2] INFO org.yc.test.fsm4.processor.BaseOrderFsmProcessor - OrderFSM_end FSM event: orderId=2
11:17:40.228 [thread-2] INFO org.yc.test.fsm4.processor.CreateOrderProcessor - Execute post-processing logic
threadName=thread-1,data=thread-1
threadName=thread-2,data=thread-2
11:17:40.246 [thread-0] INFO org.yc.test.fsm4.processor.CreateOrderProcessor - Business logic execution completed
11:17:40.247 [thread-0] INFO org.yc.test.fsm4.processor.BaseOrderFsmProcessor - OrderFSM_end FSM event: orderId=0
11:17:40.247 [thread-0] INFO org.yc.test.fsm4.processor.CreateOrderProcessor - Execute post-processing logic
threadName=thread-0,data=thread-0

4. Conclusion

Here, the encapsulation of FSM is completed, and some designs can be adjusted according to their own technical choices. For example, the single instance implementation of factory class can use spring's bean injection method, and the binding relationship between events and states can be hung out as configuration items.

Keywords: yc

Added by refined on Fri, 11 Feb 2022 00:09:10 +0200