ResponseStatus
本章聚焦 Spring MVC 的HTTP 状态码映射机制。前面章节讲解了如何捕获异常并返回错误响应,但状态码的设置目前依赖
ResponseEntity.status()或@ResponseStatus标注在异常处理方法上。如果能在异常定义时就声明它对应的 HTTP 状态码,代码会更简洁、语义更清晰。@ResponseStatus 正是为此而生的状态码映射注解。本章与全局的关系:前面章节讲解了异常捕获和响应体构造,本章讲解异常到 HTTP 状态码的自动映射。三者结合,构成完整的异常处理体系:@RestControllerAdvice 捕获异常 → @ResponseStatus 映射状态码 → 统一响应体返回错误详情。
定义与作用
@ResponseStatus 是 Spring MVC 提供的HTTP 状态码映射注解,有两种使用方式:
- 标注在自定义异常类上:当该异常被抛出时,Spring MVC 自动把响应状态码设置为注解指定的值
- 标注在 @ExceptionHandler 方法上:覆盖该方法处理异常时的默认状态码
核心作用:让异常本身携带 HTTP 语义,减少异常处理代码中的状态码硬编码。
生活类比:公司内部的故障等级制度
想象飞翔科技的运维体系:
- 网络中断(500)—— 严重故障,服务完全不可用
- 参数错误(400)—— 用户操作问题,需要修正请求
- 资源不存在(404)—— 正常的业务状态,告知用户即可
- 权限不足(403)—— 安全拦截,拒绝访问
@ResponseStatus 就像给每种故障贴上一个"等级标签":
- 当"网络中断异常"被抛出时,标签自动告诉前端"这是 500 错误"
- 当"参数非法异常"被抛出时,标签自动告诉前端"这是 400 错误"
- 异常处理代码不再需要手动判断"这是什么异常、该返回什么状态码"
关键认知:@ResponseStatus 是声明式状态码映射,不是异常捕获机制。它只负责设置 HTTP 状态码,不负责捕获异常。异常仍然需要被 @ExceptionHandler 或 @ControllerAdvice 捕获处理。
核心原理
异常到状态码映射流程
两种生效位置:
覆盖规则:@ExceptionHandler 方法上的 @ResponseStatus 覆盖异常类上的 @ResponseStatus。
适用位置与常用属性
@ResponseStatus 可以标注在异常类或方法上:
| 属性 | 类型 | 说明 |
|---|---|---|
value / code | HttpStatus | HTTP 状态码,如 HttpStatus.NOT_FOUND |
reason | String | 状态码原因短语,如 "Resource Not Found" |
标注位置
| 位置 | 作用 |
|---|---|
| 自定义异常类上 | 该异常被抛出时自动映射到指定状态码 |
| @ExceptionHandler 方法上 | 覆盖该方法的响应状态码 |
常见 HTTP 状态码使用场景
| 状态码 | 场景 | 对应异常示例 |
|---|---|---|
400 Bad Request | 请求参数格式错误、校验失败 | IllegalArgumentException |
401 Unauthorized | 未登录、认证失败 | AuthenticationException |
403 Forbidden | 已登录但权限不足 | AccessDeniedException |
404 Not Found | 资源不存在 | NoSuchElementException, EntityNotFoundException |
409 Conflict | 资源冲突(如重复创建) | DuplicateKeyException |
500 Internal Server Error | 服务器内部错误 | RuntimeException, IOException |
503 Service Unavailable | 服务暂时不可用 | ServiceUnavailableException |
完整示例
场景
飞翔科技员工管理系统需要定义一组业务异常,每个异常自带 HTTP 状态码语义。配合 @RestControllerAdvice 统一捕获,实现简洁的异常处理。
项目结构
employee-web/
├── src/main/java/
│ └── com/feixiang/web/
│ ├── controller/
│ │ └── EmployeeController.java
│ ├── exception/
│ │ ├── EmployeeNotFoundException.java
│ │ ├── InvalidParameterException.java
│ │ └── DuplicateEmployeeException.java
│ ├── advice/
│ │ └── GlobalExceptionHandler.java
│ └── dto/
│ └── ErrorResponse.java
自定义异常(标注 @ResponseStatus)
// EmployeeNotFoundException.java
package com.feixiang.web.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.NOT_FOUND)
public class EmployeeNotFoundException extends RuntimeException {
public EmployeeNotFoundException(String message) {
super(message);
}
}
// InvalidParameterException.java
package com.feixiang.web.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class InvalidParameterException extends RuntimeException {
public InvalidParameterException(String message) {
super(message);
}
}
// DuplicateEmployeeException.java
package com.feixiang.web.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.CONFLICT)
public class DuplicateEmployeeException extends RuntimeException {
public DuplicateEmployeeException(String message) {
super(message);
}
}
全局异常处理(简化版)
// GlobalExceptionHandler.java
package com.feixiang.web.advice;
import com.feixiang.web.dto.ErrorResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.bind.annotation.ResponseStatus;
import javax.servlet.http.HttpServletRequest;
@RestControllerAdvice(basePackages = "com.feixiang.web.controller")
public class GlobalExceptionHandler {
// 捕获所有带 @ResponseStatus 的自定义异常
@ExceptionHandler({
EmployeeNotFoundException.class,
InvalidParameterException.class,
DuplicateEmployeeException.class
})
public ResponseEntity<ErrorResponse> handleBusinessException(
RuntimeException ex, HttpServletRequest request) {
// 从异常类上的 @ResponseStatus 提取状态码
ResponseStatus statusAnnotation = ex.getClass().getAnnotation(ResponseStatus.class);
HttpStatus status = statusAnnotation != null ? statusAnnotation.value() : HttpStatus.INTERNAL_SERVER_ERROR;
ErrorResponse error = new ErrorResponse(
status.value(),
ex.getMessage(),
request.getRequestURI()
);
return ResponseEntity.status(status).body(error);
}
// 兜底
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleGenericException(Exception ex, HttpServletRequest request) {
return new ErrorResponse(500, "服务器内部错误", request.getRequestURI());
}
}
Controller
// EmployeeController.java
package com.feixiang.web.controller;
import com.feixiang.web.dto.EmployeeDTO;
import com.feixiang.web.exception.*;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/employees")
public class EmployeeController {
@GetMapping("/{id}")
public EmployeeDTO getEmployee(@PathVariable Long id) {
if (id <= 0) {
throw new InvalidParameterException("ID 必须大于0");
}
if (id == 999) {
throw new EmployeeNotFoundException("员工不存在: " + id);
}
EmployeeDTO dto = new EmployeeDTO();
dto.setName("张三");
dto.setDepartment("研发部");
return dto;
}
@PostMapping
public EmployeeDTO addEmployee(@RequestBody EmployeeDTO dto) {
if (dto.getName() == null || dto.getName().trim().isEmpty()) {
throw new InvalidParameterException("员工姓名不能为空");
}
// 模拟重复检查
if ("张三".equals(dto.getName())) {
throw new DuplicateEmployeeException("员工已存在: " + dto.getName());
}
dto.setName(dto.getName() + "-已保存");
return dto;
}
}
HTTP 请求示例 1:资源不存在(404)
$ curl -X GET http://localhost:8080/api/employees/999
响应:
< HTTP/1.1 404
< Content-Type: application/json
<
{
"code": 404,
"message": "员工不存在: 999",
"path": "/api/employees/999",
"timestamp": 1718000000000
}
流程解析:
getEmployee(999)抛出EmployeeNotFoundException- 异常类上标注
@ResponseStatus(HttpStatus.NOT_FOUND) @ExceptionHandler捕获异常,从注解提取状态码 404- 返回
ResponseEntity.status(404).body(error)
HTTP 请求示例 2:参数错误(400)
$ curl -X GET http://localhost:8080/api/employees/-1
响应:
< HTTP/1.1 400
< Content-Type: application/json
<
{
"code": 400,
"message": "ID 必须大于0",
"path": "/api/employees/-1",
"timestamp": 1718000000000
}
HTTP 请求示例 3:资源冲突(409)
$ curl -X POST http://localhost:8080/api/employees \
-H "Content-Type: application/json" \
-d '{"name":"张三","department":"研发部"}'
响应:
< HTTP/1.1 409
< Content-Type: application/json
<
{
"code": 409,
"message": "员工已存在: 张三",
"path": "/api/employees",
"timestamp": 1718000000000
}
@ExceptionHandler 方法上覆盖状态码
场景
架构师白歌要求:同一个 InvalidParameterException,在 API 层返回 400,但在某些特殊接口(如批量导入)中返回 422(Unprocessable Entity)。
@RestControllerAdvice
public class GlobalExceptionHandler {
// 默认处理:400
@ExceptionHandler(InvalidParameterException.class)
public ResponseEntity<ErrorResponse> handleInvalidParam(
InvalidParameterException ex, HttpServletRequest request) {
ErrorResponse error = new ErrorResponse(400, ex.getMessage(), request.getRequestURI());
return ResponseEntity.badRequest().body(error);
}
// 批量导入接口的特殊处理:覆盖为 422
@ExceptionHandler(InvalidParameterException.class)
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
public ErrorResponse handleBatchInvalidParam(
InvalidParameterException ex, HttpServletRequest request) {
return new ErrorResponse(422, "批量导入数据格式错误: " + ex.getMessage(), request.getRequestURI());
}
}
注意:上述代码中两个方法都处理同一种异常,实际需要通过
RequestCondition或更细粒度的限定来区分。这里仅演示 @ResponseStatus 的覆盖能力。
更实际的用法是直接在特定 Controller 的局部 @ExceptionHandler 上覆盖:
@RestController
@RequestMapping("/api/batch")
public class BatchImportController {
@ExceptionHandler(InvalidParameterException.class)
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) // 覆盖异常类上的 400
public ErrorResponse handleBatchError(InvalidParameterException ex) {
return new ErrorResponse(422, ex.getMessage(), "");
}
}
易错场景与面试考点
误区一:@ResponseStatus 标注后异常自动被捕获
错误认知:"我在自定义异常上加了 @ResponseStatus,抛出后 Spring 会自动返回错误响应。"
纠正:@ResponseStatus 只设置状态码,不捕获异常。如果异常未被 @ExceptionHandler 捕获,Spring Boot 会走默认错误处理(Whitelabel Error Page),此时状态码确实是 @ResponseStatus 指定的值,但响应体是 Spring Boot 的默认错误页面,不是自定义的 JSON。
正确配合:
// 1. 异常类上声明状态码
@ResponseStatus(HttpStatus.NOT_FOUND)
public class EmployeeNotFoundException extends RuntimeException { }
// 2. 必须有 @ExceptionHandler 捕获并构造响应体
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(EmployeeNotFoundException.class)
public ResponseEntity<ErrorResponse> handle(EmployeeNotFoundException ex) {
// 这里可以读取 ex.getClass().getAnnotation(ResponseStatus.class).value()
// 也可以直接硬编码,因为异常类已经声明了语义
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse(404, ex.getMessage(), ""));
}
}
误区二:@ResponseStatus 的 reason 属性被忽略
代码示例:
@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "员工不存在")
public class EmployeeNotFoundException extends RuntimeException { }
现象:如果异常未被 @ExceptionHandler 捕获,Spring Boot 默认错误页面会显示 "员工不存在"。但如果被 @ExceptionHandler 捕获,reason 属性不会被自动使用,需要手动从注解中读取。
纠正:reason 主要用于未被捕获时的默认错误展示。在自定义 @ExceptionHandler 中,建议从异常 message 或注解中主动提取:
ResponseStatus annotation = ex.getClass().getAnnotation(ResponseStatus.class);
String reason = annotation != null ? annotation.reason() : "未知错误";
误区三:在普通方法上使用 @ResponseStatus
代码示例:
@GetMapping("/{id}")
@ResponseStatus(HttpStatus.OK) // 冗余!200 是默认状态码
public EmployeeDTO getEmployee(@PathVariable Long id) {
return employeeService.findById(id);
}
纠正:成功请求的默认状态码就是 200,无需显式标注。@ResponseStatus 的价值在于非 200 状态码的声明式映射,尤其是异常场景。正常业务方法上标注 @ResponseStatus(HttpStatus.OK) 是冗余代码。
面试高频:@ResponseStatus 和 ResponseEntity 的选择
| 场景 | 推荐方式 |
|---|---|
| 异常类自带状态码语义 | @ResponseStatus 标注在异常类上 |
| 运行时动态决定状态码 | ResponseEntity.status(...) |
| 需要完整控制头、体、状态码 | ResponseEntity |
| 简单场景,固定状态码 | @ResponseStatus |
标准回答:如果状态码与异常类型是固定映射关系(如"资源不存在 = 404"),用 @ResponseStatus 标注在异常类上,语义清晰、代码简洁。如果状态码需要在运行时根据业务逻辑判断(如"根据库存数量决定 200 或 202"),用 ResponseEntity 编程式控制。
面试高频:Spring MVC 异常处理完整体系
标准回答:Spring MVC 的异常处理由三层构成:
- 局部层:Controller 内的
@ExceptionHandler,处理该 Controller 的特定异常 - 全局层:
@ControllerAdvice/@RestControllerAdvice+@ExceptionHandler,处理所有 Controller 的通用异常 - 状态码层:
@ResponseStatus标注在异常类上,声明异常对应的 HTTP 状态码
三层配合,实现"异常定义 → 状态码映射 → 全局捕获 → 统一响应"的完整闭环。
小结
@ResponseStatus 是 Spring MVC 的HTTP 状态码映射注解,标注在自定义异常类上时,为该异常声明固定的 HTTP 语义;标注在 @ExceptionHandler 方法上时,覆盖默认状态码。它只负责状态码映射,不负责异常捕获,必须与 @ExceptionHandler 配合使用。在 RESTful 项目中,配合自定义业务异常和 @RestControllerAdvice,可以实现语义清晰、代码简洁的异常处理体系。
本章与全局的关系:本章完成了异常处理体系的最后一环——状态码映射。至此,Spring MVC 教程的异常处理章节形成了完整闭环:局部 @ExceptionHandler → 全局 @ControllerAdvice → RESTful 简化 @RestControllerAdvice → 状态码映射 @ResponseStatus。