《Spring Boot 3实战案例合集》现已囊括超过60篇精选实战文章,并且此合集承诺将永久持续更新,为您带来最前沿的技术资讯与实践经验。欢迎积极订阅,享受不断升级的知识盛宴!订阅用户将特别获赠合集内所有文章的最终版MD文档(详尽学习笔记),以及完整的项目源码,助您在学习道路上畅通无阻。
环境:SpringBoot3.2.5
1. 简介
在Controller接口方法参数上通过使用@RequestBody可以将请求体中的数据自动绑定到Java对象上。如下示例:
@RestController@RequestMapping("/requestbody")public class MultiRequestBodyController {@PostMapping("")public Object save(@RequestBody User user) {// TODOreturn "success" ;}}
@RequestBody注解在开发中几乎必用。上面示例是将请求体中的信息自动绑定到一个对象,如果请求的信息中包含2个对象的信息,是否可以像下面这样使用呢?
@PostMapping("")public Object save(@RequestBody User user,@RequestBody Person person) {// TODOreturn "success" ;}
调用上面接口出现了错误

控制台也输出了如下信息
Resolved [HttpMessageNotReadableException: Required request body is missing: public Object BodyController.save(com.pack.User,com.pack.Person)
提示缺少body。
@RequestBody注解的参数会通过RequestResponseBodyMethodProcessor进行解析,而这个处理过程就是通过调用ServletRequest#getInputStream方法获取输入流,从流中读取数据(获取到数据后再通过HttpMessageConverter进行转换,如:jackson)。处理第一个参数时从流中获取了到了数据然后绑定到了对象上,当处理第二个参数时,同样还是从当前的Request中获取输入流InputStream,而对于同一个Request对象,内部机制只允许调用一次。所以就出现了上面的错误。
2. 解决办法
2.1 将多个对象包装到DTO中
public class DTO {private User user ;private Person person ;// getter, setter}
修改Controller接口
@PostMapping("")public Object save(@RequestBody DTO dto) {// TODOreturn "success" ;}
这样修改后,提交的请求参数就要修改

该种方法虽然解决了问题,也带来了一个问题,需要修改请求参数。但是这不是我们今天要将的重点,今天的重点是方法参数使用多个@RequestBody注解来接收不同的数据。
2.2 包装Request对象
要实现方法参数使用多个@RequestBody,就需要解决ServletRequest#getInputStream方法只能调用一次的问题。在Servlet规范中,我们可以通过自定义HttpServletRequestWrapper来定制Request相应的方法(该类实现了装饰器模式),如下示例:
public class PackHttpServletRequestWrapper extends HttpServletRequestWrapper {private ByteArrayInputStream bais ;public PackHttpServletRequestWrapper(HttpServletRequest request) {super(request) ;ByteArrayOutputStream baos = new ByteArrayOutputStream() ;try {StreamUtils.copy(request.getInputStream(), baos) ;bais = new ByteArrayInputStream(baos.toByteArray()) ;} catch (IOException e) {e.printStackTrace() ;}}public ServletInputStream getInputStream() throws IOException {// 当再次调用该方法时,重置bais流;将缓冲区重置到标记0的位置bais.reset() ;return new ServletInputStream() {public boolean isFinished() {return false;}public boolean isReady() {return false;}public void setReadListener(ReadListener listener) {}public int read() throws IOException {return bais.read() ;}};}}
实现一个过滤器,该过滤器传递上面自定义的Request对象。
public class PackRequestWrapperFilter implements Filter {@Overridepublic void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) req ;PackHttpServletRequestWrapper requestWrapper = new PackHttpServletRequestWrapper(request) ;chain.doFilter(requestWrapper, resp) ;}}
注册该过滤器,在SpringBoot环境下我们可以通过多种方式注册过滤器,下面是其中一种方式(@WebFilter注解更简单,不过要添加@ServletComponentScan注解)。
@BeanFilterRegistrationBean<Filter> packRequestWrapperFilter() {FilterRegistrationBean<Filter> reg = new FilterRegistrationBean<>() ;reg.setFilter(new PackRequestWrapperFilter()) ;reg.setUrlPatterns(List.of("/*")) ;return reg ;}
以上实现就可以在当前的请求中重复的调用getInputStream方法获取流。
注:ServletRequest#getReader方法只允许调用一次,所以有需要你也必须重写该方法
测试

控制台输出

注:一个方法上使用多个@RequestBody对性能肯定是有一定的影响,在上面已经提到了,读取到的数据会通过HttpMessageConverter进行转换为java对象,当你有多个@RequestBody时,那就要进行多次的读取到转换,所以应该考虑清楚再使用。
以上是本篇文章的全部内容,如对你有帮助帮忙点赞+转发+收藏
推荐文章
必学!Spring Boot结合MDC全方位的日志跟踪(支持跨线程)
面试官:说说@Configuration与@Component有什么区别?
解锁Spring资源Resource的强大功能,提升开发效率
想要精通SpringMVC?先从自定义核心组件开始,助你成为技术大佬
【纯干货】SpringCloud敏感信息配置揭秘,教你防止信息泄露!





