mcpField
S
spring-boot-crud-patterns

spring-boot-crud-patterns

github.com/giuseppe-trisciuoglio开发3653.9k 次安装7 周前更新
代码仓库

关于 spring-boot-crud-patterns

用于构建和诊断 Spring Boot 3.5+ CRUD 服务的技能,采用按功能组织、受 DDD 启发的分层结构。它指导实现领域模型、JPA 持久化、应用服务、DTO 和 REST 控制器,并提供验证步骤。

主要功能

  1. 01按功能划分包结构
  2. 02领域聚合与仓储端口
  3. 03JPA 持久化适配器
  4. 04事务型应用服务
  5. 05经过验证的 DTO 和 REST 控制器
  6. 06测试与迁移检查

适用场景

  1. 01创建 Spring Data JPA CRUD 接口
  2. 02围绕领域功能组织服务
  3. 03诊断仓储或事务回归问题
  4. 04为 REST 列表接口添加分页

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill spring-boot-crud-patterns

安装后在 Claude Code 中输入/spring-boot-crud-patterns确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

assets/specs/
product_with_rel.json
product.json
references/
crud-reference.md
examples-product-feature.md
generator-usage.md
spring-official-docs.md
references/templates/
Controller.java.tpl
CreateService.java.tpl
DeleteService.java.tpl
DomainModel.java.tpl
DomainRepository.java.tpl
DomainService.java.tpl
DtoRequest.java.tpl
DtoResponse.java.tpl
EntityExceptionHandler.java.tpl
ErrorResponse.java.tpl
ExistException.java.tpl
GetService.java.tpl
GlobalExceptionHandler.java.tpl
JpaEntity.java.tpl
ListService.java.tpl
Mapper.java.tpl
NotFoundException.java.tpl
PageResponse.java.tpl
PersistenceAdapter.java.tpl
README.md
SpringDataRepository.java.tpl
UpdateService.java.tpl
scripts/examples/
product_with_rel.json
product.json

详细介绍

Spring Boot CRUD 模式

概述

为使用以功能为中心架构的 Spring Boot 3.5+ 服务提供完整的 CRUD 工作流。创建并验证领域聚合、JPA 仓库、应用服务和 REST 控制器,并确保关注点得到合理分离。详细代码清单请参阅参考文件,以便逐步展开内容。

适用场景

  • 为由 Spring Data JPA 支持的创建/读取/更新/删除工作流创建 REST 端点。
  • 按照受 DDD 启发的架构实现功能包,包含聚合、仓库和应用服务。
  • 为外部客户端定义 DTO record、请求验证和控制器映射。
  • 诊断现有 Spring Boot 服务中的 CRUD 回归问题、仓库契约或事务边界。
  • 触发短语:“实现 Spring CRUD 控制器”、“创建端点”、“添加数据库实体”、“改进基于功能的仓库”、“为 JPA 聚合映射 DTO”、“为 REST 列表端点添加分页”。

操作说明

按照以下精简工作流构建与功能相匹配的 CRUD 服务,并设置明确的验证关卡:

1. 建立功能结构

创建 feature/<name>/ 目录,并包含 domain、application、presentation 和 infrastructure 子包。 验证:继续之前,确认目录结构与功能边界一致。

2. 定义领域模型

创建实体类,并通过工厂方法(create、update)强制执行不变量。领域逻辑应与框架无关。 验证:进入下一步之前,确认所有不变量均已由单元测试覆盖。

3. 暴露领域端口

在 domain/repository 中声明仓库接口,描述持久化契约,但不包含实现细节。 验证:确认接口签名与领域操作一致。

4. 提供基础设施适配器

在 infrastructure/persistence 中创建映射到领域模型的 JPA 实体,并实现 Spring Data 仓库。 验证:运行 @DataJpaTest,验证实体映射和仓库集成。

5. 实现应用服务

创建用于编排领域操作和 DTO 映射的 @Transactional 服务类。 验证:确保事务边界正确,并在需要时应用乐观锁。

6. 定义 DTO 和控制器

使用带有 jakarta.validation 注解的 Java record 定义 API 契约。映射 REST 端点并设置正确的状态码。 验证:测试验证约束,并确认 HTTP 状态码(POST 为 201、GET 为 200、DELETE 为 204)。

7. 验证并部署

使用 Testcontainers 运行集成测试。确认迁移(Liquibase/Flyway)与聚合架构一致。 验证:部署前执行完整测试套件,并确认已应用架构迁移脚本。

完整的代码示例与各步骤对应,请参阅 references/examples-product-feature.md。

示例

Java 代码示例:Product 功能
// feature/product/domain/Product.java
package com.example.product.domain;

import java.math.BigDecimal;
import java.time.Instant;

public record Product(
    String id,
    String name,
    String description,
    BigDecimal price,
    int stock,
    Instant createdAt,
    Instant updatedAt
) {
    public static Product create(String name, String desc, BigDecimal price, int stock) {
        if (name == null || name.isBlank()) throw new IllegalArgumentException("Name required");
        if (price == null || price.compareTo(BigDecimal.ZERO) < 0) throw new IllegalArgumentException("Invalid price");
        return new Product(null, name.trim(), desc, price, stock, Instant.now(), null);
    }

    public Product withPrice(BigDecimal newPrice) {
        return new Product(id, name, description, newPrice, stock, createdAt, Instant.now());
    }
}
// feature/product/domain/repository/ProductRepository.java
package com.example.product.domain.repository;

import com.example.product.domain.Product;
import java.util.Optional;

public interface ProductRepository {
    Product save(Product product);
    Optional<Product> findById(String id);
    void deleteById(String id);
}
// feature/product/infrastructure/persistence/ProductJpaEntity.java
package com.example.product.infrastructure.persistence;

import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.Instant;

@Entity @Table(name = "products")
public class ProductJpaEntity {
    @Id @GeneratedValue(strategy = GenerationType.UUID)
    private String id;
    private String name;
    private String description;
    private BigDecimal price;
    private int stock;
    private Instant createdAt;
    private Instant updatedAt;

    // getters, setters, constructor from domain (omitted for brevity)
}
// feature/product/infrastructure/persistence/JpaProductRepository.java
package com.example.product.infrastructure.persistence;

import com.example.product.domain.Product;
import com.example.product.domain.repository.ProductRepository;
import org.springframework.stereotype.Repository;

@Repository
public class JpaProductRepository implements ProductRepository {
    private final SpringDataProductRepository springData;

    public JpaProductRepository(SpringDataProductRepository springData) {
        this.springData = springData;
    }

    @Override
    public Product save(Product product) {
        ProductJpaEntity entity = toEntity(product);
        ProductJpaEntity saved = springData.save(entity);
        return toDomain(saved);
    }

    // findById, deleteById implementations...
}
// feature/product/presentation/rest/ProductController.java
package com.example.product.presentation.rest;

import com.example.product.domain.Product;
import com.example.product.domain.repository.ProductRepository;
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController @RequestMapping("/api/products")
public class ProductController {
    private final ProductService service;

    public ProductController(ProductService service) { this.service = service; }

    @PostMapping
    public ResponseEntity<ProductResponse> create(@Valid @RequestBody CreateProductRequest req) {
        Product product = service.create(req.toDomain());
        return ResponseEntity.status(201).body(ProductResponse.from(product));
    }

    @GetMapping("/{id}")
    public ResponseEntity<ProductResponse> getById(@PathVariable String id) {
        return service.findById(id)
            .map(p -> ResponseEntity.ok(ProductResponse.from(p)))
            .orElse(ResponseEntity.notFound().build());
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable String id) {
        service.deleteById(id);
        return ResponseEntity.noContent().build();
    }

    // record DTOs
    public record CreateProductRequest(
        @NotBlank String name,
        String description,
        @NotNull @DecimalMin("0.01") java.math.BigDecimal price,
        @Min(0) int stock
    ) {
        Product toDomain() { return Product.create(name, description, price, stock); }
    }

    public record ProductResponse(String id, String name, java.math.BigDecimal price) {
        static ProductResponse from(Product p) { return new ProductResponse(p.id(), p.name(), p.price()); }
    }
}
JSON 输入/输出示例

创建请求:

{
  "name": "Wireless Keyboard",
  "description": "Ergonomic keyboard",
  "price": 79.99,
  "stock": 50
}

创建成功响应(201):

{
  "id": "prod-123",
  "name": "Wireless Keyboard",
  "price": 79.99,
  "_links": { "self": "/api/products/prod-123" }
}

分页列表请求:

curl "http://localhost:8080/api/products?page=0&size=10&sort=name,asc"

最佳实践

  • 在功能包中按聚合将领域、应用和展示代码放在一起。
  • 使用 Java record 创建不可变 DTO;在服务边界转换领域类型。
  • 对写操作应用事务和乐观锁。
  • 规范分页默认值(page、size、sort),并记录查询参数。
  • 在 info 级别记录 CRUD 生命周期事件(创建、更新、删除),并提供结构化审计跟踪。
  • 通过 Spring Boot Actuator 提供健康状态和指标;监控吞吐量和错误率。

约束与警告

  • 绝不在控制器中直接暴露 JPA 实体,以防止延迟加载泄漏和序列化问题。
  • 绝不混用字段注入和构造函数注入;保持不可变性,以便于测试。
  • 绝不在控制器或仓储适配器中嵌入业务逻辑;将其保留在领域层或应用层。
  • 始终严格验证输入,以防止违反约束,并生成一致的错误负载。
  • 始终确保在部署架构变更之前,迁移(Liquibase/Flyway)与聚合的演进保持一致。
  • 始终在合并之前使用 Testcontainers 运行集成测试,以防止持久化回归。

参考资料

评论

支持 Markdown