1. 概述

本教程将深入探讨 Spring 5 引入的 ResponseStatusException 类。这个类的主要作用是为 HTTP 响应设置状态码。

在 RESTful 应用中,通过在响应中返回正确的 HTTP 状态码,可以清晰地向客户端传达请求处理的成功或失败。简单来说,合适的 HTTP 状态码能帮助客户端快速定位请求处理过程中可能出现的问题。

2. @ResponseStatus 注解

在深入 ResponseStatusException 之前,我们先快速回顾一下 @ResponseStatus 注解。这个注解自 Spring 3 起就用于为 HTTP 响应设置状态码。

我们可以用 @ResponseStatus 注解来设置响应的状态码和原因:

@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "Actor Not Found")
public class ActorNotFoundException extends Exception {
    // ...
}

当处理 HTTP 请求时抛出此异常,响应会自动包含注解中指定的 HTTP 状态码。

这种方式的缺点是会造成异常与状态码的强耦合。例如,所有 ActorNotFoundException 异常都会在响应中生成相同的错误消息和状态码。

3. ResponseStatusException

ResponseStatusException@ResponseStatus 的编程式替代方案,也是用于设置 HTTP 响应状态码的异常基类。它是一个 RuntimeException,因此不需要在方法签名中显式声明。

Spring 提供了三个构造函数来创建 ResponseStatusException

ResponseStatusException(HttpStatus status)
ResponseStatusException(HttpStatus status, java.lang.String reason)
ResponseStatusException(
  HttpStatus status, 
  java.lang.String reason, 
  java.lang.Throwable cause
)

构造参数说明:

  • status - 设置到 HTTP 响应中的状态码
  • reason - 解释异常原因的消息(会包含在 HTTP 响应中)
  • cause - 导致 ResponseStatusException 的原始异常

⚠️ 注意:Spring 中的 HandlerExceptionResolver 会拦截并处理所有未被 Controller 捕获的异常。其中 ResponseStatusExceptionResolver 会专门处理 ResponseStatusException 或带有 @ResponseStatus 注解的未捕获异常,从中提取状态码和原因信息并添加到 HTTP 响应中。

3.1. ResponseStatusException 的优势

使用 ResponseStatusException 有几个明显优势:

  • 解耦性:同一类型的异常可以分别处理,设置不同的响应状态码,避免强耦合
  • 减少类数量:无需创建大量自定义异常类
  • 灵活控制:支持编程式创建异常,提供更精细的异常处理控制

4. 实战示例

4.1. 生成 ResponseStatusException

下面是一个生成 ResponseStatusException 的示例:

@GetMapping("/actor/{id}")
public String getActorName(@PathVariable("id") int id) {
    try {
        return actorService.getActor(id);
    } catch (ActorNotFoundException ex) {
        throw new ResponseStatusException(
          HttpStatus.NOT_FOUND, "Actor Not Found", ex);
    }
}

Spring Boot 默认提供 /error 映射,返回包含 HTTP 状态码的 JSON 响应:

$ curl -i -s -X GET 'http://localhost:8081/actor/8'
HTTP/1.1 404
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 26 Dec 2020 19:38:09 GMT

{
    "timestamp": "2020-12-26T19:38:09.426+00:00",
    "status": 404,
    "error": "Not Found",
    "path": "/actor/8"
}

细心的读者可能发现,响应中缺少了显示原因文本 "Actor Not Found" 的 message 字段。接下来我们分析原因并解决它。

4.2. 关于 server.error.include-message 属性

server.error.include-message 属性控制错误响应中是否包含 message 字段。它支持三个值:

  • always - 错误响应始终包含 message 字段
  • never - 错误响应从不包含 message 字段
  • on_param - 仅当请求包含 message=true 参数时才包含 message 字段

⚠️ **从 Spring Boot 2.3 开始,该属性默认值为 never**。这是为了减少向客户端泄露敏感信息的风险。因此前面的示例响应中没有 message 字段。

现在设置 server.error.include-message=on_param 并测试:

$ curl -i -s -X GET 'http://localhost:8081/actor/8'
HTTP/1.1 404
...
{
    "timestamp": "2020-12-26T19:38:49.426+00:00",
    "status": 404,
    "error": "Not Found",
    "path": "/actor/8"
}

当请求不带 message=true 参数时,响应不包含 message。添加参数后:

$ curl -i -s -X GET 'http://localhost:8081/actor/8?message=true'
HTTP/1.1 404
...
{
    "timestamp": "2020-12-26T19:49:11.426+00:00",
    "status": 404,
    "error": "Not Found",
    "message": "Actor Not Found",
    "path": "/actor/8"
}

💡 开发提示:Spring Boot DevTools 会将 server.error.include-message 默认设为 always。开发时启用 DevTools 可以方便地查看完整错误信息。

为简化演示,**本教程后续示例将设置 server.error.include-message=always**。

4.3. 同一异常类型返回不同状态码

现在演示如何对同一异常类型返回不同的 HTTP 状态码:

@PutMapping("/actor/{id}/{name}")
public String updateActorName(
  @PathVariable("id") int id, 
  @PathVariable("name") String name) {
 
    try {
        return actorService.updateActor(id, name);
    } catch (ActorNotFoundException ex) {
        throw new ResponseStatusException(
          HttpStatus.BAD_REQUEST, "Provide correct Actor Id", ex);
    }
}

响应示例:

$ curl -i -s -X PUT 'http://localhost:8081/actor/8/BradPitt'
HTTP/1.1 400
...
{
    "timestamp": "2018-02-01T04:28:32.917+0000",
    "status": 400,
    "error": "Bad Request",
    "message": "Provide correct Actor Id",
    "path": "/actor/8/BradPitt"
}

5. 总结

本教程深入探讨了 ResponseStatusException 的使用方法。相比传统的 @ResponseStatus 注解,它提供了更灵活的编程式 HTTP 状态码设置方式,特别适合需要动态控制响应状态的场景。

完整源代码可在 GitHub 获取


原始标题:Spring ResponseStatusException