Spring Boot 统一返回结果怎么写

最后更新:2026-10-08
适用场景:Spring Boot 3.x、REST 接口、统一 JSON 返回结构、ResponseEntity、业务码与 HTTP 状态码

前端接一个新接口,最怕每个接口的返回格式都不一样:查用户时直接返回对象,查列表时返回数组,出错时又返回一段字符串。前端不得不为每个接口单独判断,后端排查问题也很难对齐。

统一返回结果能解决这个问题,但不能只写一个 Result<T> 类就算完成。状态码、找不到数据、参数错误和异常响应也得有明确约定。下面用上一篇文章里的用户查询接口,写一套够用、容易维护的返回方式。

一、先看结论

普通业务 JSON 接口可以约定三个字段:

{
  "code": "OK",
  "message": "成功",
  "data": {
    "id": 1,
    "username": "zhangsan"
  }
}

这三个字段的职责很简单:

字段用途
code给前端稳定判断的业务码,例如 OK、USER_NOT_FOUND
message给调用方看的简短说明
data实际业务数据,错误时可以为 null

同时保留 HTTP 状态码的含义:查询成功用 200,新增成功可以用 201,资源不存在用 404,参数不合法用 400。不要把所有请求都返回 HTTP 200,再要求前端只看 JSON 里的 code。

本文采用 ResponseEntity<ApiResponse<T>>。ApiResponse<T> 负责 JSON 结构,ResponseEntity 负责 HTTP 状态和响应头。这与 Spring MVC 的 ResponseEntity 设计一致。

二、先想清楚是否需要统一包装

如果项目只有少量内部接口,前端也能直接按 HTTP 状态处理,返回 User 或 ResponseEntity<User> 就足够了,不必为了“统一”增加一层包装。

当项目有多个业务模块、多个前端调用方,或者已经约定业务码时,固定结构更有价值。比如用户不存在、库存不足、订单状态不允许操作,都能用稳定的 code 处理,而不是让前端解析中文 message。

统一结构通常只用于业务 JSON 接口。下面这些响应要单独处理:

  • 文件下载和图片响应:保留二进制内容与 Content-Type、Content-Disposition。
  • 流式响应和 SSE:保留流格式。
  • 健康检查、第三方回调:遵守其既定协议。
  • 204 No Content:按 HTTP 语义不返回响应体。

已有接口如果正在被外部系统调用,修改返回结构属于接口契约变更。先确认调用方,再安排版本或迁移方式。

三、定义 ApiResponse

示例以 Java 17 和 Spring Boot 3.x 为基准,使用 Java record。创建 src/main/java/com/example/demo/web/ApiResponse.java:

package com.example.demo.web;

public record ApiResponse<T>(String code, String message, T data) {

    public static <T> ApiResponse<T> ok(T data) {
        return new ApiResponse<>("OK", "成功", data);
    }

    public static <T> ApiResponse<T> error(String code, String message) {
        return new ApiResponse<>(code, message, null);
    }
}

这里没有再放一个 success 布尔值。项目已经有 HTTP 状态码和业务码,第三个成功标志容易与前两者出现矛盾。例如 HTTP 是 500、code 是 OK、success 又是 false,调用方不知道以哪个为准。

如果你的项目使用 Java 8 或 Java 11,把 record 换成普通类,保留字段、构造方法和 getter 即可;示例的接口设计不用改。

业务码可以先用字符串。它比 0、10001 更容易读,也便于日志检索。等项目确实出现大量重复编码,再集中成常量或枚举,不必一开始就设计庞大的错误码体系。

四、把用户查询接口改成统一返回

假设上一篇 Spring Boot 整合 MyBatis 中已有 User 和 UserService,其中 findById 在查不到用户时返回 null,findAll 返回列表。

创建或修改 UserController:

package com.example.demo.controller;

import com.example.demo.domain.User;
import com.example.demo.service.UserService;
import com.example.demo.web.ApiResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/{id}")
    public ResponseEntity<ApiResponse<User>> findById(@PathVariable Long id) {
        User user = userService.findById(id);

        if (user == null) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND)
                    .body(ApiResponse.<User>error(
                            "USER_NOT_FOUND", "用户不存在"));
        }

        return ResponseEntity.ok(ApiResponse.ok(user));
    }

    @GetMapping
    public ResponseEntity<ApiResponse<List<User>>> findAll() {
        List<User> users = userService.findAll();
        return ResponseEntity.ok(ApiResponse.ok(users));
    }
}

这里的 ApiResponse.<User>error(...) 是显式指定泛型类型。这样返回值与 ResponseEntity<ApiResponse<User>> 一致,读代码时也能看出该接口正常情况下返回什么数据。

查询到用户

请求:

GET /users/1

HTTP 状态为 200,响应体类似:

{
  "code": "OK",
  "message": "成功",
  "data": {
    "id": 1,
    "username": "zhangsan",
    "email": "[email protected]",
    "status": 1,
    "createdAt": "2026-10-08T09:30:00"
  }
}

用户不存在

请求:

GET /users/9999

HTTP 状态为 404,响应体是:

{
  "code": "USER_NOT_FOUND",
  "message": "用户不存在",
  "data": null
}

这样前端既可以统一处理 404,也可以对 USER_NOT_FOUND 做更细的业务提示。

列表为空

如果没有用户,列表接口应该返回 200 和空数组:

{
  "code": "OK",
  "message": "成功",
  "data": []
}

空列表是一次成功查询,不应伪装成 404。data: [] 也比 data: null 更方便前端直接遍历。

五、业务码和 HTTP 状态码怎么配合

可以先定一个小范围约定:

场景HTTP 状态业务码示例data
查询成功200OK对象或数组
新增成功201OK新建资源
参数不合法400INVALID_ARGUMENTnull 或字段错误列表
资源不存在404USER_NOT_FOUNDnull
状态冲突409ORDER_STATE_CONFLICTnull
服务器故障500INTERNAL_ERRORnull

这里的业务码只是示例,项目可以按领域命名。关键是稳定、可区分,不要把异常类名直接当作对外业务码。

为什么错误不能都返回 200

HTTP 状态码不只是给浏览器看的。网关、监控、缓存、重试逻辑和 API 客户端都会读取它。

如果数据库连接失败时仍返回 200,监控可能把失败当成功,前端的通用错误处理也不会生效。JSON 里的业务码适合表达更细的业务含义,不能替代 HTTP 状态。

为什么业务码不要直接等于 HTTP 状态

同样是 404,可能是用户不存在,也可能是订单不存在。前端需要区分时,USER_NOT_FOUND 和 ORDER_NOT_FOUND 比两个 404 更具体。

同样是 409,可以区分“用户名已被占用”和“订单状态不允许取消”。HTTP 状态描述错误类别,业务码描述具体业务情况。

六、新增和删除接口怎么返回

新增资源成功后,可以返回 HTTP 201 Created 和新资源内容。假设 userService.create(request) 返回包含 ID 的 User:

User created = userService.create(request);

return ResponseEntity.status(HttpStatus.CREATED)
        .body(ApiResponse.ok(created));

上面是 Controller 方法里的返回片段,create 的参数与存储逻辑需要按实际项目实现。若前端需要新资源地址,还可以设置 Location 响应头。

删除成功且不需要任何响应体时,可以直接返回 204 No Content:

return ResponseEntity.noContent().build();

这时不要再返回 ApiResponse.ok(null)。204 本身表示响应体为空。如果团队要求删除接口也返回固定 JSON,可以使用 200,并在文档中写清楚约定。

七、分页结果放在哪里

分页接口要把列表和分页信息一起放进 data,不要让 data 有时是数组、有时又突然变成一个带 total 的对象而不说明。

例如:

{
  "code": "OK",
  "message": "成功",
  "data": {
    "items": [
      { "id": 1, "username": "zhangsan" },
      { "id": 2, "username": "lisi" }
    ],
    "page": 1,
    "size": 10,
    "total": 26
  }
}

前端可以稳定读取 data.items 和 data.total。普通列表接口继续返回数组也可以,但最好在接口文档里区分“列表”和“分页列表”。

如果使用 MyBatis 分页插件,插件负责生成分页查询;ApiResponse<T> 只负责 HTTP 响应结构。不要把分页插件的内部对象直接作为对外接口格式。

八、统一返回类不能自动处理所有异常

写了 ApiResponse<T>,只会影响你显式返回它的接口。以下错误仍可能走 Spring Boot 默认错误响应:

@RequestBody JSON 格式错误
参数校验失败
请求路径不存在
业务代码抛出异常
数据库或其他依赖故障

所以实际项目通常会配合 @RestControllerAdvice 和 @ExceptionHandler,把需要对外暴露的异常映射成约定格式。Spring MVC 文档中,@RestControllerAdvice 负责让异常处理方法对多个 Controller 生效,并把结果写入响应体。

不过不要为了凑“统一”把所有异常都捕获成 HTTP 200。也不要把 exception.getMessage() 原样返回给用户;数据库、文件路径和内部实现细节可能混在里面。

对于已经使用 Spring 的 ProblemDetail(RFC 9457)错误格式的项目,也可以保留它。Spring Framework 本身支持从异常处理方法返回 ProblemDetail,无需强迫错误响应和成功响应使用同一个外层结构。前后端约定一致即可。

九、怎么验证返回结果

启动项目后,用命令行同时看响应头和响应体:

curl -i http://localhost:8080/users/1
curl -i http://localhost:8080/users/9999
curl -i http://localhost:8080/users

Windows PowerShell 中建议明确调用系统自带的 curl.exe:

curl.exe -i http://localhost:8080/users/1
curl.exe -i http://localhost:8080/users/9999

重点检查四件事:

1. 查到数据时,HTTP 是否为 200,data 是否为对象
2. 查不到用户时,HTTP 是否为 404,code 是否为 USER_NOT_FOUND
3. 列表为空时,data 是否为 []
4. Content-Type 是否为 application/json

如果只看浏览器中的 JSON,而不看 HTTP 状态,很容易漏掉“错误也返回 200”的问题。

常见坑

1. 只有统一返回类,没有统一约定

一个接口 code 用数字,另一个用字符串;有的接口 data 是数组,有的是分页对象却没有说明。前端仍然要写很多特殊判断。

至少先固定字段含义、成功码、错误码命名和常见 HTTP 状态,再推广到其他接口。

2. 把找不到一条数据与列表为空混为一谈

GET /users/9999 找不到用户,可以返回 404。GET /users 没有记录,是一次成功查询,返回 200 和 [] 更自然。

3. 所有错误都塞进 message

前端不应该靠比较“用户不存在”“未找到用户”这类中文文案做判断。文案可以调整,业务码应保持稳定。

4. 对所有返回值强行自动包装

用 ResponseBodyAdvice 可以集中包装响应,但要处理字符串、文件、流式输出、已经包装过的对象和框架错误响应。小项目先在 Controller 显式返回 ApiResponse<T>,更容易看清接口行为。

5. 把内部异常原文放进响应

异常信息适合记录在服务端日志中。对客户端返回稳定的业务码和有限的说明,避免暴露 SQL、连接信息或代码路径。

6. 新旧接口突然切换格式

对外接口的 JSON 结构变动会影响调用方。已有消费者的项目应先约定迁移方案,再修改响应格式。

排查清单

[ ] 业务 JSON 接口是否使用一致的 code、message、data 字段
[ ] HTTP 状态是否与实际结果一致
[ ] 查单条资源不存在时是否返回 404
[ ] 空列表是否返回 []
[ ] 新增和删除接口是否遵守 201、204 等约定
[ ] 前端是否根据稳定业务码判断具体错误
[ ] 参数错误和异常是否有明确的响应处理方式
[ ] 文件、流式和第三方协议接口是否保留原有格式
[ ] 现有调用方是否知道返回结构变更

常见问题 FAQ

1. 统一返回结果一定要有 code、message、data 吗?

不一定。这是常见的业务接口约定,不是 Spring Boot 的强制格式。若项目只靠 HTTP 状态就能表达需求,直接返回资源也可以。

2. success 字段有必要加吗?

通常没有必要。HTTP 状态和业务码已经表达结果,额外的布尔字段容易产生不一致。团队已有固定契约时按契约执行。

3. 业务码用 int 还是 String?

两者都可以。字符串如 USER_NOT_FOUND 可读性更好;数字适合已有完整编码规范的团队。无论选哪种,发布后不要随意改动同一错误的编码。

4. 返回 404 时 data 应该是什么?

上面的约定使用 null。如果项目采用 ProblemDetail,错误体会是另一种结构。选一种契约并保持稳定。

5. List 为空时应该返回 404 吗?

通常返回 200 和空数组。集合资源存在,只是当前没有符合条件的元素。

6. 统一返回类能处理参数校验异常吗?

不能自动处理。校验失败通常需要异常处理逻辑把错误转换为约定响应;这部分适合在全局异常处理里实现。

7. 可以只返回 HTTP 200,用 code 区分错误吗?

技术上可以,但会让通用 HTTP 客户端、监控和网关失去状态信息。业务 API 更建议同时返回正确的 HTTP 状态和稳定业务码。

最后总结

Spring Boot 统一返回结果,先把契约定清楚,再写工具类。对普通业务 JSON 接口,ApiResponse<T> 可以统一 code、message、data;ResponseEntity 负责真正的 HTTP 状态。

从单条查询和列表查询开始最容易验证:查到数据返回 200,单条不存在返回 404,列表为空返回 200 加空数组。异常、文件下载和流式接口按各自的响应方式处理。

相关文章

SpringBoot 教程

Spring Boot 接收 JSON 参数

Spring Boot 整合 MyBatis

Spring Boot 连接 MySQL

Spring Boot 配置文件怎么写

参考资料

Spring Framework 6.2:ResponseEntity

Spring Framework 6.2:Controller Advice

Spring Framework 6.2:Error Responses

更新记录

2026-10-08:创建文章,增加 ApiResponse、ResponseEntity、业务码与 HTTP 状态码、空列表和异常边界示例。