关于 java-spring-best-practices
面向使用现代 Java 构建 Spring Boot 应用的开发者参考技能,涵盖架构、验证、错误处理、测试、日志和性能实践,适用于 Spring Boot 3.5.x、4.0.x 与 Java 17+。
主要功能
- 01分层应用架构
- 02基于 Record 的 DTO 与配置
- 03ProblemDetail 错误处理
- 04虚拟线程与 RestClient
- 05Mockito 和 Testcontainers 测试
- 06结构化日志与可观测性
适用场景
- 01设计 Spring Boot 服务结构
- 02统一 API 验证与错误响应
- 03使用真实数据库测试集成
- 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.mdreferences/architecture.mdconfig-reference.mdexception-handling.mdperformance-guide.mdstreams-guide.mdtesting-guide.mdscripts/GlobalExceptionHandler.javalogback-config.xmlpom-template.xmlProductController.javaProductService.javaProductServiceTest.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 DTO | Java Records |
Collectors.toList() | .toList()(Java 16+) |
| 使用 H2 进行集成测试 | 使用 Testcontainers 和真实数据库 |
| 自定义错误 JSON 包装器 | ProblemDetail(RFC 7807) |
| 在新代码中使用 RestTemplate | RestClient |
synchronized + 虚拟线程 | ReentrantLock |
System.out.println() | @Slf4j + 占位符 |
参考文档
- 架构 — 分层结构与按功能划分的结构
- 异常处理 — 密封层次结构、ProblemDetail
- 配置 — 配置文件、Docker Compose、Records
- Stream 与 Java 惯用写法 — 函数式模式、现代语法
- 测试 — Mockito、Testcontainers、切片测试
- 性能 — 虚拟线程、缓存、可观测性
代码示例
- ProductController.java — 使用 Records 的 REST 控制器
- ProductService.java — 包含业务逻辑的服务
- GlobalExceptionHandler.java — ProblemDetail 处理器
- ProductServiceTest.java — 单元测试 + IT 模式
- pom-template.xml — 完整的 Maven 配置
设计原则
- 不可变性优先 — DTO/配置使用 Records,所有字段均为 final
- 单一职责 — 每个类只做一件事
- 快速失败 — 尽早验证输入,抛出有意义的异常
- 让无效状态无法存在 — 密封层次结构、强类型、验证
- 默认具备可观测性 — 从一开始就使用结构化日志、指标和链路追踪
最后更新:2026 年 3 月 | 基于:Spring Boot 3.5.x / 4.0.x、Java 17+(推荐 21+)