乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 SpringMVC概述与DispatcherServlet

    • 本章导读:Spring MVC概述与DispatcherServlet
    • Spring MVC 是什么
    • MVC 设计模式
    • 前端控制器模式
    • DispatcherServlet
    • 核心组件协作
  • 第2章 控制器与请求映射

    • 本章导读:控制器与请求映射
    • Controller
    • RestController
    • RequestMapping
    • GetMapping
    • PostMapping
    • PutMapping
    • DeleteMapping
    • PathVariable
    • RESTful
    • 请求映射原理
  • 第3章 请求参数获取与转换

    • 本章导读:请求参数获取与转换
    • RequestParam
    • RequestBody
    • RequestHeader
    • CookieValue
    • Model
    • ModelAttribute
    • 数据绑定原理
    • 数据校验
  • 第4章 响应数据与视图解析

    • 本章导读:响应数据与视图解析
    • ResponseBody
    • ResponseEntity
    • ModelAndView
    • ViewResolver
    • HttpMessageConverter
    • forward与redirect
  • 第5章 拦截器过滤器与跨域

    • 本章导读:拦截器、过滤器与跨域
    • HandlerInterceptor
    • WebMvcConfigurer
    • CrossOrigin
    • 登录验证实战
  • 第6章 文件上传与异常处理

    • 本章导读:文件上传与异常处理
    • MultipartFile
    • 文件下载
    • ExceptionHandler
    • ControllerAdvice
    • RestControllerAdvice
    • ResponseStatus
  • 第7章 高级特性与最佳实践

    • 本章导读:高级特性与最佳实践
    • SessionAttributes
    • SessionAttribute
    • RedirectAttributes
    • MockMvc测试
    • 国际化
    • 最佳实践
  • 第8章 扩展与异步机制

    • 本章导读:扩展与异步机制
    • 异步请求处理
    • 自定义参数解析器
    • 内容协商

ResponseStatus

本章聚焦 Spring MVC 的HTTP 状态码映射机制。前面章节讲解了如何捕获异常并返回错误响应,但状态码的设置目前依赖 ResponseEntity.status() 或 @ResponseStatus 标注在异常处理方法上。如果能在异常定义时就声明它对应的 HTTP 状态码,代码会更简洁、语义更清晰。@ResponseStatus 正是为此而生的状态码映射注解。

本章与全局的关系:前面章节讲解了异常捕获和响应体构造,本章讲解异常到 HTTP 状态码的自动映射。三者结合,构成完整的异常处理体系:@RestControllerAdvice 捕获异常 → @ResponseStatus 映射状态码 → 统一响应体返回错误详情。


定义与作用

@ResponseStatus 是 Spring MVC 提供的HTTP 状态码映射注解,有两种使用方式:

  1. 标注在自定义异常类上:当该异常被抛出时,Spring MVC 自动把响应状态码设置为注解指定的值
  2. 标注在 @ExceptionHandler 方法上:覆盖该方法处理异常时的默认状态码

核心作用:让异常本身携带 HTTP 语义,减少异常处理代码中的状态码硬编码。

生活类比:公司内部的故障等级制度

想象飞翔科技的运维体系:

  • 网络中断(500)—— 严重故障,服务完全不可用
  • 参数错误(400)—— 用户操作问题,需要修正请求
  • 资源不存在(404)—— 正常的业务状态,告知用户即可
  • 权限不足(403)—— 安全拦截,拒绝访问

@ResponseStatus 就像给每种故障贴上一个"等级标签":

  • 当"网络中断异常"被抛出时,标签自动告诉前端"这是 500 错误"
  • 当"参数非法异常"被抛出时,标签自动告诉前端"这是 400 错误"
  • 异常处理代码不再需要手动判断"这是什么异常、该返回什么状态码"

关键认知:@ResponseStatus 是声明式状态码映射,不是异常捕获机制。它只负责设置 HTTP 状态码,不负责捕获异常。异常仍然需要被 @ExceptionHandler 或 @ControllerAdvice 捕获处理。


核心原理

异常到状态码映射流程

两种生效位置:

覆盖规则:@ExceptionHandler 方法上的 @ResponseStatus 覆盖异常类上的 @ResponseStatus。


适用位置与常用属性

@ResponseStatus 可以标注在异常类或方法上:

属性类型说明
value / codeHttpStatusHTTP 状态码,如 HttpStatus.NOT_FOUND
reasonString状态码原因短语,如 "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
}

流程解析:

  1. getEmployee(999) 抛出 EmployeeNotFoundException
  2. 异常类上标注 @ResponseStatus(HttpStatus.NOT_FOUND)
  3. @ExceptionHandler 捕获异常,从注解提取状态码 404
  4. 返回 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 的异常处理由三层构成:

  1. 局部层:Controller 内的 @ExceptionHandler,处理该 Controller 的特定异常
  2. 全局层:@ControllerAdvice / @RestControllerAdvice + @ExceptionHandler,处理所有 Controller 的通用异常
  3. 状态码层:@ResponseStatus 标注在异常类上,声明异常对应的 HTTP 状态码

三层配合,实现"异常定义 → 状态码映射 → 全局捕获 → 统一响应"的完整闭环。


小结

@ResponseStatus 是 Spring MVC 的HTTP 状态码映射注解,标注在自定义异常类上时,为该异常声明固定的 HTTP 语义;标注在 @ExceptionHandler 方法上时,覆盖默认状态码。它只负责状态码映射,不负责异常捕获,必须与 @ExceptionHandler 配合使用。在 RESTful 项目中,配合自定义业务异常和 @RestControllerAdvice,可以实现语义清晰、代码简洁的异常处理体系。

本章与全局的关系:本章完成了异常处理体系的最后一环——状态码映射。至此,Spring MVC 教程的异常处理章节形成了完整闭环:局部 @ExceptionHandler → 全局 @ControllerAdvice → RESTful 简化 @RestControllerAdvice → 状态码映射 @ResponseStatus。

上一页
RestControllerAdvice