Spring State Machine案例

wen java案例 1

本文目录导读:

Spring State Machine案例

  1. 项目结构
  2. Maven依赖 (pom.xml)
  3. 主应用类
  4. 领域模型
  5. 状态机配置
  6. 仓库接口
  7. 服务层
  8. 控制器
  9. 配置文件
  10. 测试用例
  11. 使用示例
  12. 核心特性说明

我将为您提供一个完整的Spring State Machine实战案例,以订单状态管理为例。

项目结构

spring-statemachine-demo
├── pom.xml
├── src/main/java/com/example/order/
│   ├── OrderStateMachineApplication.java
│   ├── config/
│   │   ├── OrderStateMachineConfig.java
│   │   └── StateMachineListener.java
│   ├── domain/
│   │   ├── Order.java
│   │   └── OrderEvent.java
│   ├── repository/
│   │   └── OrderRepository.java
│   ├── service/
│   │   ├── OrderService.java
│   │   └── OrderServiceImpl.java
│   └── controller/
│       └── OrderController.java
└── src/main/resources/
    └── application.yml

Maven依赖 (pom.xml)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.14</version>
        <relativePath/>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>spring-statemachine-demo</artifactId>
    <version>1.0.0</version>
    <properties>
        <java.version>11</java.version>
        <spring-statemachine.version>3.2.1</spring-statemachine.version>
    </properties>
    <dependencies>
        <!-- Spring Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- Spring Data JPA -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <!-- H2 Database -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- Spring State Machine -->
        <dependency>
            <groupId>org.springframework.statemachine</groupId>
            <artifactId>spring-statemachine-core</artifactId>
            <version>${spring-statemachine.version}</version>
        </dependency>
        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

主应用类

package com.example.order;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class OrderStateMachineApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderStateMachineApplication.class, args);
    }
}

领域模型

Order.java

package com.example.order.domain;
import lombok.Data;
import javax.persistence.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "order_no", unique = true, length = 32)
    private String orderNo;
    @Column(name = "customer_name", length = 50)
    private String customerName;
    @Column(name = "total_amount")
    private BigDecimal totalAmount;
    @Enumerated(EnumType.STRING)
    @Column(name = "status", length = 20)
    private OrderStatus status;
    @Column(name = "created_time")
    private LocalDateTime createdTime;
    @Column(name = "updated_time")
    private LocalDateTime updatedTime;
    @Column(name = "description", length = 200)
    private String description;
}

OrderStatus.java

package com.example.order.domain;
public enum OrderStatus {
    CREATED,        // 已创建
    PENDING_PAYMENT, // 待支付
    PAID,           // 已支付
    SHIPPED,        // 已发货
    COMPLETED,      // 已完成
    CANCELLED       // 已取消
}

OrderEvent.java

package com.example.order.domain;
public enum OrderEvent {
    SUBMIT,         // 提交订单
    PAY,            // 支付
    SHIP,           // 发货
    CONFIRM,        // 确认收货
    CANCEL          // 取消订单
}

状态机配置

OrderStateMachineConfig.java

package com.example.order.config;
import com.example.order.domain.OrderEvent;
import com.example.order.domain.OrderStatus;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.statemachine.config.EnableStateMachine;
import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter;
import org.springframework.statemachine.config.builders.StateMachineStateConfigurer;
import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer;
import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer;
import org.springframework.statemachine.listener.StateMachineListener;
import org.springframework.statemachine.listener.StateMachineListenerAdapter;
import org.springframework.statemachine.state.State;
import java.util.EnumSet;
@Slf4j
@Configuration
@EnableStateMachine(name = "orderStateMachine")
public class OrderStateMachineConfig extends EnumStateMachineConfigurerAdapter<OrderStatus, OrderEvent> {
    @Override
    public void configure(StateMachineConfigurationConfigurer<OrderStatus, OrderEvent> config) 
            throws Exception {
        config
            .withConfiguration()
            .machineId("orderStateMachine")
            .listener(listener());
    }
    @Override
    public void configure(StateMachineStateConfigurer<OrderStatus, OrderEvent> states) 
            throws Exception {
        states
            .withStates()
            .initial(OrderStatus.CREATED)
            .states(EnumSet.allOf(OrderStatus.class))
            .end(OrderStatus.COMPLETED)
            .end(OrderStatus.CANCELLED);
    }
    @Override
    public void configure(StateMachineTransitionConfigurer<OrderStatus, OrderEvent> transitions) 
            throws Exception {
        transitions
            // 创建 -> 待支付
            .withExternal()
                .source(OrderStatus.CREATED)
                .target(OrderStatus.PENDING_PAYMENT)
                .event(OrderEvent.SUBMIT)
                .and()
            // 待支付 -> 已支付
            .withExternal()
                .source(OrderStatus.PENDING_PAYMENT)
                .target(OrderStatus.PAID)
                .event(OrderEvent.PAY)
                .and()
            // 待支付 -> 已取消(支付超时或用户取消)
            .withExternal()
                .source(OrderStatus.PENDING_PAYMENT)
                .target(OrderStatus.CANCELLED)
                .event(OrderEvent.CANCEL)
                .and()
            // 已支付 -> 已发货
            .withExternal()
                .source(OrderStatus.PAID)
                .target(OrderStatus.SHIPPED)
                .event(OrderEvent.SHIP)
                .and()
            // 已发货 -> 已完成
            .withExternal()
                .source(OrderStatus.SHIPPED)
                .target(OrderStatus.COMPLETED)
                .event(OrderEvent.CONFIRM);
    }
    @Bean
    public StateMachineListener<OrderStatus, OrderEvent> listener() {
        return new StateMachineListenerAdapter<OrderStatus, OrderEvent>() {
            @Override
            public void stateChanged(State<OrderStatus, OrderEvent> from, State<OrderStatus, OrderEvent> to) {
                log.info("状态变更: {} -> {}", 
                    from == null ? "无" : from.getId(), 
                    to == null ? "无" : to.getId());
            }
        };
    }
}

仓库接口

OrderRepository.java

package com.example.order.repository;
import com.example.order.domain.Order;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
    Optional<Order> findByOrderNo(String orderNo);
}

服务层

OrderService.java

package com.example.order.service;
import com.example.order.domain.Order;
import com.example.order.domain.OrderEvent;
import org.springframework.statemachine.StateMachine;
public interface OrderService {
    Order createOrder(Order order);
    Order submitOrder(String orderNo);
    Order payOrder(String orderNo);
    Order shipOrder(String orderNo);
    Order confirmOrder(String orderNo);
    Order cancelOrder(String orderNo);
    Order getOrderByOrderNo(String orderNo);
}

OrderServiceImpl.java

package com.example.order.service;
import com.example.order.domain.Order;
import com.example.order.domain.OrderEvent;
import com.example.order.domain.OrderStatus;
import com.example.order.repository.OrderRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;
import org.springframework.statemachine.StateMachine;
import org.springframework.statemachine.StateMachineEventPublisher;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.UUID;
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
    private final OrderRepository orderRepository;
    private final StateMachine<OrderStatus, OrderEvent> orderStateMachine;
    private final StateMachineEventPublisher eventPublisher;
    @Override
    @Transactional
    public Order createOrder(Order order) {
        order.setOrderNo(generateOrderNo());
        order.setStatus(OrderStatus.CREATED);
        order.setCreatedTime(LocalDateTime.now());
        order.setUpdatedTime(LocalDateTime.now());
        return orderRepository.save(order);
    }
    @Override
    @Transactional
    public Order submitOrder(String orderNo) {
        return processOrderEvent(orderNo, OrderEvent.SUBMIT);
    }
    @Override
    @Transactional
    public Order payOrder(String orderNo) {
        return processOrderEvent(orderNo, OrderEvent.PAY);
    }
    @Override
    @Transactional
    public Order shipOrder(String orderNo) {
        return processOrderEvent(orderNo, OrderEvent.SHIP);
    }
    @Override
    @Transactional
    public Order confirmOrder(String orderNo) {
        return processOrderEvent(orderNo, OrderEvent.CONFIRM);
    }
    @Override
    @Transactional
    public Order cancelOrder(String orderNo) {
        return processOrderEvent(orderNo, OrderEvent.CANCEL);
    }
    @Override
    public Order getOrderByOrderNo(String orderNo) {
        return orderRepository.findByOrderNo(orderNo)
            .orElseThrow(() -> new RuntimeException("订单不存在: " + orderNo));
    }
    private Order processOrderEvent(String orderNo, OrderEvent event) {
        Order order = getOrderByOrderNo(orderNo);
        OrderStatus currentStatus = order.getStatus();
        OrderStatus targetStatus = getTargetStatus(currentStatus, event);
        // 检查当前状态是否允许该事件
        if (!isValidTransition(currentStatus, event)) {
            throw new RuntimeException(
                String.format("订单状态不允许该操作: 当前状态=%s, 事件=%s", 
                    currentStatus, event));
        }
        log.info("订单[{}]执行事件: {},状态: {} -> {}", 
            orderNo, event, currentStatus, targetStatus);
        try {
            // 发送事件到状态机
            orderStateMachine.sendEvent(MessageBuilder
                .withPayload(event)
                .setHeader("orderNo", orderNo)
                .build());
            // 更新订单状态
            order.setStatus(targetStatus);
            order.setUpdatedTime(LocalDateTime.now());
            // 根据事件设置描述信息
            order.setDescription(event.getDescription());
            return orderRepository.save(order);
        } catch (Exception e) {
            log.error("状态机处理失败: {}", e.getMessage());
            throw new RuntimeException("状态机处理失败: " + e.getMessage());
        }
    }
    private OrderStatus getTargetStatus(OrderStatus current, OrderEvent event) {
        switch (current) {
            case CREATED:
                if (event == OrderEvent.SUBMIT) return OrderStatus.PENDING_PAYMENT;
                break;
            case PENDING_PAYMENT:
                if (event == OrderEvent.PAY) return OrderStatus.PAID;
                if (event == OrderEvent.CANCEL) return OrderStatus.CANCELLED;
                break;
            case PAID:
                if (event == OrderEvent.SHIP) return OrderStatus.SHIPPED;
                break;
            case SHIPPED:
                if (event == OrderEvent.CONFIRM) return OrderStatus.COMPLETED;
                break;
            default:
                break;
        }
        throw new RuntimeException("无效的状态转换");
    }
    private boolean isValidTransition(OrderStatus current, OrderEvent event) {
        try {
            getTargetStatus(current, event);
            return true;
        } catch (RuntimeException e) {
            return false;
        }
    }
    private String generateOrderNo() {
        return "ORD" + System.currentTimeMillis() + 
               UUID.randomUUID().toString().substring(0, 8).toUpperCase();
    }
}

控制器

OrderController.java

package com.example.order.controller;
import com.example.order.domain.Order;
import com.example.order.service.OrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderController {
    private final OrderService orderService;
    @PostMapping
    public ResponseEntity<Order> createOrder(@RequestBody Order order) {
        return ResponseEntity.ok(orderService.createOrder(order));
    }
    @GetMapping("/{orderNo}")
    public ResponseEntity<Order> getOrder(@PathVariable String orderNo) {
        return ResponseEntity.ok(orderService.getOrderByOrderNo(orderNo));
    }
    @PostMapping("/{orderNo}/submit")
    public ResponseEntity<Order> submitOrder(@PathVariable String orderNo) {
        return ResponseEntity.ok(orderService.submitOrder(orderNo));
    }
    @PostMapping("/{orderNo}/pay")
    public ResponseEntity<Order> payOrder(@PathVariable String orderNo) {
        return ResponseEntity.ok(orderService.payOrder(orderNo));
    }
    @PostMapping("/{orderNo}/ship")
    public ResponseEntity<Order> shipOrder(@PathVariable String orderNo) {
        return ResponseEntity.ok(orderService.shipOrder(orderNo));
    }
    @PostMapping("/{orderNo}/confirm")
    public ResponseEntity<Order> confirmOrder(@PathVariable String orderNo) {
        return ResponseEntity.ok(orderService.confirmOrder(orderNo));
    }
    @PostMapping("/{orderNo}/cancel")
    public ResponseEntity<Order> cancelOrder(@PathVariable String orderNo) {
        return ResponseEntity.ok(orderService.cancelOrder(orderNo));
    }
}

配置文件

application.yml

server:
  port: 8080
spring:
  application:
    name: order-state-machine-demo
  datasource:
    url: jdbc:h2:mem:orderdb
    driver-class-name: org.h2.Driver
    username: sa
    password: 
  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
    hibernate:
      ddl-auto: create-drop
    show-sql: true
  h2:
    console:
      enabled: true
      path: /h2-console
logging:
  level:
    com.example.order: DEBUG
    org.springframework.statemachine: INFO

测试用例

OrderServiceTest.java

package com.example.order.service;
import com.example.order.domain.Order;
import com.example.order.domain.OrderStatus;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
public class OrderServiceTest {
    @Autowired
    private OrderService orderService;
    @Test
    public void testOrderLifecycle() {
        // 1. 创建订单
        Order order = new Order();
        order.setCustomerName("张三");
        order.setTotalAmount(new BigDecimal("299.00"));
        Order createdOrder = orderService.createOrder(order);
        assertEquals(OrderStatus.CREATED, createdOrder.getStatus());
        assertNotNull(createdOrder.getOrderNo());
        String orderNo = createdOrder.getOrderNo();
        // 2. 提交订单
        Order submittedOrder = orderService.submitOrder(orderNo);
        assertEquals(OrderStatus.PENDING_PAYMENT, submittedOrder.getStatus());
        // 3. 支付订单
        Order paidOrder = orderService.payOrder(orderNo);
        assertEquals(OrderStatus.PAID, paidOrder.getStatus());
        // 4. 发货
        Order shippedOrder = orderService.shipOrder(orderNo);
        assertEquals(OrderStatus.SHIPPED, shippedOrder.getStatus());
        // 5. 确认收货
        Order completedOrder = orderService.confirmOrder(orderNo);
        assertEquals(OrderStatus.COMPLETED, completedOrder.getStatus());
    }
    @Test
    public void testInvalidTransition() {
        // 创建订单
        Order order = new Order();
        order.setCustomerName("李四");
        order.setTotalAmount(new BigDecimal("199.00"));
        Order createdOrder = orderService.createOrder(order);
        String orderNo = createdOrder.getOrderNo();
        // 尝试从未支付状态直接发货(应该失败)
        assertThrows(RuntimeException.class, () -> {
            orderService.shipOrder(orderNo);
        });
    }
    @Test
    public void testCancelOrder() {
        // 创建订单
        Order order = new Order();
        order.setCustomerName("王五");
        order.setTotalAmount(new BigDecimal("399.00"));
        Order createdOrder = orderService.createOrder(order);
        String orderNo = createdOrder.getOrderNo();
        // 提交订单
        orderService.submitOrder(orderNo);
        // 取消订单
        Order cancelledOrder = orderService.cancelOrder(orderNo);
        assertEquals(OrderStatus.CANCELLED, cancelledOrder.getStatus());
    }
}

使用示例

正常流程测试

# 1. 创建订单
curl -X POST http://localhost:8080/api/orders \
  -H "Content-Type: application/json" \
  -d '{
    "customerName": "张三",
    "totalAmount": 299.00
  }'
# 2. 提交订单
curl -X POST http://localhost:8080/api/orders/{orderNo}/submit
# 3. 支付订单
curl -X POST http://localhost:8080/api/orders/{orderNo}/pay
# 4. 发货
curl -X POST http://localhost:8080/api/orders/{orderNo}/ship
# 5. 确认收货
curl -X POST http://localhost:8080/api/orders/{orderNo}/confirm

核心特性说明

状态定义清晰

  • 使用枚举定义所有订单状态
  • 每个状态都有明确的含义

事件驱动

  • 通过事件触发状态转换
  • 支持异步事件处理

完整的状态转换控制

  • 定义合法的转换路径
  • 自动阻止非法操作

监听器支持

  • 监听状态变化
  • 可扩展业务逻辑

持久化

  • 状态自动持久化到数据库
  • 支持复杂业务场景

这个案例展示了Spring State Machine的核心用法,包括状态定义、事件触发、转换控制、监听器等,您可以根据实际业务需求进行扩展和修改。

抱歉,评论功能暂时关闭!