mcpField
J
java-spring-best-practices

java-spring-best-practices

github.com/cosbort开发539 次安装13 周前更新
代码仓库

关于 java-spring-best-practices

面向使用现代 Java 构建 Spring Boot 应用的开发者参考技能,涵盖架构、验证、错误处理、测试、日志和性能实践,适用于 Spring Boot 3.5.x、4.0.x 与 Java 17+。

主要功能

  1. 01分层应用架构
  2. 02基于 Record 的 DTO 与配置
  3. 03ProblemDetail 错误处理
  4. 04虚拟线程与 RestClient
  5. 05Mockito 和 Testcontainers 测试
  6. 06结构化日志与可观测性

适用场景

  1. 01设计 Spring Boot 服务结构
  2. 02统一 API 验证与错误响应
  3. 03使用真实数据库测试集成
  4. 04应用 Java 21 虚拟线程

安装

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

$npx skills add https://github.com/cosbort/agent-skills --skill java-spring-best-practices

安装后在 Claude Code 中输入/java-spring-best-practices确认已加载。

包含的文件

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

README.md
references/
architecture.md
config-reference.md
exception-handling.md
performance-guide.md
streams-guide.md
testing-guide.md
scripts/
GlobalExceptionHandler.java
logback-config.xml
pom-template.xml
ProductController.java
ProductService.java
ProductServiceTest.java

详细介绍

Java Spring Boot 最佳实践 v2.0

适用于生产环境的 Spring Boot 指南。已更新至 Spring Boot 3.5.x / 4.0.x、Java 17+(针对 21+ 优化)。

v2.0 新特性

  • 使用 Java Records 作为 DTO(默认不可变)
  • 使用虚拟线程处理高并发 I/O(spring.threads.virtual.enabled=true)
  • 使用 ProblemDetail(RFC 7807)实现标准化错误响应
  • 使用 RestClient 替代 RestTemplate(流畅式、同步)
  • Spring Boot 3.4+ 内置结构化日志
  • 以 Testcontainers 作为主要集成测试策略(取代 H2)
  • 使用 Micrometer Observation API 实现可观测性
  • 为开发环境提供 Docker Compose 集成
  • 使用 sealed 类构建异常层次结构
  • 兼顾 GraalVM Native Image / CDS

快速入门检查清单

  • ✅ Java 17+(虚拟线程需要 21+)
  • ✅ 使用 spring-boot-starter-parent 管理依赖
  • ✅ 使用 @RequiredArgsConstructor 进行构造器注入
  • ✅ 使用 Java Records 作为 DTO(不使用可变的 @Data 类)
  • ✅ 控制器仅负责路由;服务负责业务逻辑
  • ✅ ProblemDetail(RFC 7807)+ @RestControllerAdvice
  • ✅ 对 record 组件使用 @Valid + Bean Validation
  • ✅ 使用 SLF4J {} 占位符 + 结构化日志
  • ✅ 使用 YAML 配置文件 profile(dev/prod/test)
  • ✅ 单元测试(Mockito)+ 集成测试(Testcontainers)
  • ✅ spring.threads.virtual.enabled=true

核心架构

controller/   → HTTP routing only (thin)
    ↓
service/      → Business logic, caching, validation
    ↓
repository/   → Spring Data JPA
    ↓
entity/       → JPA domain models

每一层只依赖其下方的层。控制器绝不直接访问仓库。请参阅 architecture.md,了解基于分层与基于功能的模式。

依赖注入(仅使用构造器注入)
@Service
@RequiredArgsConstructor
public class ProductService {
    private final ProductRepository repository;
    private final ProductMapper mapper;
}

使用 Java Records 作为 DTO

Records 可替代 @Data Lombok 类:不可变、简洁且线程安全。

// Request (with validation)
public record CreateProductRequest(
    @NotBlank @Size(min = 3, max = 100) String name,
    @NotNull @DecimalMin("0.01") BigDecimal price,
    @Size(max = 500) String description
) {}

// Response
public record ProductResponse(
    Long id, String name, BigDecimal price,
    String description, Instant createdAt
) {}
映射
@Component
public class ProductMapper {
    public ProductResponse toResponse(Product entity) {
        return new ProductResponse(entity.getId(), entity.getName(),
            entity.getPrice(), entity.getDescription(), entity.getCreatedAt());
    }

    public Product toEntity(CreateProductRequest req) {
        return Product.builder()
            .name(req.name()).price(req.price()).description(req.description()).build();
    }
}
使用 Records 的控制器
@RestController
@RequestMapping("/api/v1/products")
@RequiredArgsConstructor
public class ProductController {
    private final ProductService service;

    @PostMapping
    public ResponseEntity<ProductResponse> create(
            @Valid @RequestBody CreateProductRequest request) {
        return ResponseEntity.status(HttpStatus.CREATED).body(service.create(request));
    }

    @GetMapping("/{id}")
    public ResponseEntity<ProductResponse> getById(@PathVariable Long id) {
        return ResponseEntity.ok(service.getById(id));
    }

    @GetMapping
    public ResponseEntity<Page<ProductResponse>> list(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {
        return ResponseEntity.ok(service.getAll(PageRequest.of(page, size)));
    }
}

完整示例请参阅 ProductController.java 和 ProductService.java。

异常处理(ProblemDetail + sealed 类)

启用 RFC 7807
spring:
  mvc:
    problemdetails:
      enabled: true
sealed 异常层次结构
public sealed abstract class ApplicationException extends RuntimeException
    permits ResourceNotFoundException, DuplicateResourceException,
            BusinessRuleException, ExternalServiceException {

    private final HttpStatus httpStatus;
    protected ApplicationException(String message, HttpStatus httpStatus) {
        super(message);
        this.httpStatus = httpStatus;
    }
    public HttpStatus getHttpStatus() { return httpStatus; }
}

public final class ResourceNotFoundException extends ApplicationException {
    public ResourceNotFoundException(String resource, Object id) {
        super("%s not found with id: %s".formatted(resource, id), HttpStatus.NOT_FOUND);
    }
}
处理器
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(ApplicationException.class)
    public ProblemDetail handleApp(ApplicationException ex) {
        log.warn("Application exception: {}", ex.getMessage());
        ProblemDetail p = ProblemDetail.forStatusAndDetail(ex.getHttpStatus(), ex.getMessage());
        p.setTitle(ex.getClass().getSimpleName());
        p.setProperty("timestamp", Instant.now());
        return p;
    }
}

响应:{"type":"about:blank","title":"ResourceNotFoundException","status":404,"detail":"Product not found with id: 42","timestamp":"..."}

完整层次结构、验证处理器和相关模式请参阅 exception-handling.md。

虚拟线程(Java 21+)

只需设置一个属性,即可扩展处理阻塞式 I/O(JDBC、REST 调用),无需使用响应式编程:

spring:
  threads:
    virtual:
      enabled: true
  main:
    keep-alive: true

Tomcat、@Async、@Scheduled 都会自动使用虚拟线程。避免在 I/O 期间使用 synchronized 代码块(会导致线程固定)——请改用 ReentrantLock。

有关避免线程固定、连接池大小设置和检测方法,请参阅 performance-guide.md。

RestClient(替代 RestTemplate)

@Service
@RequiredArgsConstructor
public class ExternalApiService {
    private final RestClient restClient;

    public ProductResponse fetchProduct(Long id) {
        return restClient.get()
            .uri("/products/{id}", id)
            .retrieve()
            .onStatus(HttpStatusCode::is4xxClientError, (req, res) -> {
                throw new ResourceNotFoundException("External product", id);
            })
            .body(ProductResponse.class);
    }
}

对于声明式接口,请使用 @HttpExchange。有关配置和超时设置,请参阅 performance-guide.md。

日志记录

@Slf4j
@Service
public class ProductService {
    public void process(Long id) {
        log.info("Processing product ID: {}", id);       // ✅ Placeholders
        log.error("Error processing product", exception); // ✅ Pass exception
    }
}
结构化日志(Spring Boot 3.4+)
logging:
  structured:
    format:
      console: ecs    # Options: ecs, gelf, logstash

配置

spring:
  threads:
    virtual:
      enabled: true
  datasource:
    url: jdbc:postgresql://localhost:5432/db
    username: ${DB_USERNAME:dev}
    hikari:
      maximum-pool-size: 20
  jpa:
    hibernate.ddl-auto: validate
    open-in-view: false
  mvc:
    problemdetails:
      enabled: true
  cache:
    type: caffeine
    caffeine:
      spec: maximumSize=1000,expireAfterWrite=10m
基于 Record 的配置属性
@ConfigurationProperties(prefix = "app")
public record AppProperties(
    @NotBlank String name,
    @Valid JwtProperties jwt
) {
    public record JwtProperties(@NotBlank String secret, @NotNull Duration expiration) {}
}

有关 profile、Docker Compose 和外部化配置,请参阅 config-reference.md。

测试

单元测试(Mockito + AssertJ)
@ExtendWith(MockitoExtension.class)
class ProductServiceTest {
    @Mock private ProductRepository repository;
    @Mock private ProductMapper mapper;
    @InjectMocks private ProductService service;

    @Test
    void shouldThrow_whenNotFound() {
        when(repository.findById(1L)).thenReturn(Optional.empty());
        assertThatThrownBy(() -> service.getById(1L))
            .isInstanceOf(ResourceNotFoundException.class);
    }
}
集成测试(Testcontainers)
@SpringBootTest
@AutoConfigureMockMvc
@Testcontainers
class ProductControllerIT {

    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgres =
        new PostgreSQLContainer<>("postgres:16-alpine");

    @Autowired private MockMvc mockMvc;

    @Test
    void shouldCreateProduct() throws Exception {
        mockMvc.perform(post("/api/v1/products")
                .contentType(APPLICATION_JSON)
                .content("""
                    {"name":"Widget","price":29.99,"description":"A widget"}
                    """))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.name").value("Widget"));
    }
}

有关全面的模式、切片测试和共享容器,请参阅 testing-guide.md。

现代 Java 惯用写法

// .toList() instead of Collectors.toList() (Java 16+)
List<String> names = products.stream().map(Product::getName).toList();

// Pattern matching switch (Java 21+, exhaustive on sealed types)
return switch (ex) {
    case ResourceNotFoundException e -> "Missing: " + e.getMessage();
    case DuplicateResourceException e -> "Conflict: " + e.getMessage();
    case BusinessRuleException e -> "Rule: " + e.getMessage();
    case ExternalServiceException e -> "External: " + e.getMessage();
};

// Text blocks for JPQL
@Query("""
    SELECT p FROM Product p LEFT JOIN FETCH p.category
    WHERE p.price > :min ORDER BY p.createdAt DESC
    """)
List<Product> findExpensive(@Param("min") BigDecimal min);

有关高级模式,请参阅 streams-guide.md。

常见陷阱

错误解决方案
@Autowired 字段注入构造函数注入(@RequiredArgsConstructor)
可变的 @Data DTOJava Records
Collectors.toList().toList()(Java 16+)
使用 H2 进行集成测试使用 Testcontainers 和真实数据库
自定义错误 JSON 包装器ProblemDetail(RFC 7807)
在新代码中使用 RestTemplateRestClient
synchronized + 虚拟线程ReentrantLock
System.out.println()@Slf4j + 占位符

参考文档

代码示例

设计原则

  1. 不可变性优先 — DTO/配置使用 Records,所有字段均为 final
  2. 单一职责 — 每个类只做一件事
  3. 快速失败 — 尽早验证输入,抛出有意义的异常
  4. 让无效状态无法存在 — 密封层次结构、强类型、验证
  5. 默认具备可观测性 — 从一开始就使用结构化日志、指标和链路追踪

最后更新:2026 年 3 月 | 基于:Spring Boot 3.5.x / 4.0.x、Java 17+(推荐 21+)

评论

支持 Markdown