最佳实践
本章是 Spring MVC 教程的收官章节。经过前面七个章节的学习,你已经掌握了 DispatcherServlet 调度、请求映射、参数绑定、视图解析、拦截器、异常处理等全部核心机制。但知道机制不等于能写出好代码。本章将把这些散点知识整合为一套可落地的工程规范,涵盖统一响应格式、全局异常架构、RESTful 设计准则和前后端分离配置要点。遵循这些实践,你的项目才能在团队协作中保持清晰、在运维排查中保持高效、在面试答辩中展现专业深度。
定义与作用
Spring MVC 最佳实践不是某一条代码规则,而是一套覆盖 API 设计、异常处理、响应规范、跨域配置、静态资源管理的系统性工程准则。它的目标是:
- 对前端:提供一致的响应格式,消除"有时返回 JSON、有时返回 HTML、有时返回空白"的混乱
- 对后端:建立清晰的分层异常体系,让业务代码只关注业务,错误处理由全局架构接管
- 对运维:通过标准化的日志和状态码,快速定位问题层级(客户端错误 / 业务错误 / 系统故障)
- 对团队:用约定代替争论,新成员能在 30 分钟内理解项目的 Web 层规范
核心原理
统一响应格式:给前端一个可预期的契约
前后端分离项目中,前端需要解析每一个响应体。如果不同接口的返回结构各不相同,前端就要写大量防御性代码。统一响应格式是API 契约的底线。
统一响应体实现(Spring Boot 2.x):
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse<T> {
private Integer code;
private String message;
private T data;
private Long timestamp;
private String traceId;
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(
ResultCode.SUCCESS.getCode(),
ResultCode.SUCCESS.getMessage(),
data,
System.currentTimeMillis(),
MDC.get("traceId")
);
}
public static <T> ApiResponse<T> error(ResultCode resultCode, String message) {
return new ApiResponse<>(
resultCode.getCode(),
message != null ? message : resultCode.getMessage(),
null,
System.currentTimeMillis(),
MDC.get("traceId")
);
}
}
public enum ResultCode {
SUCCESS(200, "成功"),
BAD_REQUEST(400, "请求参数错误"),
UNAUTHORIZED(401, "未授权"),
FORBIDDEN(403, "禁止访问"),
NOT_FOUND(404, "资源不存在"),
BUSINESS_ERROR(1000, "业务异常"),
SYSTEM_ERROR(500, "系统内部错误");
private final int code;
private final String message;
ResultCode(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() { return code; }
public String getMessage() { return message; }
}
关键设计决策:
code使用业务状态码(200/400/1000),与 HTTP 状态码解耦,方便前端做细粒度错误处理traceId用于分布式链路追踪,李眉在排查生产问题时可以通过一个 ID 串联所有日志timestamp让前端能判断响应是否过期(如缓存场景)
全局异常处理架构
业务代码中不应该出现 try-catch 块包裹业务逻辑的情况。所有异常应该由 @RestControllerAdvice 统一捕获、分类、封装。
全局异常处理器实现:
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
// 1. 参数校验异常(JSR-303 + @Valid)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResponse<Void> handleValidationException(MethodArgumentNotValidException e) {
List<String> errors = e.getBindingResult().getFieldErrors().stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.toList());
log.warn("参数校验失败: {}", errors);
return ApiResponse.error(ResultCode.BAD_REQUEST, String.join("; ", errors));
}
// 2. 参数绑定异常(类型转换失败、必填参数缺失)
@ExceptionHandler({BindException.class, MethodArgumentTypeMismatchException.class})
public ApiResponse<Void> handleBindException(Exception e) {
log.warn("参数绑定失败: {}", e.getMessage());
return ApiResponse.error(ResultCode.BAD_REQUEST, "请求参数格式错误");
}
// 3. 业务异常(由业务层主动抛出)
@ExceptionHandler(BusinessException.class)
public ApiResponse<Void> handleBusinessException(BusinessException e) {
log.info("业务异常: code={}, message={}", e.getResultCode().getCode(), e.getMessage());
return ApiResponse.error(e.getResultCode(), e.getMessage());
}
// 4. 系统异常(数据库连接失败、第三方服务超时等)
@ExceptionHandler(SystemException.class)
public ApiResponse<Void> handleSystemException(SystemException e) {
log.error("系统异常: {}", e.getMessage(), e);
// 可集成钉钉/企业微信告警
alertService.sendAlert(e);
return ApiResponse.error(ResultCode.SYSTEM_ERROR, "系统繁忙,请稍后重试");
}
// 5. 兜底异常(未预期的任何异常)
@ExceptionHandler(Exception.class)
public ApiResponse<Void> handleException(Exception e) {
log.error("未预期异常: {}", e.getMessage(), e);
return ApiResponse.error(ResultCode.SYSTEM_ERROR, "系统内部错误");
}
}
异常分层定义:
// 业务异常:用户可理解的错误,如"库存不足"、"订单已取消"
public class BusinessException extends RuntimeException {
private final ResultCode resultCode;
public BusinessException(ResultCode resultCode, String message) {
super(message);
this.resultCode = resultCode;
}
public ResultCode getResultCode() { return resultCode; }
}
// 系统异常:用户不应感知的底层故障,如"数据库连接超时"
public class SystemException extends RuntimeException {
public SystemException(String message, Throwable cause) {
super(message, cause);
}
}
// 参数异常:参数校验失败,通常由 @Valid 自动触发
public class ParamException extends BusinessException {
public ParamException(String message) {
super(ResultCode.BAD_REQUEST, message);
}
}
RESTful API 设计规范
RESTful 不是"用 URL 做动词",而是用 HTTP 协议的原生语义表达资源操作。
| 检查项 | 规范 | 反例 | 正例 |
|---|---|---|---|
| URL 命名 | 名词复数,小写,连字符分隔 | /getUserInfo /user_list | /users /order-items |
| HTTP 方法 | GET 查询、POST 创建、PUT 全量更新、PATCH 局部更新、DELETE 删除 | POST /users/delete | DELETE /users/{id} |
| 状态码 | 2xx 成功、4xx 客户端错误、5xx 服务端错误 | 所有响应都返回 200 | 参数错误返回 400,业务错误返回 200 但 code ≠ 200 |
| 路径参数 | 用于资源定位(ID) | /users?id=123(GET 用查询参数除外) | /users/123 |
| 查询参数 | 用于过滤、排序、分页 | /users/page/1/size/20 | /users?page=1&size=20 |
| 响应体 | 统一格式,包含自描述信息 | 直接返回裸数组 [] | 返回 ApiResponse 包装体 |
| 版本控制 | URL 路径或请求头 | 无版本控制 | /api/v1/users 或 Accept: application/vnd.feixiang.v1+json |
飞翔科技 RESTful 接口示例:
@RestController
@RequestMapping("/api/v1/warehouses")
public class WarehouseController {
// 查询仓库列表(支持过滤和分页)
@GetMapping
public ApiResponse<Page<Warehouse>> list(
@RequestParam(required = false) String city,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
return ApiResponse.success(warehouseService.list(city, PageRequest.of(page, size)));
}
// 查询单个仓库
@GetMapping("/{warehouseId}")
public ApiResponse<WarehouseDetail> getById(@PathVariable String warehouseId) {
return ApiResponse.success(warehouseService.findById(warehouseId));
}
// 创建仓库
@PostMapping
public ApiResponse<Warehouse> create(@Valid @RequestBody WarehouseCreateRequest request) {
return ApiResponse.success(warehouseService.create(request));
}
// 全量更新仓库信息
@PutMapping("/{warehouseId}")
public ApiResponse<Warehouse> update(
@PathVariable String warehouseId,
@Valid @RequestBody WarehouseUpdateRequest request) {
return ApiResponse.success(warehouseService.update(warehouseId, request));
}
// 局部更新:仅更新仓库状态
@PatchMapping("/{warehouseId}/status")
public ApiResponse<Warehouse> updateStatus(
@PathVariable String warehouseId,
@RequestParam WarehouseStatus status) {
return ApiResponse.success(warehouseService.updateStatus(warehouseId, status));
}
// 删除仓库
@DeleteMapping("/{warehouseId}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable String warehouseId) {
warehouseService.delete(warehouseId);
}
}
前后端分离项目的 MVC 配置要点
前后端分离意味着 Spring MVC 不再负责视图渲染,而是纯粹作为 API 网关。配置策略需要相应调整:
@Configuration
public class WebConfig implements WebMvcConfigurer {
// 1. CORS 配置:允许前端开发服务器访问
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:3000", "https://app.feixiang.tech")
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
// 2. 静态资源:仅保留 Swagger / Actuator 等必要资源
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/swagger-ui/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/");
}
// 3. 拦截器白名单:登录、注册、Swagger 等接口放行
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthInterceptor())
.addPathPatterns("/api/**")
.excludePathPatterns(
"/api/v1/auth/login",
"/api/v1/auth/register",
"/api/v1/auth/refresh",
"/swagger-ui/**",
"/v3/api-docs/**"
);
}
}
完整示例
场景
飞翔科技的仓储管理系统即将上线。大翔作为 CTO,在上线前评审中提出三个硬性要求:
- 前端黄俪说:"如果后端返回格式不统一,我的错误处理代码要写 20 套"
- 运维李眉说:"生产环境出问题,我需要 10 秒内判断是前端传错参数还是数据库挂了"
- 架构师白歌说:"所有 Controller 方法里不许出现 try-catch,业务异常直接抛"
小崔负责落实这套规范。
统一响应格式落地
// 所有 Controller 返回 ApiResponse
@RestController
@RequestMapping("/api/v1/inventory")
public class InventoryController {
@GetMapping("/items/{itemId}")
public ApiResponse<ItemDetail> getItem(@PathVariable String itemId) {
ItemDetail item = inventoryService.findById(itemId);
if (item == null) {
// 直接抛业务异常,由 GlobalExceptionHandler 处理
throw new BusinessException(ResultCode.NOT_FOUND, "商品不存在: " + itemId);
}
return ApiResponse.success(item);
}
}
全局异常处理落地
@Slf4j
@RestControllerAdvice(basePackages = "com.feixiang.api")
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiResponse<Void>> handleBusiness(BusinessException e) {
ApiResponse<Void> response = ApiResponse.error(e.getResultCode(), e.getMessage());
// 业务异常返回 200 OK,但 body 中 code 标识错误
return ResponseEntity.ok(response);
}
@ExceptionHandler(SystemException.class)
public ResponseEntity<ApiResponse<Void>> handleSystem(SystemException e) {
log.error("系统异常", e);
ApiResponse<Void> response = ApiResponse.error(
ResultCode.SYSTEM_ERROR, "系统繁忙,请稍后重试"
);
// 系统异常返回 500,触发 Nginx 错误页或告警
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}
}
白歌的架构决策说明:
- 业务异常返回 HTTP 200:因为请求本身到达了服务端,只是业务条件不满足(如库存不足)。前端不需要重试,只需要提示用户
- 系统异常返回 HTTP 500:因为服务端自身故障,前端可能需要降级或重试。Nginx 和监控工具也会根据 5xx 状态码触发告警
RESTful 设计检查清单
白歌要求每个新接口在 Code Review 时必须通过以下检查:
| 检查项 | 通过标准 | 检查人 |
|---|---|---|
| URL 是否使用名词复数 | /warehouses 而非 /getWarehouse | 白歌 |
| HTTP 方法是否符合语义 | 创建用 POST,更新用 PUT/PATCH | 白歌 |
| 是否返回统一响应体 | 所有响应均为 ApiResponse<T> | 黄俪(前端验收) |
| 异常是否由全局处理器接管 | Controller 中无 try-catch | 白歌 |
| 参数校验是否使用 @Valid | 复杂对象必须标注 @Valid | 小崔 |
| 分页参数是否统一 | page + size,默认 size ≤ 100 | 小崔 |
| 敏感操作是否有日志 | DELETE / PUT 记录操作日志 | 李眉 |
易错场景与面试考点
误区一:统一响应格式就是"所有情况都返回 200"
错误认知:为了前端方便,所有响应都返回 HTTP 200,错误信息放在 body 的 code 字段里。
纠正:HTTP 状态码和 body 中的 code 承担不同职责。HTTP 状态码描述传输层结果(请求是否成功到达、服务端是否理解请求),body 中的 code 描述业务层结果(业务条件是否满足)。混淆两者会导致:
- 缓存服务器错误缓存 4xx 响应(因为返回了 200)
- 负载均衡器无法根据 5xx 自动剔除故障节点
- 监控告警规则失效
// ❌ 错误:参数错误也返回 200
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<Void>> handleValidationError() {
return ResponseEntity.ok(ApiResponse.error(ResultCode.BAD_REQUEST, "参数错误"));
}
// ✅ 正确:参数错误返回 400,业务错误返回 200,系统错误返回 500
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<Void>> handleValidationError() {
return ResponseEntity.badRequest()
.body(ApiResponse.error(ResultCode.BAD_REQUEST, "参数错误"));
}
误区二:@RestControllerAdvice 不加 basePackages 导致副作用
错误认知:一个 @RestControllerAdvice 就能处理整个应用的异常。
纠正:中大型项目中,可能同时存在 API 模块、Admin 模块、定时任务模块。不加 basePackages 会导致 Admin 后台的异常也被 API 的全局处理器拦截,返回 JSON 而不是 HTML 错误页。
// ❌ 错误:影响所有 Controller
@RestControllerAdvice
public class GlobalExceptionHandler { ... }
// ✅ 正确:限定只处理 API 包下的 Controller
@RestControllerAdvice(basePackages = "com.feixiang.api")
public class ApiExceptionHandler { ... }
@RestControllerAdvice(basePackages = "com.feixiang.admin")
public class AdminExceptionHandler { ... }
面试高频:如何设计一个健壮的 Spring MVC 全局异常处理方案?
标准回答:
- 异常分层:定义
BusinessException(用户可理解)、SystemException(用户不应感知)、ParamException(参数校验失败)三层异常体系 - 全局拦截:用
@RestControllerAdvice+@ExceptionHandler按异常类型分类处理,Controller 中不写 try-catch - 响应规范:统一
ApiResponse<T>结构,包含 code / message / data / timestamp / traceId - 状态码分离:HTTP 状态码描述传输结果(2xx/4xx/5xx),body 中的 code 描述业务结果
- 日志策略:参数错误记 WARN(可定位但无需紧急处理),业务错误记 INFO(正常业务分支),系统错误记 ERROR(触发告警)
- 模块隔离:通过
basePackages让不同模块有独立的异常处理器,避免 API 和 Admin 互相干扰
小结
Spring MVC 的最佳实践不是锦上添花,而是工程化开发的底线。统一响应格式消除了前后端的协作摩擦,全局异常架构让业务代码保持纯净,RESTful 规范让 API 具备自描述性,CORS 和拦截器白名单让前后端分离项目安全可控。这些实践共同构成了一套可落地、可检查、可演进的 Web 层工程规范。
本章与全局的关系:本章是教程的终点,也是你实际项目的起点。前面所有章节讲解的 DispatcherServlet、HandlerMapping、WebDataBinder、ViewResolver、Interceptor 等机制,在这里汇聚为一套完整的工程方案。建议你以本章为模板,结合团队实际情况,制定一份属于你们项目的《Spring MVC 开发规范》,并在 Code Review 中强制执行。