改造你的Web应用,让其支持@RequestBody内容的重复读取


思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。

作者:毅航😜


在前几章中我们对SpringMVC中注解@ReqeustBody注解的解析原理进行了深入的剖析,在之前的分析中,并就SpringMVC中重复使用@ReqeustBody所导致的现象即原因进行了深入的剖析。

而本文我们则主要介绍如何通过手动Coding的方式,使SpringMVC内部支持@RequestBody的多次读取。

不了解@RequestBody解析原理的读者可参考:剖析SpringMVC内部对于@ReqeustBody注解的解析深入剖析@RequestBody无法被重复解析的原因进行了解~

前言

众所周知 Spring MVC不支持多个@RequestBody注解用于同一个方法参数上 。但在剖析SpringMVC内部对于@ReqeustBody注解的解析我们曾留下如下这样一段代码,并放出豪言我们有手段让SpringMVC支持如下代码的解析!

java 复制代码
@PostMapping("/duplicate")
public ResponseEntity getBookAndUserInfo(@RequestBody BookInfo bookInfo ,
                                         @RequestBody UserInfo userInfo) {
    BookInfoDto bookInfoDto = BookInfoDto.builder()
            .bookInfo(bookInfo)
            .userInfo(userInfo)
            .build();


    return new ResponseEntity(bookInfoDto, HttpStatus.OK);
}

你可能会想SpringMVC内部不支持重复使用@RequestBody一定有其道理,按着规矩来就可以了,何必写成这样呢?并且上述代码完全可以将BookInfoUserInfo封装为同一个实体,然后在进行转换即可。这样做事没错,但这次笔者想做点不一样的,希望笔者的思路能给你带来启发!

@RequestBody无法重复解析的原因

在之前的分析中,我们只是简要的分析了@ReqeustBody无法重复解析的原理。这次我们通过手动Debug的方式来一行一行的分析。理论结合实践往往的更加透彻的了理解。

本次请求示例代码如下所示:

java 复制代码
@PostMapping("/duplicate")
public ResponseEntity getBookAndUserInfo(@RequestBody BookInfo bookInfo ,
                                         @RequestBody UserInfo userInfo) {
    BookInfoDto bookInfoDto = BookInfoDto.builder()
            .bookInfo(bookInfo)
            .userInfo(userInfo)
            .build();


    return new ResponseEntity(bookInfoDto, HttpStatus.OK);
}

不难发现,在方法getBookAndUserInfo中的入参中通过两个@RequestBody来进行修饰。进一步,对于InvocableHandlerMethod # getMethodArgumentValues而言,其在解析入参信息时,其中的parameters数组便记录了当前被请求方法入参信息,具体如下所示:

由于在getBookAndUserInfo中方法入参包含两个参数,因此parameters的容量为2。因此AbstractMessageConverterMethodArgumentResolver 中的readWithMessageConverters中进行参数解析的逻辑也就会被调用两次。

注:此处不熟悉相关调用逻辑可参参考:深入剖析@RequestBody无法被重复解析的原因

更进一步,两次调用readWithMessageConverters方法时,其内部在构建EmptyBodyCheckingHttpInputMessage时的逻辑如下

  1. 解析第一个参数BookInfo参数时,构建的EmptyBodyCheckingHttpInputMessage对象时的情况如下
  1. 解析第二个参数UserInfo参数时,构建的EmptyBodyCheckingHttpInputMessage对象时的情况如下

对比上图不难发现,当在第二次调用readWithMessageConverters进行构建EmptyBodyCheckingHttpInputMessage对象时,我们注意到其会将一个body成员变量置为null

其置为null的原因,我们曾在剖析SpringMVC内部对于@ReqeustBody注解的解析中提及过,核心原因无非就是:当前I/O流已经关系,所以无法从body请求体中读取内容!

进一步,EmptyBodyCheckingHttpInputMessage中成员变量body被置为null后,其所诱发的连锁反应就是导致readWithMessageConvertersmessage.hasBody()的执行结果返回false,进而导致参数无法被解析。

具体到当前例子来看,这就导致第二个被@RequestBody修饰的UserInfo参数无法解析,进而导致本该被解析参数返回null,从而导致出现请求出现400

重复读取@RequestBody内容

那有没有一种办法让我们重复读取@ReqeustBody内容呢?答案是肯定。在提供解决方案之前,我们不妨先来看看导致@RequestBody无法重复读取原因是什么。

通过之前分析不难发现,如果一个方法入参中同时包含两个@ReqeustBody所修饰的Java对象,那么第二个被@ReqeustBody所修饰的对象在进行解析时,其在读取请求体中相关内容时存在无法解析的问题。

那导致该问题的原因是什么呢?具体来看,在EmptyBodyCheckingHttpInputMessage构造器中执行pushbackInputStream.read() 时会返回一个-1。而这个-1则表示无法从当前请求体中读取相关内容。那为什么会导致这样的问题呢?

答案也很简单,就是因为SpringMVC中对于请求体的内容是通过I/O流进行处理的,当处理完毕后会将相应的I/O流进行关闭。@ReqeustBody内容主要封装于请求体中,如果一个请求方法中使用多个@ReqeustBody注解进行修饰,那么SpringMVC在解析时会遍历请求方法所有的入参信息,并且会重复获取请求体中的内容,以完成相应Java对象的封装。

但是请求体的中I/O在第一次解析处理后会关闭,这就导致后续再处理时,无法从请求体中获取相应内容,进而也就导致后续被@ReqeustBody修饰的对象无法完成封装。

明白了问题所在后,不知道你能否想到了相对应解决策略?此处只提供一种缓存的思路,如果你有其他好的思路也可在评论区留言~~~

所谓缓存的思路其实也很简单,既然你对于请求体中的内容只读取一次,那么如果我们把请求体中内容进行缓存,这样你下次再调用pushbackInputStream.read()时是不是就可以读取到了,进而也就不会使得EmptyBodyCheckingHttpInputMessagebody置为null

顺着这个思路,我们再来看EmptyBodyCheckingHttpInputMessage的构造方法。

java 复制代码
public EmptyBodyCheckingHttpInputMessage(HttpInputMessage inputMessage) throws IOException {
   this.headers = inputMessage.getHeaders();
   InputStream inputStream = inputMessage.getBody();
   if (inputStream.markSupported()) {
      inputStream.mark(1);
      this.body = (inputStream.read() != -1 ? inputStream : null);
      inputStream.reset();
   }
   else {
      PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream);
      int b = pushbackInputStream.read();
      if (b == -1) {
         this.body = null;
      }
      else {
         this.body = pushbackInputStream;
         pushbackInputStream.unread(b);
      }
   }
}

我们注意到pushbackInputStream的读取依赖于inputMessage.getBody()。而inputMessage.getBody()则主要会获取当前请求Reqeust对象,并读取其中的I/O流信息。

进一步,既然会获取当前请求Reqeust对象,那么是不是就可以通过对Reqeust进行处理呢?只要我们读取请求中的Reqeust中请求体内容,并进行缓存我们对于请求体缓存的目标也就实现了。

更进一步,为了SpringMVC中获取的请求可以知道知晓我们所缓存的内容,我们还需要对原先Reqeuest进行替换,那什么组件能支持我们完成这样操作呢?最简单的手段无非是使用过滤器(Filter)。

相关改造代码如下所示:

  1. 缓存请求体内容,构造信息HttpServletRequest
java 复制代码
public class CacheRequestBodyContent  extends HttpServletRequestWrapper {
    private byte[] body = null;
    /**
     * Constructs a request object wrapping the given request.
     *
     * @param request The request to wrap
     * @throws IllegalArgumentException if the request is null
     */
    public CacheRequestBodyContent(HttpServletRequest request, ServletResponse response) {
        super(request);
        try {
            request.setCharacterEncoding("utf-8");
            response.setCharacterEncoding("utf-8");
            body = IoUtil.readBytes(request.getInputStream(), false);
        } catch (Exception e) {
            log.error("请求数据读取失败,请重试");
        }
    }

    
    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream cache = new ByteArrayInputStream(body);
        return new ServletInputStream() {
            @Override
            public int read() throws IOException {
                return cache.read();
            }

            @Override
            public int available() throws IOException {
                return body.length;
            }

            // .. 省略其他无关方法
        };
    }

}

此处之所以还要重写其中的getReader getInputStream方法主要为了保证 pushbackInputStream.read()读取内容时,可以读取到我们所缓存的请求体内容。

  1. 定义过滤器
java 复制代码
public class CacheRequestFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        ServletRequest requestWrapper = null;
        if (request instanceof HttpServletRequest) {
            
            requestWrapper = new CacheRequestBodyContent((HttpServletRequest) request, response);
        }
        if (null == requestWrapper) {
            chain.doFilter(request, response);
        } else {
            chain.doFilter(requestWrapper, response);
        }
    }
}

其中,通过 requestWrapper = new CacheRequestBodyContent((HttpServletRequest) request, response);这行代码对默认的Request对象进行替换,以换成我我们自定义的Reqeust对象。

  1. 配置过滤器
java 复制代码
@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean someFilterRegistration() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new CacheRequestFilter());
        registration.addUrlPatterns("/*");
        registration.setName("requestFilter");
        registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);
        return registration;
    }
}

配置完毕启动程序后,我们调用http://localhost:8080/users/duplicate内容,我们会可以看到起会得到如下结果:

显然,通过我们这样的改造使得SpringMVC内部支持了对@ReqeuestBody注解的解析。SpringMVC内部无法重复解析@ReqeustBody的问题被我们完美解决!

总结

至此我们也就利用三篇文章完成的对SpringMVC@ReqeustBody的解析原理进行剖析,并利用相关的源码知识对SpringMVC中无法重复解析@ReqeustBody注解的问题进行解决。

如果觉得文章对你有所帮助不妨点赞+收藏+关注作者,我们下次再见!

相关推荐
哎呀呀嗯呀呀12 分钟前
class 031 位运算的骚操作
java·算法·位运算
2401_8581205315 分钟前
古典舞在线交流平台:SpringBoot设计与实现详解
java·spring boot·后端
大白飞飞23 分钟前
IDEA创建、导入、删除maven项目
java·maven·intellij-idea
赐你岁月如歌27 分钟前
如何使用ssm实现基于web的网站的设计与实现+vue
java·后端·ssm
2401_857297911 小时前
秋招内推2025-招联金融
java·前端·算法·金融·求职招聘
一 乐1 小时前
考研论坛平台|考研论坛小程序系统|基于java和微信小程序的考研论坛平台小程序设计与实现(源码+数据库+文档)
java·数据库·学习·考研·微信·小程序·源码
一 乐1 小时前
租拼车平台|小区租拼车管理|基于java的小区租拼车管理信息系统小程序设计与实现(源码+数据库+文档)
java·数据库·vue.js·微信·notepad++·拼车
xmh-sxh-13142 小时前
如何选择数据库架构
java
jxxchallenger2 小时前
踩坑spring cloud gateway /actuator/gateway/refresh不生效
java·数据库·gateway
远望樱花兔2 小时前
【d59】【Java】【力扣】146.LRU缓存
java·开发语言·算法