SpringMVC 的返回值看起来只是方法签名上的一个类型差异,真正影响的却是整条响应链路:到底是走视图解析器、请求转发到页面,还是直接把对象写进响应体。很多 404、跳转失效、JSON 返回异常,往往都不是框架“没生效”,而是返回方式和注解语义混在了一起。
这篇文章按实际开发中的两类场景来拆解:传统服务端渲染如何返回页面,前后端分离项目如何返回 JSON;同时把 void 的 404 坑、@ResponseBody 的生效位置,以及 @RestController 的本质一起讲清楚,方便你看到代码就能判断底层会怎么执行。
SpringMVC 常见返回方式有哪些
返回 String:最常见的视图名称写法
在传统服务端渲染项目里,Controller 方法直接返回字符串,默认含义就是“视图名称”。

@RequestMapping("/index")
public String index(){
// 返回视图名
return "index";
}
这段代码的执行流程可以概括为:
- SpringMVC 拿到返回值
index - 视图解析器
ViewResolver按前缀、后缀拼接出目标资源,例如/WEB-INF/index.jsp - 请求被转发到对应页面渲染,同时把
Model中的数据带过去
这里有一个默认规则要先记住:返回普通字符串 = 返回视图名称。
但如果字符串带了特殊前缀,含义就会变:
return "forward:/page":表示请求转发,不再走视图解析器return "redirect:/login":表示重定向,不再走视图解析器
返回 void:为什么最容易出现 404
void 是最容易让人误判的一种返回方式,因为它看上去“什么都不返回”,但 SpringMVC 并不会什么都不做。
@RequestMapping("/demo")
public void demo(HttpServletRequest request,HttpServletResponse response){
}
如果方法直接返回 void,SpringMVC 会尝试查找一个和当前请求路径同名的视图。找不到,就会出现 404。
也就是说,void 并不等于“结束响应”,除非你在方法内部已经手动把响应处理完。
常见的三种处理方式如下。
方式 1:使用 response 手动输出内容
如果你要直接返回文本或 JSON,可以自己写响应体:
response.setContentType("application/json;charset=utf-8");
response.getWriter().write("{"code":200}");
这种方式能用,但问题也很明显:直接依赖原生 Servlet API,代码偏底层,写多了会比较繁琐,因此不适合在业务代码里大范围使用。
方式 2:在方法里手动 forward 或 redirect
如果你的目标是跳页面,也可以在 void 方法内部手动完成请求转发或重定向。
请求转发:
request.getRequestDispatcher("/success").forward(request,response);
重定向:
response.sendRedirect("/login");
这里有个很重要的约束:一旦已经执行了 forward 或 redirect,就不要再尝试返回视图名称,也不要继续往响应体里输出内容,否则很容易抛出异常。
返回 ModelAndView:数据和视图一起封装
ModelAndView 的含义比较直接,就是把数据和视图名放进同一个对象里返回。
@RequestMapping("/list")
public ModelAndView list(){
ModelAndView mv = new ModelAndView();
mv.addObject("name","张三"); // 存入request域,页面可以获取
mv.setViewName("list"); // 设置视图名称
return mv;
}
它的特点是:
ModelAndView= Model(数据) + View(视图名称)- 在老项目里比较常见
- 优点是表达集中,缺点是相对没那么轻量
如果项目还保留较多传统 MVC 写法,这种形式依然很常见;但在新代码里,很多团队会更偏向下面的 Model + String。
返回 Model + String:服务端渲染里的主流写法
这种写法把“数据”和“视图”拆开表达,通常可读性更高。
@RequestMapping("/list")
public String list(Model model){
model.addAttribute("name","张三");
return "list"; // 视图名称
}
Model:负责向 request 域存放页面需要的数据- 返回字符串:负责指定视图名称
相比 ModelAndView,这种写法更清晰地表达了两个动作:一边组装页面数据,一边决定跳到哪个视图。因此在服务端渲染项目里,它通常是更常见的主流方案。
前后端分离项目如何返回 JSON
@ResponseBody 的核心作用是什么
前面几种方式,本质上都属于“返回页面”这条链路;而前后端分离项目并不需要服务器跳转页面,更常见的目标是直接返回 JSON 数据。

这时核心注解就是 @ResponseBody:
// 方式1:注解加在方法上
@RequestMapping("/user")
@ResponseBody
public User getUser(){
User user = new User();
user.setName("李四");
return user; // 框架自动转为JSON
}
@ResponseBody 告诉 SpringMVC:
- 不要再使用视图解析器
- 不要再跳转页面
- 直接把返回值写入响应体
如果返回的是对象,底层会经过这样的链路:
返回对象 → MappingJackson2HttpMessageConverter → Jackson序列化 → JSON字符串 → 输出到response响应体
所以,@ResponseBody 的本质不是“返回 JSON 注解”这么简单,它更准确的语义是:放弃视图解析,改走消息转换器,把返回值直接当响应体输出。
@ResponseBody 可以写在哪里
@ResponseBody 有两种常见写法,而且作用范围不同。
第一种,写在方法上,只对当前方法生效:
@Controller
public class UserController{
@ResponseBody
@RequestMapping("/user")
public User getUser(){
return new User();
}
}
第二种,写在类上,对当前类中所有方法生效:
@Controller
@ResponseBody
public class UserController{
// 所有方法返回值都会序列化为JSON,不再解析视图
}
如果一个 Controller 里既有页面跳转又有接口返回,通常更适合把 @ResponseBody 放在具体方法上,而不是直接加在整个类上。
@RestController 到底是什么
很多人把 @RestController 当成一个“专门返回 JSON 的 Controller 注解”,这个理解没错,但还可以更进一步。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
}
从定义上就能看出来,它本质上是一个组合注解:
@RestController = @Controller + @ResponseBody
也就是说,只要类上标了 @RestController,当前 Controller 的所有方法就默认直接返回响应体,通常表现为返回 JSON,而不是跳转视图。
// 等价于 @Controller + @ResponseBody
@RestController
public class UserController{
@RequestMapping("/user")
public User getUser(){
return new User();
}
}
@Controller 和 @RestController 怎么区分
这两个注解最容易混淆,但判断标准其实很简单。
@Controller- 默认返回字符串时,表示视图名称,主要用于页面跳转
- 如果想返回 JSON,必须额外加
@ResponseBody
@RestController- 自带
@ResponseBody - 所有方法默认直接返回 JSON 或其他响应体内容
- 自带
因此,服务端渲染项目通常以 @Controller 为主;前后端分离项目通常以 @RestController 为主。
几个最容易踩坑的地方
误区 1:@RestController 能不能直接返回页面
结论是:不能按普通视图名那样直接跳页面。
在 @RestController 里,如果你直接返回一个字符串,默认会被当作响应体内容输出,而不是交给视图解析器。
原文给出的跳转写法是:
// RestController中跳转写法
return "forward:/index";
return "redirect:/login";
结合下文的注解语义,这里更稳妥的理解是:在带有响应体语义的场景里,返回字符串本身非常容易与“视图跳转”发生冲突。实际开发中,如果某个类既承担页面跳转又承担接口返回,最好还是拆回 @Controller,由页面方法返回视图名,接口方法单独加 @ResponseBody。
误区 2:@ResponseBody 和 forward/redirect 同时使用
这是一个非常典型的冲突场景:
@ResponseBody
public String test(){
// 冲突!@ResponseBody会把"forward:/index"当成普通字符串直接返回给前端,不会执行转发
return "forward:/index";
}
原因很直接:加了 @ResponseBody 之后,返回值已经进入“写响应体”这条链路,forward:/ 或 redirect:/ 不再被当成视图控制前缀,而只是一个普通字符串。
所以这类代码的关键判断原则是:要么走视图解析/跳转,要么走响应体输出,二者不要混写。
误区 3:@ResponseBody 能不能解析 JSP 页面
不能。
@ResponseBody 的语义就是“把返回值当作响应体输出”,它天然意味着放弃视图解析。因此它不可能再去解析 JSP,也不会帮你跳到模板页面。
按场景快速选型:一张表看懂返回方式
三类场景可以这样归类
类别 A:服务端渲染(跳转页面,传统 JSP/Thymeleaf)
- 返回视图名称
String(配合Model传数据) - 返回
ModelAndView
控制器注解:
@Controller,不能加@ResponseBody
类别 B:手动操控原生响应(void 返回)
response输出内容- 代码内手动
forward/redirect
适合特殊场景,项目中不推荐大量使用
类别 C:前后端分离(返回 JSON,不跳转页面)
@Controller + 方法上 @ResponseBody@RestController(内置 ResponseBody,直接返回实体对象)
返回方式对照表
| 返回类型 | 配套注解 | 作用 |
|---|---|---|
| String(普通视图名) | @Controller | 走视图解析器,跳转页面 |
| String(forward:/ redirect:/) | @Controller | 转发/重定向 |
| ModelAndView | @Controller | 封装数据+视图,跳转页面 |
| void | @Controller | 需要手动 response 输出/转发/重定向,否则 404 |
| 实体类/集合 | @Controller + @ResponseBody | 自动序列化为 JSON 返回 |
| 实体类/集合 | @RestController | 全部方法默认返回 JSON |
实际项目里怎么选更合适
如果是传统服务端渲染项目,优先使用 @Controller,方法返回视图名称,并配合 Model 传递页面数据。
如果是前后端分离项目,例如 Vue、小程序或其他接口型应用,通常统一使用 @RestController,让方法直接返回实体对象或统一结果对象。
如果一个 Controller 里同时存在页面跳转和 JSON 接口,最稳妥的做法是继续使用 @Controller:页面方法正常返回视图名,接口方法单独加上 @ResponseBody。这样职责更清晰,也更不容易踩到返回值语义冲突的问题。







