Spring MVC Controller 返回值类型全解
一、底层机制先说清楚
Controller 方法的返回值由 HandlerMethodReturnValueHandler 链决定怎么处理。是否需要视图解析、是否序列化成 JSON,取决于:
- 方法/类上有没有
@ResponseBody(或@RestController) - 返回类型本身是否被某个 handler 支持(如
ResponseEntity、Callable、View)
理解这一点后,所有返回类型可以分成四类:视图类、数据类、异步类、特殊类。
二、视图类(传统 MVC / 服务端渲染)
1. String —— 返回逻辑视图名
@GetMapping("/user")
public String user(Model model) {
model.addAttribute("user", userService.get(1));
return "user/detail"; // → /WEB-INF/views/user/detail.jsp
}
场景:JSP / Thymeleaf / FreeMarker 页面渲染,最常见的传统写法。
注意:加 @ResponseBody 后含义完全变了 —— 变成直接把字符串写进响应体:
@ResponseBody
@GetMapping("/ping")
public String ping() { return "pong"; } // HTTP body = pong,不解析视图
2. ModelAndView —— 视图名 + 数据一步到位
@GetMapping("/user")
public ModelAndView user() {
ModelAndView mv = new ModelAndView("user/detail");
mv.addObject("user", userService.get(1));
mv.setStatus(HttpStatus.OK);
return mv;
}
场景:需要在方法内部动态决定视图名(如根据参数选择模板),或想把视图和模型封装成一个对象传递时。
3. Model / ModelMap / Map —— 只返回数据,视图名由请求路径推断
@GetMapping("/user/list")
public Map<String, Object> list() {
return Map.of("users", userService.list()); // 视图名推断为 "user/list"
}
场景:不想硬编码视图名,依赖 RequestToViewNameTranslator 用 URL 推断模板路径。REST 场景下配合 @ResponseBody 就是返回 JSON。
4. void —— 自己操作 HttpServletResponse
@GetMapping("/export")
public void export(HttpServletResponse resp) throws IOException {
resp.setContentType("text/csv");
resp.getWriter().write(csv);
}
场景:需要手动完全控制响应(流式写出、自定义 header、老式 Servlet 风格)。可读性差,现代代码不推荐。
5. View 实现类 —— 显式指定渲染方式
@GetMapping("/old")
public View redirect() {
return new RedirectView("/new-url"); // 302
}
@GetMapping("/json")
public View jsonView() {
MappingJackson2JsonView v = new MappingJackson2JsonView();
v.setAttributesMap(Map.of("code", 0)); // 直接输出 JSON 视图
return v;
}
场景:框架内部/特殊渲染需求,业务代码少见。
6. 重定向与 Flash 属性
@PostMapping("/user")
public String create(User user, RedirectAttributes ra) {
userService.save(user);
ra.addFlashAttribute("msg", "保存成功"); // 存 session,重定向后取一次
ra.addAttribute("id", user.getId()); // 拼到 URL 上 ?id=1
return "redirect:/user/list";
}
场景:表单提交后防重复提交(PRG 模式)。forward: 前缀则是不改 URL 的服务器内部转发。
三、数据类(REST / JSON API)
1. @ResponseBody + 任意对象 / @RestController
@RestController // = @Controller + @ResponseBody
public class UserController {
@GetMapping("/api/users/{id}")
public User get(@PathVariable Long id) {
return userService.get(id); // → Jackson 序列化成 JSON
}
}
场景:前后端分离的主流写法。返回 DTO/VO/统一响应包装类(如 Result<T>)。
2. ResponseEntity<T> —— 数据 + 状态码 + 响应头
@GetMapping("/api/users/{id}")
public ResponseEntity<User> get(@PathVariable Long id) {
return userService.find(id)
.map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.notFound().build());
}
场景:需要精确控制 HTTP 状态码和 Header 时的首选。创建资源推荐:
@PostMapping("/api/users")
public ResponseEntity<User> create(@RequestBody User user) {
User saved = userService.save(user);
URI loc = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}").buildAndExpand(saved.getId()).toUri();
return ResponseEntity.created(loc).body(saved); // 201 + Location
}
3. ResponseEntity<Void> —— 只要状态码,不要 body
@DeleteMapping("/api/users/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
userService.delete(id);
return ResponseEntity.noContent().build(); // 204
}
场景:DELETE / 无返回内容的操作。
4. HttpHeaders / HttpEntity<T>
@GetMapping("/api/headers")
public HttpHeaders headers() { // 只返回响应头
HttpHeaders h = new HttpHeaders();
h.set("X-Trace-Id", "abc");
return h;
}
场景:HttpEntity 是 ResponseEntity 的父类,当状态码由框架/异常决定、你只想控制 body 和 header 时使用。实际业务里几乎都用 ResponseEntity。
5. ProblemDetail(Spring 6 / Boot 3,RFC 7807)
@ExceptionHandler(NotFoundException.class)
public ProblemDetail handle(NotFoundException e) {
ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage());
pd.setTitle("资源不存在");
pd.setProperty("traceId", MDC.get("traceId"));
return pd; // Content-Type: application/problem+json
}
场景:标准化的 API 错误响应,替代自定义错误结构。
6. 文件下载 / 流式输出
// 一次性输出(小文件,会被完整读入内存)
@GetMapping("/api/file")
public ResponseEntity<byte[]> download() {
byte[] data = service.load();
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"a.pdf\"")
.contentType(MediaType.APPLICATION_PDF)
.body(data);
}
// 大文件推荐:Resource + 复用 Content-Length / Range 支持
@GetMapping("/api/file2")
public ResponseEntity<Resource> download2() {
Resource r = new FileSystemResource("/data/big.zip");
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"big.zip\"")
.body(r);
}
场景:导出 Excel/PDF、图片、附件下载。
四、异步类(长耗时 / 推送)
| 返回类型 | 特点 | 场景 |
|---|---|---|
Callable<T> | 由 Spring 的 AsyncTaskExecutor 执行,自动处理超时 | 简单异步,线程由框架管理 |
DeferredResult<T> | 结果由其他线程写入,不占用容器线程 | 长轮询、等待 MQ/回调结果 |
WebAsyncTask<T> | Callable 增强版,可设超时和回调 | 需要精细控制超时/异常 |
StreamingResponseBody | 直接在响应流上写,不占 Servlet 线程 | 大文件、CSV 导出 |
SseEmitter | text/event-stream 单向推送 | 进度条、实时通知 |
Mono<T> / Flux<T> | WebFlux 响应式 | 响应式技术栈(注意:Spring MVC 也支持返回,但语义不同) |
@GetMapping("/api/async")
public DeferredResult<String> async() {
DeferredResult<String> dr = new DeferredResult<>(5000L, "timeout");
executor.execute(() -> dr.setResult(service.slowWork()));
return dr;
}
@GetMapping(value = "/api/progress", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter progress() {
SseEmitter emitter = new SseEmitter();
executor.execute(() -> {
for (int i = 0; i <= 100; i += 10) emitter.send(i + "%");
emitter.complete();
});
return emitter;
}
场景:接口耗时超过几秒(避免占满 Tomcat 线程池)、AI 流式输出、实时进度推送。
五、特殊 / 补充写法
@ResponseStatus 声明状态码
@PostMapping("/api/users")
@ResponseStatus(HttpStatus.CREATED)
public User create(@RequestBody User u) { return service.save(u); }
场景:状态码固定、不需要动态 header 时,比 ResponseEntity 更简洁。
异常处理返回值
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BizException.class)
public ResponseEntity<Result<Void>> handle(BizException e) {
return ResponseEntity.badRequest().body(Result.fail(e.getMessage()));
}
}
场景:统一错误响应,Controller 里就可以放心直接返回业务数据。
@ModelAttribute 方法
@ModelAttribute("cities")
public List<String> cities() { return cityService.all(); }
场景:为同一 Controller 的所有视图预置公共数据,不参与 URL 映射。
六、怎么选:决策清单
| 你的需求 | 推荐返回类型 |
|---|---|
| 渲染 HTML 页面 | String(视图名) / ModelAndView |
| 返回 JSON,状态码固定 200 | @RestController + 业务对象 / Result<T> |
| 返回 JSON,需控制状态码 / Header | ResponseEntity<T> |
| 只返回状态码(204/201 无内容) | ResponseEntity<Void> + @ResponseStatus |
| 文件下载 | ResponseEntity<Resource> / StreamingResponseBody |
| 接口耗时长 | Callable / DeferredResult |
| 实时推送 | SseEmitter / Flux |
| 手动操控响应 | void + HttpServletResponse(尽量少用) |
| 标准化错误体 | ProblemDetail + @RestControllerAdvice |
| 重定向 | "redirect:/path" + RedirectAttributes |
一句话原则:传统 MVC 优先 String/ModelAndView;REST API 默认 @RestController 直接返回对象,一旦需要控制状态码或 Header 就升级为 ResponseEntity<T>,耗时接口用异步类型,文件/推送用流式类型。
七、常见坑
String加不加@ResponseBody语义完全不同 —— 一个走视图解析,一个写 body。类上用了@RestController就全是后者。void返回但什么都没写 —— Spring 会当成"视图已由方法处理",结果 404 或空响应。用void必须自己写 response。Model/Map不加@ResponseBody会去找视图模板 —— 会报Circular view path或 404,别想当然当 JSON 用。byte[]大文件会全量进内存 —— 用Resource/StreamingResponseBody替代。ResponseEntity和@ResponseStatus同时用 ——ResponseEntity优先级更高,@ResponseStatus被忽略。- 异步返回类型里抛异常 —— 不会被普通的
@ExceptionHandler捕获,需要DeferredResult.setErrorResult()或AsyncRequestTimeoutException处理。