在 Spring Boot 写 REST API 时,很多项目会把控制器返回值统一写成 ResponseEntity<ApiResult<T>>。看起来很“规范”,但如果每个接口只是ResponseEntity.ok(...),没有改变状态码、没有设置响应头,也没有处理特殊响应场景,那它更多只是给代码套了一层壳。
ResponseEntity 真正解决的问题不是“统一返回格式”,而是完整控制一次 HTTP 响应 :状态码、响应头、响应体。用对了,它能让 API 语义更清晰;用滥了,它就是样板代码制造机,还是全自动款。
1. ResponseEntity 是什么? ResponseEntity<T> 是 Spring Framework 提供的 HTTP 响应包装对象。它继承自 HttpEntity<T>,在请求体和响应头的基础上,额外增加了 HTTP 状态码。
可以简单理解成:
1 ResponseEntity = HTTP Status + HTTP Headers + HTTP Body
比如下面这个 HTTP 响应:
1 2 3 4 5 6 7 8 HTTP/1.1 201 CreatedLocation : /users/1001Content-Type : application/json{ "id" : 1001 , "name" : "Mario" }
用 ResponseEntity 表达就是:
1 2 3 4 5 6 7 8 9 10 @PostMapping("/users") public ResponseEntity<UserVO> create (@RequestBody CreateUserRequest request) { UserVO user = userService.create(request); URI location = URI.create("/users/" + user.id()); return ResponseEntity .created(location) .body(user); }
这里它做了三件事:
返回 201 Created,表示资源创建成功;
设置 Location 响应头,告诉客户端新资源的位置;
返回创建后的资源内容。
如果只是返回一个普通对象,Spring MVC 也能把它序列化成 JSON;但普通对象不能直接表达状态码和响应头。
2. 它和 @ResponseBody、@RestController 是什么关系? 在 @RestController 中,方法返回对象时,Spring 会通过 HttpMessageConverter 把对象写入响应体。也就是说:
1 2 3 4 @GetMapping("/users/{id}") public UserVO detail (@PathVariable Long id) { return userService.detail(id); }
这已经能返回 JSON,并且默认是 200 OK。
等价的“无意义套壳”写法是:
1 2 3 4 @GetMapping("/users/{id}") public ResponseEntity<UserVO> detail (@PathVariable Long id) { return ResponseEntity.ok(userService.detail(id)); }
这两段代码在普通成功响应下没有本质差异。第二段只是多写了 ResponseEntity.ok(...),但没有带来额外 HTTP 语义。
所以可以先记住一句话:
只返回响应体,用普通对象;需要控制状态码或响应头,再用 ResponseEntity。
3. 基本用法 3.1 返回 200 OK 1 2 3 4 @GetMapping("/hello") public ResponseEntity<String> hello () { return ResponseEntity.ok("Hello, World!" ); }
如果只是这样,其实可以直接返回 String 或 DTO:
1 2 3 4 @GetMapping("/hello") public String hello () { return "Hello, World!" ; }
ok(...) 更适合和响应头、条件状态码等场景一起使用。
3.2 自定义状态码 1 2 3 4 5 6 @GetMapping("/users/{id}") public ResponseEntity<UserVO> detail (@PathVariable Long id) { return userService.findById(id) .map(ResponseEntity::ok) .orElseGet(() -> ResponseEntity.notFound().build()); }
如果用户存在,返回 200 OK;如果用户不存在,返回 404 Not Found。
Spring 5.1 之后可以使用 ResponseEntity.of(Optional<T>) 简化这个写法:
1 2 3 4 @GetMapping("/users/{id}") public ResponseEntity<UserVO> detail (@PathVariable Long id) { return ResponseEntity.of(userService.findById(id)); }
Spring 6.0.5 之后还可以用 ResponseEntity.ofNullable(...):
1 2 3 4 @GetMapping("/users/{id}") public ResponseEntity<UserVO> detail (@PathVariable Long id) { return ResponseEntity.ofNullable(userService.findNullableById(id)); }
它的语义是:对象不为 null 时返回 200 OK,对象为 null 时返回 404 Not Found。
3.3 新增资源返回 201 Created 新增资源时,不建议永远返回 200 OK。更符合 HTTP 语义的写法是返回 201 Created:
1 2 3 4 5 6 7 8 @PostMapping("/users") public ResponseEntity<UserVO> create (@RequestBody CreateUserRequest request) { UserVO user = userService.create(request); return ResponseEntity .created(URI.create("/users/" + user.id())) .body(user); }
created(location) 会设置状态码为 201 Created,并把传入的 URI 设置到 Location 响应头中。
3.4 删除成功返回 204 No Content 删除成功后,如果不需要返回响应体,直接返回 204 No Content:
1 2 3 4 5 @DeleteMapping("/users/{id}") public ResponseEntity<Void> delete (@PathVariable Long id) { userService.delete(id); return ResponseEntity.noContent().build(); }
不要写成:
1 return ResponseEntity.ok().build();
200 OK 也能跑,但 204 No Content 更准确。HTTP 状态码不是装饰品,它是客户端、网关、监控、缓存都能读懂的协议语义。
3.5 设置响应头 比如导出文件、缓存控制、ETag、重定向、Location,都需要响应头。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @GetMapping("/users/export") public ResponseEntity<Resource> exportUsers () throws IOException { Path path = userExportService.export(); FileSystemResource resource = new FileSystemResource (path); String filename = "users.xlsx" ; String contentDisposition = ContentDisposition .attachment() .filename(filename, StandardCharsets.UTF_8) .build() .toString(); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition) .contentType(MediaType.APPLICATION_OCTET_STREAM) .contentLength(Files.size(path)) .body(resource); }
如果是大文件或远程流,不建议先全部读进内存再返回。可以返回 Resource,让 Spring MVC 把内容写入响应流。
3.6 条件请求和缓存控制 ResponseEntity 也适合设置 ETag、Last-Modified、Cache-Control 等头部。
1 2 3 4 5 6 7 8 9 @GetMapping("/articles/{id}") public ResponseEntity<ArticleVO> detail (@PathVariable Long id) { ArticleVO article = articleService.detail(id); return ResponseEntity.ok() .eTag("\"" + article.version() + "\"" ) .cacheControl(CacheControl.maxAge(Duration.ofMinutes(10 ))) .body(article); }
这类场景如果只返回 ArticleVO,就无法优雅地表达缓存相关的 HTTP 头。
4. ResponseEntity 适合哪些场景? 4.1 状态码需要根据业务结果动态变化 例如查询详情:存在返回 200,不存在返回 404。
1 2 3 4 5 @GetMapping("/orders/{id}") public ResponseEntity<OrderVO> detail (@PathVariable Long id) { Optional<OrderVO> order = orderService.findById(id); return ResponseEntity.of(order); }
再比如幂等创建:新建成功返回 201,资源已存在返回 200 或 409,这时就需要根据实际结果决定状态码。
4.2 需要设置响应头 典型场景包括:
场景
常见响应头
创建资源
Location
文件下载
Content-Disposition、Content-Type、Content-Length
缓存控制
Cache-Control、ETag、Last-Modified
分页信息
X-Total-Count、Link
异步任务
Location、Retry-After
例如分页查询,如果团队希望把分页元信息放在响应头中:
1 2 3 4 5 6 7 8 @GetMapping("/users") public ResponseEntity<List<UserVO>> page (PageQuery query) { PageResult<UserVO> page = userService.page(query); return ResponseEntity.ok() .header("X-Total-Count" , String.valueOf(page.total())) .body(page.records()); }
当然,分页元信息也可以放进响应体。关键不是哪种一定正确,而是团队要保持一致。
4.3 无响应体的成功操作 删除、确认、取消、关闭这类动作,很多时候不需要返回业务数据。
1 2 3 4 5 @PostMapping("/orders/{id}/cancel") public ResponseEntity<Void> cancel (@PathVariable Long id) { orderService.cancel(id); return ResponseEntity.noContent().build(); }
这比返回 { "success": true } 更干净。
4.4 文件下载、流式响应 1 2 3 4 5 6 7 8 9 10 @GetMapping("/files/{id}") public ResponseEntity<Resource> download (@PathVariable Long id) { DownloadFile file = fileService.getDownloadFile(id); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, file.contentDisposition()) .contentType(file.mediaType()) .contentLength(file.size()) .body(file.resource()); }
这种场景 ResponseEntity<Resource> 比直接返回 byte[] 更适合,尤其是文件较大时。
4.5 全局异常处理 在 @RestControllerAdvice 中,ResponseEntity 很适合配合 ProblemDetail 返回结构化错误。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BizException.class) public ResponseEntity<ProblemDetail> handleBizException (BizException ex) { ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail( HttpStatus.BAD_REQUEST, ex.getMessage() ); problemDetail.setTitle("业务校验失败" ); problemDetail.setProperty("code" , ex.getCode()); return ResponseEntity .badRequest() .body(problemDetail); } }
Spring Framework 6 开始提供 ProblemDetail,用于表达 RFC 9457 风格的错误响应。如果你的项目已经是 Spring Boot 3.x,可以优先考虑这种错误响应模型。
5. 哪些场景不建议用 ResponseEntity? 5.1 普通查询成功响应 如果永远是 200 OK,也不需要设置响应头,直接返回 DTO/VO 更清晰。
推荐:
1 2 3 4 @GetMapping("/users/{id}") public UserVO detail (@PathVariable Long id) { return userService.detail(id); }
不推荐为了“统一”强行写成:
1 2 3 4 @GetMapping("/users/{id}") public ResponseEntity<UserVO> detail (@PathVariable Long id) { return ResponseEntity.ok(userService.detail(id)); }
这不是大错,但没必要。代码不是年会,不需要每个方法都穿正装。
5.2 Service 层返回 ResponseEntity 不要让业务层返回 ResponseEntity:
1 2 3 4 public ResponseEntity<UserVO> detail (Long id) { }
ResponseEntity 是 Web 层概念。Service 层应该返回业务结果、领域对象、DTO 或异常,不应该依赖 Spring MVC 的 HTTP 类型。
更合理的分层是:
1 2 3 4 5 6 7 8 9 10 public Optional<UserVO> findById (Long id) { return userRepository.findVOById(id); }@GetMapping("/users/{id}") public ResponseEntity<UserVO> detail (@PathVariable Long id) { return ResponseEntity.of(userService.findById(id)); }
HTTP 语义留在 Controller/Adapter 层,业务语义留在 Application/Service/Domain 层。层次清楚了,代码就没那么容易长成藤壶。
5.3 所有异常都在 Controller 里手动返回 不推荐这样写:
1 2 3 4 5 6 7 8 9 10 @GetMapping("/users/{id}") public ResponseEntity<?> detail(@PathVariable Long id) { try { return ResponseEntity.ok(userService.detail(id)); } catch (BizException ex) { return ResponseEntity.badRequest().body(ex.getMessage()); } catch (Exception ex) { return ResponseEntity.internalServerError().body("系统异常" ); } }
控制器会很快变成异常处理垃圾场。更好的方式是:正常逻辑正常写,异常统一交给 @RestControllerAdvice。
6. ResponseEntity 和统一返回体怎么配合? 很多公司项目都有统一返回体,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public record ApiResponse <T>( String code, String message, T data ) { public static <T> ApiResponse<T> success (T data) { return new ApiResponse <>("SUCCESS" , "success" , data); } public static <T> ApiResponse<T> failure (String code, String message) { return new ApiResponse <>(code, message, null ); } }
这和 ResponseEntity 并不冲突。它们负责的层次不同:
层次
负责内容
示例
HTTP 状态码
请求在协议层面的结果
200、201、204、400、404、409
HTTP 响应头
协议元信息
Location、ETag、Content-Disposition
响应体
业务数据或错误详情
ApiResponse<T>、ProblemDetail、DTO
所以可以这样写:
1 2 3 4 5 6 7 8 @PostMapping("/users") public ResponseEntity<ApiResponse<UserVO>> create (@RequestBody CreateUserRequest request) { UserVO user = userService.create(request); return ResponseEntity .created(URI.create("/users/" + user.id())) .body(ApiResponse.success(user)); }
也可以这样写:
1 2 3 4 @GetMapping("/users/{id}") public ApiResponse<UserVO> detail (@PathVariable Long id) { return ApiResponse.success(userService.detail(id)); }
关键是不要把所有错误都塞成 HTTP 200 + body.code = ERROR。这样前端当然能解析,但网关、监控、日志、重试器、缓存、链路追踪都很难判断真实结果。
一个可落地的团队规则是:
普通查询成功:200 OK + ApiResponse<T>;
创建成功:201 Created + Location + ApiResponse<T>;
删除成功且无返回体:204 No Content;
参数错误:400 Bad Request;
资源不存在:404 Not Found;
资源冲突或幂等冲突:409 Conflict;
业务校验失败:团队可统一选择 400 或 422,但必须一致;
系统异常:500 Internal Server Error,由全局异常处理兜底。
7. 常见状态码建议
状态码
含义
常见使用场景
200 OK
请求成功
查询、修改后返回结果
201 Created
创建成功
新增资源
202 Accepted
已接收但未处理完成
异步任务、导入任务
204 No Content
成功但无响应体
删除、取消、确认操作
400 Bad Request
请求参数错误
参数格式错误、校验失败
401 Unauthorized
未认证
未登录、Token 无效
403 Forbidden
已认证但无权限
权限不足
404 Not Found
资源不存在
根据 ID 查询不到资源
409 Conflict
资源状态冲突
重复创建、版本冲突、状态不允许变更
422 Unprocessable Content
请求格式正确但语义无法处理
业务语义校验失败,团队明确采用时使用
500 Internal Server Error
服务端异常
未预期异常,由全局异常处理兜底
状态码不需要玩得太花。大多数业务系统把 200、201、204、400、401、403、404、409、500 用清楚,就已经比“万物 200”强很多。
8. Controller 写法模板 8.1 查询详情 1 2 3 4 @GetMapping("/users/{id}") public ResponseEntity<UserVO> detail (@PathVariable Long id) { return ResponseEntity.of(userService.findById(id)); }
如果团队不希望详情不存在时返回 404,而是抛业务异常,那就统一交给异常处理器:
1 2 3 4 @GetMapping("/users/{id}") public UserVO detail (@PathVariable Long id) { return userService.detail(id); }
两种都可以,但不要同一个项目里一会儿 404,一会儿 200 + code=NOT_FOUND,一会儿抛异常。客户端会被你练出读心术,但它大概率练不会。
8.2 新增资源 1 2 3 4 5 6 7 8 @PostMapping("/users") public ResponseEntity<UserVO> create (@RequestBody CreateUserRequest request) { UserVO user = userService.create(request); return ResponseEntity .created(URI.create("/users/" + user.id())) .body(user); }
8.3 更新资源 1 2 3 4 5 6 7 8 @PutMapping("/users/{id}") public ResponseEntity<UserVO> update ( @PathVariable Long id, @RequestBody UpdateUserRequest request ) { UserVO user = userService.update(id, request); return ResponseEntity.ok(user); }
如果更新后不需要返回对象:
1 2 3 4 5 6 7 8 @PutMapping("/users/{id}") public ResponseEntity<Void> update ( @PathVariable Long id, @RequestBody UpdateUserRequest request ) { userService.update(id, request); return ResponseEntity.noContent().build(); }
8.4 删除资源 1 2 3 4 5 @DeleteMapping("/users/{id}") public ResponseEntity<Void> delete (@PathVariable Long id) { userService.delete(id); return ResponseEntity.noContent().build(); }
8.5 异步任务提交 1 2 3 4 5 6 7 8 9 @PostMapping("/imports") public ResponseEntity<ImportTaskVO> submit (@RequestBody ImportRequest request) { ImportTaskVO task = importService.submit(request); return ResponseEntity .accepted() .header(HttpHeaders.LOCATION, "/imports/" + task.taskId()) .body(task); }
202 Accepted 适合表达“请求已经接收,但任务还没执行完”。
9. 测试时怎么验证 ResponseEntity? 用 MockMvc 可以同时验证状态码、响应头和响应体。
1 2 3 4 5 6 7 8 9 10 11 12 13 @Test void should_create_user () throws Exception { mockMvc.perform(post("/users" ) .contentType(MediaType.APPLICATION_JSON) .content(""" { "name": "Mario" } """ )) .andExpect(status().isCreated()) .andExpect(header().string(HttpHeaders.LOCATION, Matchers.startsWith("/users/" ))) .andExpect(jsonPath("$.name" ).value("Mario" )); }
如果你用了 ResponseEntity,测试就不要只测 JSON。否则就像买了显卡只跑计算器,不能说没用,但多少有点亏。
10. 最佳实践总结
ResponseEntity 是 HTTP 响应控制工具,不是统一返回体工具。
只返回普通 JSON 时,直接返回 DTO/VO 即可。
需要动态状态码、响应头、无响应体、文件下载、异步任务、错误响应时,再使用 ResponseEntity。
不要在 Service/Domain 层返回 ResponseEntity,它应该停留在 Controller/Adapter 层。
ResponseEntity.of(Optional<T>) 和 ResponseEntity.ofNullable(T) 很适合处理“存在返回 200,不存在返回 404”的详情查询。
新增资源优先考虑 201 Created + Location。
删除成功且无需返回体时,优先使用 204 No Content。
统一返回体可以和 ResponseEntity 共存,但不要用 HTTP 200 + body.code 吃掉所有 HTTP 语义。
异常处理尽量交给 @RestControllerAdvice,不要在每个 Controller 方法里手写 try-catch。
团队要先统一状态码规范,再谈代码风格。否则 ResponseEntity 只是把混乱包装得更精致。
11. 一句话结论 ResponseEntity 适合用来表达 HTTP 协议语义:状态码、响应头、响应体。普通接口直接返回业务对象即可;只有当你真的需要控制 HTTP 响应时,再让 ResponseEntity 出场。工具用在刀刃上,代码才不会从“简洁架构”变成“豪华装修毛坯房”。
参考资料