Spring Boot 3 REST API 工程化实践:校验、异常、日志与测试
写出一个能返回 JSON 的接口并不难难的是让几十个接口长期保持一致参数错误要容易定位业务异常不能泄露堆栈日志能够串起一次请求重构后还要有测试兜底。本文以Java 17、Spring Boot 3.x为基础搭建一套小而完整的 REST API 骨架。示例使用 Spring Boot 3 对应的jakarta.*包。1. 先确定接口契约业务响应可以统一外形但不能抹掉 HTTP 状态码的语义。例如参数错误仍应返回400资源不存在返回404未知服务端错误返回500。import java.time.Instant; ​ public record ApiResponseT( String code, String message, T data, String traceId, Instant timestamp ) { public static T ApiResponseT success(T data, String traceId) { return new ApiResponse(OK, success, data, traceId, Instant.now()); } ​ public static T ApiResponseT failure( String code, String message, T data, String traceId) { return new ApiResponse(code, message, data, traceId, Instant.now()); } }code是稳定的机器可读标识message面向人类traceId用于查日志。不要让前端根据可能变化的中文提示判断业务分支。项目至少需要 Web、Validation 和 Test 三组依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency2. 在入口完成参数校验请求对象只描述输入返回对象只描述输出避免把数据库实体直接暴露给 API。import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; ​ public record CreateUserRequest( NotBlank(message name must not be blank) Size(max 50, message name length must be 50) String name, ​ NotBlank(message email must not be blank) Email(message email format is invalid) String email, ​ NotNull(message age must not be null) Min(value 18, message age must be 18) Max(value 120, message age must be 120) Integer age ) {} ​ public record UserView(Long id, String name, String email, Integer age) {}控制器只负责协议转换。Valid触发请求体校验业务规则则留在 Service 中。import jakarta.validation.Valid; import org.slf4j.MDC; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; ​ RestController RequestMapping(/api/users) public class UserController { private final UserService userService; ​ public UserController(UserService userService) { this.userService userService; } ​ PostMapping ResponseStatus(HttpStatus.CREATED) public ApiResponseUserView create(Valid RequestBody CreateUserRequest request) { UserView user userService.create(request); return ApiResponse.success(user, MDC.get(traceId)); } }Bean Validation 只判断字段是否合法。诸如“邮箱是否已注册”“库存是否充足”需要访问业务数据应由 Service 判断并抛出业务异常。3. 为业务错误建立稳定分类public enum ErrorCode { INVALID_ARGUMENT, USER_NOT_FOUND, EMAIL_ALREADY_EXISTS, INTERNAL_ERROR } ​ public class BusinessException extends RuntimeException { private final ErrorCode code; ​ public BusinessException(ErrorCode code, String message) { super(message); this.code code; } ​ public ErrorCode getCode() { return code; } }错误码是对外契约。已经发布的含义不要随意复用内部数据库异常也不要原样返回给调用方。4. 用全局异常处理保持一致RestControllerAdvice把异常集中映射为状态码和响应体控制器不需要重复try/catch。import jakarta.validation.ConstraintViolationException; import java.util.LinkedHashMap; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; ​ RestControllerAdvice public class GlobalExceptionHandler { private static final Logger log LoggerFactory.getLogger(GlobalExceptionHandler.class); ​ ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntityApiResponseMapString, String handleValidation( MethodArgumentNotValidException exception) { MapString, String fields new LinkedHashMap(); exception.getBindingResult().getFieldErrors().forEach(error - fields.putIfAbsent(error.getField(), error.getDefaultMessage())); ​ return ResponseEntity.badRequest().body(ApiResponse.failure( ErrorCode.INVALID_ARGUMENT.name(), request validation failed, fields, traceId())); } ​ ExceptionHandler(ConstraintViolationException.class) public ResponseEntityApiResponseVoid handleConstraint( ConstraintViolationException exception) { return ResponseEntity.badRequest().body(ApiResponse.failure( ErrorCode.INVALID_ARGUMENT.name(), exception.getMessage(), null, traceId())); } ​ ExceptionHandler(HttpMessageNotReadableException.class) public ResponseEntityApiResponseVoid handleUnreadableBody() { return ResponseEntity.badRequest().body(ApiResponse.failure( ErrorCode.INVALID_ARGUMENT.name(), request body is malformed, null, traceId())); } ​ ExceptionHandler(BusinessException.class) public ResponseEntityApiResponseVoid handleBusiness(BusinessException exception) { HttpStatus status exception.getCode() ErrorCode.USER_NOT_FOUND ? HttpStatus.NOT_FOUND : HttpStatus.CONFLICT; return ResponseEntity.status(status).body(ApiResponse.failure( exception.getCode().name(), exception.getMessage(), null, traceId())); } ​ ExceptionHandler(Exception.class) public ResponseEntityApiResponseVoid handleUnexpected(Exception exception) { log.error(Unhandled request exception, exception); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body( ApiResponse.failure(ErrorCode.INTERNAL_ERROR.name(), internal server error, null, traceId())); } ​ private String traceId() { return MDC.get(traceId); } }最后的兜底处理器必须记录完整异常但响应只返回受控信息。把 SQL、类名或堆栈发给客户端既不稳定也可能泄露系统细节。5. 给每次请求添加 traceIdMDC 会把 traceId 带入同一线程产生的日志。由于线程池会复用线程清理 MDC 是必需步骤。import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.UUID; import org.slf4j.MDC; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; ​ Component Order(Ordered.HIGHEST_PRECEDENCE) public class TraceIdFilter extends OncePerRequestFilter { private static final String TRACE_ID traceId; ​ Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String traceId UUID.randomUUID().toString().replace(-, ); MDC.put(TRACE_ID, traceId); response.setHeader(X-Trace-Id, traceId); try { filterChain.doFilter(request, response); } finally { MDC.remove(TRACE_ID); } } }在 Logback pattern 中加入%X{traceId:-no-trace}即可打印该值。分布式系统中应优先接入 OpenTelemetry 等追踪方案并遵循统一的 trace context而不是让每个服务各自生成互不关联的 ID。6. 用接口测试锁定行为下面的测试验证三个关键契约HTTP 状态、稳定错误码和字段级错误信息。import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; ​ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; ​ WebMvcTest(UserController.class) Import({GlobalExceptionHandler.class, TraceIdFilter.class}) class UserControllerTest { Autowired private MockMvc mockMvc; ​ MockBean private UserService userService; ​ Test void shouldRejectInvalidEmail() throws Exception { mockMvc.perform(post(/api/users) .contentType(MediaType.APPLICATION_JSON) .content( {name:Alice,email:bad-email,age:20} )) .andExpect(status().isBadRequest()) .andExpect(jsonPath($.code).value(INVALID_ARGUMENT)) .andExpect(jsonPath($.data.email).value(email format is invalid)) .andExpect(jsonPath($.traceId).isNotEmpty()); } ​ Test void shouldCreateUser() throws Exception { when(userService.create(any())).thenReturn( new UserView(1L, Alice, aliceexample.com, 20)); ​ mockMvc.perform(post(/api/users) .contentType(MediaType.APPLICATION_JSON) .content( {name:Alice,email:aliceexample.com,age:20} )) .andExpect(status().isCreated()) .andExpect(jsonPath($.code).value(OK)) .andExpect(jsonPath($.data.id).value(1)); } }Service 还应单独测试业务分支涉及数据库约束时再增加包含真实数据库行为的集成测试。只依赖 MockMvc 无法发现 SQL、事务和数据库方言问题。7. 上线前检查清单HTTP 状态码与业务错误码各司其职错误码含义稳定。DTO 使用jakarta.validationController 参数确实添加了Valid。未知异常记录堆栈但响应不暴露内部实现。日志包含 traceId过滤器和异步任务都会清理 MDC。API 测试覆盖成功、校验失败、业务冲突和未知异常。时间、分页、空值和金额等字段有明确的序列化约定。总结REST API 的工程质量来自一致的边界DTO 负责输入约束Service 负责业务规则异常处理器负责协议映射traceId 负责定位请求测试负责锁定契约。这套骨架并不复杂却能显著减少重复代码也让后续增加鉴权、审计和链路追踪时有清晰的落点。

相关新闻

SpringBoot+Vue图书商城系统开发实践

SpringBoot+Vue图书商城系统开发实践

1. 项目概述这个图书商城管理系统是我去年为一个高校图书馆开发的线上借阅平台,采用前后端分离架构,后端基于SpringBootMyBatisMySQL技术栈,前端使用Vue.js框架。系统上线后日均访问量稳定在3000,成功替代了原有的手工登记模式。提…

2026/8/4 6:28:36 阅读更多 →
Docker Commit实战:从零定制镜像,快速掌握容器化部署

Docker Commit实战:从零定制镜像,快速掌握容器化部署

1. 项目概述:为什么需要定制自己的Docker镜像?在之前的Docker入门教程里,我们学会了如何拉取和使用现成的官方镜像,比如nginx:latest或者ubuntu:20.04。这就像去超市买预制菜,方便快捷,开袋即用。但实际工作…

2026/8/4 6:28:35 阅读更多 →
Claude会话隔离实战:实现AI代码审查与独立项目咨询的纯净环境

Claude会话隔离实战:实现AI代码审查与独立项目咨询的纯净环境

1. 为什么需要让Claude“从零思考”?如果你用过Claude一段时间,可能会发现一个有趣的现象:当你开启一个新对话,想让它帮你分析一段全新的代码时,它有时会突然冒出一句“根据我们之前的讨论,这里是不是应该……

2026/8/4 6:27:35 阅读更多 →

最新新闻

从零构建AI智能体:基于LangChain的ReAct模式实战指南

从零构建AI智能体:基于LangChain的ReAct模式实战指南

在实际 AI 项目开发中,我们常常遇到这样的困境:大语言模型(LLM)本身能力强大,能说会道,但让它独立完成一个复杂的、多步骤的任务时,却常常表现得像个“健忘的专家”——它可能忘记上一步的指令&…

2026/8/4 7:12:54 阅读更多 →
CNN新闻精听法:10分钟高效提升英语听力的系统方案

CNN新闻精听法:10分钟高效提升英语听力的系统方案

之前为了提升英文听力,尝试过各种方法,从泛听到精听,效果总是不尽如人意,要么材料太枯燥坚持不下去,要么难度不合适打击信心。直到我开始尝试每天坚持听10分钟CNN新闻,并配合一套系统的方法,听力…

2026/8/4 7:12:54 阅读更多 →
企业级网络安全纵深防御体系设计与实践

企业级网络安全纵深防御体系设计与实践

1. 企业级网络安全纵深防御方案设计概述企业级网络安全纵深防御(Defense in Depth)不是简单的安全产品堆砌,而是一套基于风险管理的动态防护体系。我在为多家金融和互联网企业设计安全方案时发现,90%的安全事件都源于防御层次单一…

2026/8/4 7:12:54 阅读更多 →
DVWA靶场XSS攻防实战:从反射型漏洞到安全编码的思维演进

DVWA靶场XSS攻防实战:从反射型漏洞到安全编码的思维演进

1. 项目概述:一次完整的XSS攻防思维训练最近在带新人做安全测试的入门训练,我总会把DVWA靶场的XSS(Reflected)关卡作为第一个实战点。这不仅仅是因为它经典,更因为从Low到Impossible的四个难度等级,完美地勾…

2026/8/4 7:12:54 阅读更多 →
从XML数据解析到XSS防御:前端安全实战指南

从XML数据解析到XSS防御:前端安全实战指南

1. 项目概述:从游戏到实战的XSS防御思维最近在玩一个叫“Secure Code Game”的编程安全游戏,里面有个叫“Planet XMLon”的关卡,专门考验开发者对XSS(跨站脚本攻击)的防御能力。这让我想起了很多新手,甚至是…

2026/8/4 7:12:54 阅读更多 →
UE5崩溃排查实战指南:从访问违规到内存泄漏的完整解决方案

UE5崩溃排查实战指南:从访问违规到内存泄漏的完整解决方案

1. 项目概述:UE5崩溃,开发者绕不开的“坎”如果你正在用虚幻引擎5(UE5)做项目,无论是独立游戏、影视动画还是数字孪生,那么“崩溃”这个词对你来说绝对不陌生。它就像一个不请自来的访客,在你最…

2026/8/4 7:11:54 阅读更多 →

日新闻

AI Agent白手起家26: 使用标准事件驱动大模型实践

AI Agent白手起家26: 使用标准事件驱动大模型实践

纲要 练习目标:掌握大模型标准事件的调用回顾 LangChain 中的核心标准事件 invokestreambatchastream_eventswith_structured_output 环境准备实战代码:多种事件调用对比 同步调用与流式输出批量处理异步事件流监听结构化输出 运行说明与预期结果总结与扩…

2026/8/4 0:00:40 阅读更多 →
dealsea是什么?跨境卖家必知的美国deal站入门指南

dealsea是什么?跨境卖家必知的美国deal站入门指南

说实话,第一次听说美国这个老牌折扣网站的跨境卖家,十个有八个会问同一个问题:这个平台到底是干嘛的?我见过一个做家居出口的朋友,他在亚马逊上月销二十万美金,却从来没用过它。我给他看了首页——一屏一屏…

2026/8/4 0:01:40 阅读更多 →
清华大学重磅EST:植物自导电闪蒸焦耳热600°C/2600°C两步法!稀土超积累植物秒级转化为CeO₂-石墨烯电催化剂!

清华大学重磅EST:植物自导电闪蒸焦耳热600°C/2600°C两步法!稀土超积累植物秒级转化为CeO₂-石墨烯电催化剂!

通讯作者:邓兵、刘建国通讯单位:清华大学DOI:https://doi.org/10.1021/acs.est.6c00603研究背景稀土元素(REEs)是清洁能源技术与电子器件不可或缺的核心原料,然而传统提取方式依赖能耗高、排放大的采矿与强…

2026/8/4 0:01:40 阅读更多 →

周新闻

最大流算法详解:从水管网络到Ford-Fulkerson与Dinic实战

最大流算法详解:从水管网络到Ford-Fulkerson与Dinic实战

1. 从水管网络到最大流:一个核心问题的诞生想象一下,你是一个城市供水系统的总工程师。你的城市有多个水源(水库),需要通过一个复杂的地下管道网络,将水输送到各个居民区。每条管道都有其最大通水能力&…

2026/8/3 4:58:13 阅读更多 →
基于Springboot的企业门户网站(源码+LW+调试文档+讲解)

基于Springboot的企业门户网站(源码+LW+调试文档+讲解)

温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台…

2026/8/3 1:53:31 阅读更多 →
MATLAB xcorr函数详解:从互相关原理到四大实战应用

MATLAB xcorr函数详解:从互相关原理到四大实战应用

1. 从一次信号“找茬”说起:为什么我们需要互相关几年前,我在处理一组声学传感器数据时遇到了一个棘手的问题。我有两个麦克风记录了一段相同的音频信号,理论上它们接收到的声音波形应该非常相似,只是由于麦克风位置不同&#xff…

2026/8/4 5:26:40 阅读更多 →

月新闻

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南 【免费下载链接】BaiduNetdiskPlugin-macOS For macOS.百度网盘 破解SVIP、下载速度限制~ 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduNetdiskPlugin-macOS 还在为百度网盘macOS版的龟速下…

2026/8/3 13:07:03 阅读更多 →
终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 还在为网易云音乐下载的NCM格式文件无法在其他播放器播放而烦恼吗?ncmdump解密工具帮你轻松解决这个困…

2026/8/3 5:19:38 阅读更多 →
HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

AgentCard 智能体卡片:为英语学习 App 打造桌面级学习助手适用平台:HarmonyOS 7.0 (API 26 Beta)一、引言 HarmonyOS 7.0(API 26 Beta)新增了 AgentCard 智能体卡片能力,这是继 HMAF(鸿蒙智能体框架&#x…

2026/8/3 8:27:36 阅读更多 →