今天聊一个Spring MVC里最基础、但也最容易被忽略的注解ResponseBody。很多人刚开始做接口开发时都碰到过这种情况——Controller方法写得没毛病业务逻辑也执行了可前端请求过来不是404就是看到一段莫名其妙的HTML错误页再不然返回的对象变成了[object Object]。起初我也以为是自己路径写错了折腾了半天才搞明白问题就出在这个小小的注解上。这篇文章不打算只讲“加了就能返回JSON”这种废话而是把背后的那套机制彻底捋一遍为什么你的方法返回值会被当成视图名、ResponseBody到底改变了Spring MVC处理流程中的哪一环、HttpMessageConverter是怎么介入的、以及实际项目里围绕它有哪些高频踩坑点。适合刚接触Spring的初学者也适合用了很久但一直没深究原理的开发者在面试前查漏补缺。1. 一切要从Spring MVC的请求处理流程说起1.1 老式开发里Controller的默认归宿是“视图”要理解ResponseBody得先回到Spring MVC最初的设计语境。在早期的Java Web开发中前后端是不分离的。浏览器发起一个请求服务器端处理完业务逻辑之后渲染一个完整的HTML页面返回给浏览器这是标准操作。Spring MVC为了支撑这种模式设计了一套“控制器返回逻辑视图名视图解析器负责找到真实JSP页面并渲染”的流程。举个例子不加ResponseBody时写这样的方法Controller public class UserController { GetMapping(/user) public String user() { return user; } }在Spring Boot项目中如果没有额外配置类路径下的static目录里也不存在user.html这里return user并不会把字符串user直接塞进响应体。Spring MVC拿到返回值user之后会觉得这是一个“逻辑视图名”于是委托ViewResolver去解析找找看有没有对应名为user的模板或者JSP。如果找不到就会抛出404或者返回一段异常解析页面的错误信息。这背后的核心组件叫DispatcherServlet它是Spring MVC的前端控制器。一个请求进来之后DispatcherServlet按顺序要做的事情大致是找到HandlerMapping定位到处理请求的Controller方法调用HandlerAdapter执行该方法拿到返回值后再交给HandlerMethodReturnValueHandler决定怎么处理。在没标注ResponseBody的情况下处理返回值的是ModelAndViewMethodReturnValueHandler或者类似的视图型处理器它们会认为方法返回的字符串是视图名。1.2 前后端分离之后这种默认行为就成了麻烦后来前端框架流行起来页面渲染的活被Vue、React这类框架接走了。后端做的只是把数据通过JSON格式返回给前端前端拿到数据自己组织页面。这时候如果还用“返回逻辑视图名”的老一套就会非常别扭。我要返回的是一个用户对象、一个订单列表不是页面名。可Spring MVC的默认行为恰恰是往“视图”方向处理的这就导致接口开发时总有人忘记处理返回值。所以真正的问题是默认情况下Spring MVC认为Controller方法返回的是ModelAndView或逻辑视图名而接口开发需要的是“把方法返回值直接当作HTTP响应体内容返回给客户端且通常是JSON字符串”。为了把这两种行为区分开Spring MVC提供了ResponseBody注解它的字面意思就是“把返回值写在响应体里”跳过ViewResolver那一套视图解析逻辑。理解了这一点后面所有问题都能串起来为什么返回字符串但不加注解会404因为被当成视图名去找了。为什么加了注解就正常因为走的是另一条处理链路响应体直接被写入压根不经过视图解析器。2. ResponseBody到底改变了什么2.1 核心机制HttpMessageConverter登场ResponseBody不是魔法它背后站着一组真正干活的组件HttpMessageConverter中文叫HTTP消息转换器。你可以把它想象成一个多功能的翻译官。服务端的Controller方法返回了一个Java对象这个Java对象对网络来说是没法直接传输的得转成字节流才能放进HTTP响应体。反过来前端传过来的JSON数据也需要被转成Java对象才能被Controller方法接收。Spring MVC在标注了ResponseBody的方法执行完成之后会把返回值交给RequestResponseBodyMethodProcessor这个返回值处理器。这个处理器拿到返回值后再根据请求头里的Accept字段、方法声明里的produces属性、返回值的实际类型从容器中所有已注册的HttpMessageConverter里挑选一个合适的把返回值转换后写到响应输出流。我们可以看一个极简的示意流程请求进入 DispatcherServlet - HandlerAdapter 执行 Controller 方法 - 方法带 ResponseBody - RequestResponseBodyMethodProcessor 登场 - 遍历 HttpMessageConverter 列表 - 找到能处理当前返回类型的转换器 - 转换器把 Java 对象写成字节 - 写入 Response OutputStream2.2 常见的消息转换器都有哪些Spring Boot的Web场景里容器中默认注册了一批转换器。最常见的两个是StringHttpMessageConverter处理String类型。前端拿到的是纯文本不额外转JSON。它还有一个特别容易踩坑的细节——处理String时默认使用的字符集在较老版本里是ISO-8859-1所以以前经常有人发现接口返回中文乱码后来靠produces application/json;charsetUTF-8或者全局配置才解决。MappingJackson2HttpMessageConverter当项目引入了Jackson依赖Spring Boot Web场景默认包含这个转换器就负责把Java对象序列化成JSON字符串也能把前端传来的JSON字符串反序列化成Java对象。它支持的类型范围非常广几乎所有Java对象都能被它处理。除此之外还有处理XML的Jaxb2RootElementHttpMessageConverter、处理字节流的ByteArrayHttpMessageConverter、处理Resource的ResourceHttpMessageConverter等等。日常接口开发中我们打交道最多的就是前两个。2.3 RestController偷懒背后的组合注解既然接口开发的场景里每个方法都要返回JSON那每个方法都写一次ResponseBody确实很啰嗦。Spring从4.0开始提供了RestController这是一个组合注解Target(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) Documented Controller ResponseBody public interface RestController { }看到了吗RestController本身就包含了Controller和ResponseBody。所以它标在类上意味着这个类里所有方法的返回值都会被直接写入响应体不再走视图解析。这带来的不光是少敲几个字更是语义上的明确这个类就是用来提供数据接口的不是返回页面的。项目里只要用了RestController类里的方法就默认有了万能回复效果。3. 实际开发中怎么正确使用ResponseBody3.1 接口方法书写的几种姿势先看最常规的写法。类上用Controller方法上加ResponseBody作用范围精确到方法Controller RequestMapping(/api/user) public class UserController { GetMapping(/info) ResponseBody public UserInfo getUserInfo(RequestParam Long userId) { UserInfo info userService.getUserInfo(userId); return info; } }前端调/api/user/info?userId1拿到的就是{ userId: 1, name: 张小山, age: 28, phone: 13800001111 }换一种姿势类上直接用RestController方法上不需要再加ResponseBodyRestController RequestMapping(/api/user) public class UserController { GetMapping(/list) public ListUserInfo getUserList() { return userService.listAll(); } }还有一种情况容易被忽略方法返回类型是void但你仍然标了ResponseBody。比如方法内部通过HttpServletResponse自己往输出流里写了内容这时候响应状态是200但响应体内容取决于你手动写入的数据。如果你什么都没写前端就会收到一个空的响应体。这种写法在文件导出、图片输出等场景偶尔遇到但常规JSON接口不会这么干。3.2 返回值类型对转换结果的影响很多人以为“加了ResponseBody就一定会返回JSON”这是不对的。ResponseBody只是把返回值交给消息转换器处理最终返回什么格式取决于返回值的具体类型以及选中的转换器。返回String时StringHttpMessageConverter介入响应体就是一段纯文本默认Content-Type是text/plain。如果你希望返回的字符串也能被前端按JSON解析可以声明GetMapping(value /hello, produces application/json;charsetUTF-8) ResponseBody public String hello() { return {\message\:\hello\}; }但日常没人会手动拼JSON字符串太容易出错。返回一个Java对象才是主流它会被MappingJackson2HttpMessageConverter序列化成JSON。返回List、Map、自定义实体类最终呈现的都是JSON格式。返回ResponseEntityT也能配合ResponseBody其实ResponseEntity本身已经隐式处理了响应体适合需要精确控制HTTP状态码和响应头的接口。这里有个非常容易混淆的点。假如方法声明返回类型是Object实际返回的却是一个String字符串最终到底走JSON还是纯文本这种场景在代码里写得花一点就可能出现。按照转换器的选择顺序StringHttpMessageConverter优先级往往在Jackson之前但它的canWrite判断时会检查方法声明的返回类型声明是Object时它可能拒绝处理而Jackson转换器会因为支持Object类型而接手最终输出的会是带引号的JSON字符串。这个细节面试里可以拿出来讲实际代码里却建议尽量避免写这种“声明类型不匹配实际类型”的接口否则处理起来非常绕。3.3 全局RequestBody与ResponseBody的分工顺带提一句ResponseBody经常和RequestBody成对出现。前者管输出Java对象转JSON写进响应体后者管输入前端传的JSON字符串转成Java对象绑定到方法参数。看两个注解的名字就能记住一个管写一个管读。RestController RequestMapping(/api/order) public class OrderController { PostMapping(/create) public OrderVO createOrder(RequestBody CreateOrderRequest request) { // 前端传的JSON被RequestBody反序列化成CreateOrderRequest对象 return orderService.create(request); } }我见过不少半路转后端开发的朋友一开始分不清这两个写方法时把RequestBody加到方法上、把ResponseBody加到参数上然后报错报得一脸懵。实际上只要记住注解作用的“位置”方法参数上用的往往是RequestBody方法上或类上用的往往是ResponseBody。4. 高频踩坑与排查实录4.1 返回字符串却跳到了错误页面或404这是在没有正确使用RestController或ResponseBody时最容易出现的现象。现象可以统一归纳为接口URL是对的日志里业务逻辑也执行了可前端收到的不是JSON而是一个HTML错误页甚至状态码是404。原因就是前面反复提到的那套“视图解析”逻辑返回值被当成逻辑视图名ViewResolver去找对应的模板或静态页面找不到就报错。在Spring Boot项目里如果类路径classpath:/templates/或classpath:/static/没有对应资源就会直接404。排查顺序很有讲究先看一眼Controller类上是不是用了Controller而不是RestController。如果用的Controller再确认方法上有没有ResponseBody。如果问别人“接口路径对不对”不如直接看返回结果里那段HTML渲染的报错信息里面往往写着“Could not resolve view with name xxx”一眼就能定位。提示处理这个问题有个土办法——接口标注的路径页面肯定是没有了但响应体里的HTML可能本身就包含异常堆栈信息把它拿出来认真看比盲猜路径省事得多。4.2 返回对象却出现406或奇怪的序列化报错还有一种情况是方法确实加了ResponseBody返回的也是对象但前端状态码是406 Not Acceptable。这通常意味着请求头里的Accept字段要求的格式比如application/xml和当前转换器能生成的格式不匹配反过来没有转换器能处理。另外如果项目是自己手动搭的Maven工程只引入了Spring MVC依赖忘了引入Jackson库那么当你返回Java对象时容器里根本没有能序列化JSON的转换器。Spring会尝试找有没有能处理的找不到就抛出异常表现可能是HttpMediaTypeNotAcceptableException也可能是报错“No converter found for return value of type”。解决办法很直接确认spring-boot-starter-web是否被正常引入如果不用Spring Boot就检查是否有Jackson的依赖依赖坐标dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.15.2/version /dependency4.3 常见误区速查表把实际开发中容易踩的坑整理成一张表方便你对照排查。表现可能原因解决方案返回字符串得到404或HTML没加ResponseBody走了视图解析方法加注解或类改RestController返回对象得到406缺少Jackson依赖或Accept不匹配引入Jackson检查请求头Accept返回String中文乱码字符集没有显式指定UTF-8produces指定charsetUTF-8或配置编码过滤器返回对象里的字段全部丢失实体类字段没有getterJackson序列化不到在实体类上补setter/getter接口能调用但前端拿到空响应体方法返回void且没有手动写OutputStream改返回类型为对象或ResponseEntity前端收到带引号的字符串返回类型声明为Object实际返回StringJackson把它序列化成JSON字符串保证方法返回类型和实际类型一致4.4 被全局异常处理和拦截器“截胡”的问题就算方法本身写对了ResponseBody接口也不一定按预期返回。大多数项目都会做一个全局异常处理器比如用RestControllerAdvice统一处理业务异常。这个注解本身也组合了ResponseBody所以正常情况下异常返回也能序列化成JSON。但有些老项目用了ControllerAdvice但漏了ResponseBody异常处理方法的返回值同样会被当成视图解析逻辑。结果是接口正常时没事一抛异常前端就收到一个错误页面非常误导人。排查时看到“正常返回OK异常返回HTML”基本就是全局异常处理器少了ResponseBody或者没用RestControllerAdvice。拦截器也有类似问题。如果在preHandle里往response写了一段内容后续又继续放行让Controller执行两个地方同时向响应体写数据很容易出现拼接异常。这不是ResponseBody本身的问题但排查接口返回怪异时也要记得看看拦截器是不是偷偷改了响应。5. 几个进阶使用建议5.1 统一响应体封装实际项目里很少让Controller直接返回业务实体一般会统一包一层响应体。比如public class ResultT { private int code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(0); result.setMessage(ok); result.setData(data); return result; } public static T ResultT error(int code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }Controller方法返回ResultUserInfo前端每种接口拿到的结构都是固定的{code, message, data}方便统一处理。这种方式的好处是前端不用为每个接口单独定制解析逻辑错误码也能在结构体里直接体现。我个人在开发时已经把这种封装变成了默认动作。要注意的是ResultT里泛型的处理对Jackson来说非常友好它本身是Object的子实现序列化时直接扫描实际字段即可。但如果你用了一个非常复杂的多层次泛型结构建议在方法上明确返回类型方便转换器正确识别字段。5.2 自定义消息转换器控制序列化细节如果项目里不只是JSON还需要XML、YAML或者你需要对JSON的序列化做定制比如日期格式、null字段处理、枚举脱敏可以考虑自定义HttpMessageConverter。比较常见的操作是在配置类里定制MappingJackson2HttpMessageConverter对象Bean public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() { MappingJackson2HttpMessageConverter converter new MappingJackson2HttpMessageConverter(); ObjectMapper objectMapper new ObjectMapper(); objectMapper.setDateFormat(new SimpleDateFormat(yyyy-MM-dd HH:mm:ss)); objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); converter.setObjectMapper(objectMapper); return converter; }这样全局的日期会统一输出成yyyy-MM-dd HH:mm:ss字段值为null的键也会被省略掉。实测下来这种全局控制在接口联调阶段能减少很多前端对空字段的兼容成本。不过提醒一句自定义全局转换器是“一把双刃剑”。一旦序列化规则改得太个性化接口跨系统对接时别人拿通用工具解析你的未知字段可能会不兼容。如果你只是想在某个接口上做特殊处理不如在对应字段上用Jackson注解比如JsonFormat、JsonInclude把影响范围控制在局部。5.3 面试时怎么把这个问题说清楚如果这篇文章能帮你对付一次面试那面试里关于ResponseBody最好的回答路径应该是这样的先铺垫“Spring MVC默认处理返回值时走视图解析会找ViewResolver”再说“ResponseBody让返回值交给RequestResponseBodyMethodProcessor跳过视图解析”接着点出“背后由HttpMessageConverter完成对象到字节流的转换Spring Boot Web环境默认有String和Jackson两种核心转换器”最后延伸到“RestController是Controller ResponseBody的组合注解作用范围从方法提升到整个类”。把这条链路讲清楚面试官基本就能确认你对Spring MVC的执行流程心里有数。注意不要只在方法上机械地加注解。真正重要的是理解你的接口返回“是什么、谁能处理它、处理成什么格式”。6. 写在后面一种更省心的使用习惯我在实际开发里形成的固定习惯是Controller层一律使用RestController方法返回值要么是业务实体、要么是统一响应体ResultT异常统一交给RestControllerAdvice全局处理不再在Controller里手动拼JSON字符串。这样做的最大好处是整个团队写接口的风格高度统一不需要每个人每次写方法时都去纠结加不加ResponseBody。最后分享一个排查问题时的小技巧如果你判断接口的返回值和ResponseBody相关最快的方法是直接在浏览器或者调试工具里看响应头Content-Type。如果看到text/html说明返回值多半走了视图解析路线如果看到application/json说明转换器已经正常接管。很多时候一个Content-Type就能说明问题比在代码里反复加日志更直观。
阅读完成 · 觉得有帮助?