httpservletrequest_HttpServletResponse和HttpServletRequest取值的2个坑你知道吗?

  • Spring全家桶笔记:Spring+Spring Boot+Spring Cloud+Spring MVC
  • 十面字节跳动,依旧空手而归,我该放弃吗?
  • 疫情期间“闭关修炼”,吃透这本Java核心知识,跳槽面试不心慌
  • 2020复工跳槽季,ZooKeeper灵魂27连问,这谁顶得住?

有时候,我们需要用拦截器对Request或者Response流里面的数据进行拦截,读取里面的一些信息,也许是作为日志检索,也许是做一些校验,但是当我们读取里请求或者回调的流数据后,会发现这些流数据在下游就无法再次被消费了,这里面是其实存在着两个潜在的坑。

0b53f599bc574e0884e2efbfe0e4684d.png

坑一

Request的 getInputStream()、getReader()、getParameter()方法互斥,也就是使用了其中一个,再使用另外的两,是获取不到数据的。除了互斥外,getInputStream()和getReader()都只能使用一次,getParameter单线程上可重复使用。

三个方法互斥原因

org.apache.catalina.connector.Request方法实现了javax.servlet.http.HttpServletRequest接口,我们来看看这三个方法的实现:

getInputStream

@Overridepublic ServletInputStream getInputStream() throws IOException {    if (usingReader) {        throw new IllegalStateException            (sm.getString("coyoteRequest.getInputStream.ise"));    }    usingInputStream = true;    if (inputStream == null) {        inputStream = new CoyoteInputStream(inputBuffer);    }    return inputStream;}

getReader

@Overridepublic BufferedReader getReader() throws IOException {    if (usingInputStream) {        throw new IllegalStateException            (sm.getString("coyoteRequest.getReader.ise"));    }    usingReader = true;    inputBuffer.checkConverter();    if (reader == null) {        reader = new CoyoteReader(inputBuffer);    }    return reader;}

首先来看getInputStream()和getReader()这两个方法,可以看到,在读流时分别用usingReader和usingInputStream标志做了限制,这两个方法的互斥很好理解。下面看一看getParameter()方法是怎么跟他们互斥的。

getParameter

@Overridepublic String getParameter(String name) {// 只会解析一遍Parameter    if (!parametersParsed) {        parseParameters();    }  // 从coyoteRequest中获取参数    return coyoteRequest.getParameters().getParameter(name);}

粗略一看好像没有互斥,别着急,继续往下看,我们进到parseParameters()方法中来看一看(可以直接看源码中间部分):

protected void parseParameters() {//标识位,标志已经被解析过。    parametersParsed = true;    Parameters parameters = coyoteRequest.getParameters();    boolean success = false;    try {        // Set this every time in case limit has been changed via JMX        parameters.setLimit(getConnector().getMaxParameterCount());        // getCharacterEncoding() may have been overridden to search for        // hidden form field containing request encoding        String enc = getCharacterEncoding();        boolean useBodyEncodingForURI = connector.getUseBodyEncodingForURI();        if (enc != null) {            parameters.setEncoding(enc);            if (useBodyEncodingForURI) {                parameters.setQueryStringEncoding(enc);            }        } else {            parameters.setEncoding                (org.apache.coyote.Constants.DEFAULT_CHARACTER_ENCODING);            if (useBodyEncodingForURI) {                parameters.setQueryStringEncoding                    (org.apache.coyote.Constants.DEFAULT_CHARACTER_ENCODING);            }        }        parameters.handleQueryParameters();// 重点看这里:这里会判断是否有读取过流。如果有,则直接return。        if (usingInputStream || usingReader) {            success = true;            return;        }        if( !getConnector().isParseBodyMethod(getMethod()) ) {            success = true;            return;        }        String contentType = getContentType();        if (contentType == null) {            contentType = "";        }        int semicolon = contentType.indexOf(';');        if (semicolon >= 0) {            contentType = contentType.substring(0, semicolon).trim();        } else {            contentType = contentType.trim();        }        if ("multipart/form-data".equals(contentType)) {            parseParts(false);            success = true;            return;        }        if (!("application/x-www-form-urlencoded".equals(contentType))) {            success = true;            return;        }        int len = getContentLength();        if (len > 0) {            int maxPostSize = connector.getMaxPostSize();            if ((maxPostSize > 0) && (len > maxPostSize)) {                Context context = getContext();                if (context != null && context.getLogger().isDebugEnabled()) {                    context.getLogger().debug(                            sm.getString("coyoteRequest.postTooLarge"));                }                checkSwallowInput();                return;            }            byte[] formData = null;            if (len < CACHED_POST_LEN) {                if (postData == null) {                    postData = new byte[CACHED_POST_LEN];                }                formData = postData;            } else {                formData = new byte[len];            }            try {                if (readPostBody(formData, len) != len) {                    return;                }            } catch (IOException e) {                // Client disconnect                Context context = getContext();                if (context != null && context.getLogger().isDebugEnabled()) {                    context.getLogger().debug(                            sm.getString("coyoteRequest.parseParameters"),                            e);                }                return;            }            parameters.processParameters(formData, 0, len);        } else if ("chunked".equalsIgnoreCase(                coyoteRequest.getHeader("transfer-encoding"))) {            byte[] formData = null;            try {                formData = readChunkedPostBody();            } catch (IOException e) {                // Client disconnect or chunkedPostTooLarge error                Context context = getContext();                if (context != null && context.getLogger().isDebugEnabled()) {                    context.getLogger().debug(                            sm.getString("coyoteRequest.parseParameters"),                            e);                }                return;            }            if (formData != null) {                parameters.processParameters(formData, 0, formData.length);            }        }        success = true;    } finally {        if (!success) {            parameters.setParseFailed(true);        }    }}

这样一来,就说明了getParameter()方法也不能随意读取的。那么为什么它们都只能读取一次呢?

只能读取一次的原因

getInputStream()和getReader()方法都只能读取一次,而getParameter()是在单线程上可重复使用,主要是因为getParameter()中会解析流中的数据后存放在了一个LinkedHashMap中,相关的内容可以看Parameters类中的封装,在上面parseParameters()方法的源码中也可以看到一开始就生成了一个Parameters对象。后续读取的数据都存在了这个对象中。但是getInputStream()和getReader()方法就没有这样做,getInputStream()方法返回CoyoteInputStream,getReader()返回CoyoteReader,CoyoteInputStream继承了InputStream,CoyoteReader继承了BufferedReader,从源码看InputStream和BufferedReader在读取数据后,记录数据读取的坐标不会被重置,因为CoyoteInputStream和CoyoteReader都没有实现reset方法,这导致数据只能被读取一次。

坑二

Response与Request一样,getOutputStream()和getWriter()方法也是互斥的,并且Response中的body数据也只能消费一次。

互斥原因

getOutputStream

@Overridepublic ServletOutputStream getOutputStream()    throws IOException {    if (usingWriter) {        throw new IllegalStateException            (sm.getString("coyoteResponse.getOutputStream.ise"));    }    usingOutputStream = true;    if (outputStream == null) {        outputStream = new CoyoteOutputStream(outputBuffer);    }    return outputStream;}

getWriter

@Overridepublic PrintWriter getWriter()    throws IOException {    if (usingOutputStream) {        throw new IllegalStateException            (sm.getString("coyoteResponse.getWriter.ise"));    }    if (ENFORCE_ENCODING_IN_GET_WRITER) {        setCharacterEncoding(getCharacterEncoding());    }    usingWriter = true;    outputBuffer.checkConverter();    if (writer == null) {        writer = new CoyoteWriter(outputBuffer);    }    return writer;}

只能读取一次的原因

在Response中,读取是指从OutputStream中重新把body数据读出来,而OutputStream也和InputStream存在同样的问题,流只能读取一次,这里就不展开讲了。

解决方案

在Spring库中,提供了ContentCachingResponseWrapper和ContentCachingRequestWrapper两个类,分别解决了Response和Request不能重复读以及方法互斥问题。我们可以直接用ContentCachingRequestWrapper来包装Request,ContentCachingResponseWrapper来包装Response,包装后,在读取流数据的时候会将这个数据缓存一份,等读完以后,再将流数据重新写入Request或者Response就可以了。下面是一个简单的使用示例:

ContentCachingResponseWrapper responseToCache = new ContentCachingResponseWrapper(response);String responseBody = new String(responseToCache.getContentAsByteArray());responseToCache.copyBodyToResponse();

缓存一份流数据,这就是基本的解决思路,下面我们从源码层面来看一看,主要关注getContentAsByteArray()、copyBodyToResponse()方法就行:

public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {   private final FastByteArrayOutputStream content = new FastByteArrayOutputStream(1024);   private final ServletOutputStream outputStream = new ResponseServletOutputStream();   private PrintWriter writer;   private int statusCode = HttpServletResponse.SC_OK;   private Integer contentLength;   /**    * Create a new ContentCachingResponseWrapper for the given servlet response.    * @param response the original servlet response    */   public ContentCachingResponseWrapper(HttpServletResponse response) {      super(response);   }   @Override   public void setStatus(int sc) {      super.setStatus(sc);      this.statusCode = sc;   }   @SuppressWarnings("deprecation")   @Override   public void setStatus(int sc, String sm) {      super.setStatus(sc, sm);      this.statusCode = sc;   }   @Override   public void sendError(int sc) throws IOException {      copyBodyToResponse(false);      try {         super.sendError(sc);      }      catch (IllegalStateException ex) {         // Possibly on Tomcat when called too late: fall back to silent setStatus         super.setStatus(sc);      }      this.statusCode = sc;   }   @Override   @SuppressWarnings("deprecation")   public void sendError(int sc, String msg) throws IOException {      copyBodyToResponse(false);      try {         super.sendError(sc, msg);      }      catch (IllegalStateException ex) {         // Possibly on Tomcat when called too late: fall back to silent setStatus         super.setStatus(sc, msg);      }      this.statusCode = sc;   }   @Override   public void sendRedirect(String location) throws IOException {      copyBodyToResponse(false);      super.sendRedirect(location);   }   @Override   public ServletOutputStream getOutputStream() throws IOException {      return this.outputStream;   }   @Override   public PrintWriter getWriter() throws IOException {      if (this.writer == null) {         String characterEncoding = getCharacterEncoding();         this.writer = (characterEncoding != null ? new ResponsePrintWriter(characterEncoding) :               new ResponsePrintWriter(WebUtils.DEFAULT_CHARACTER_ENCODING));      }      return this.writer;   }   @Override   public void flushBuffer() throws IOException {      // do not flush the underlying response as the content as not been copied to it yet   }   @Override   public void setContentLength(int len) {      if (len > this.content.size()) {         this.content.resize(len);      }      this.contentLength = len;   }   // Overrides Servlet 3.1 setContentLengthLong(long) at runtime   public void setContentLengthLong(long len) {      if (len > Integer.MAX_VALUE) {         throw new IllegalArgumentException("Content-Length exceeds ContentCachingResponseWrapper's maximum (" +               Integer.MAX_VALUE + "): " + len);      }      int lenInt = (int) len;      if (lenInt > this.content.size()) {         this.content.resize(lenInt);      }      this.contentLength = lenInt;   }   @Override   public void setBufferSize(int size) {      if (size > this.content.size()) {         this.content.resize(size);      }   }   @Override   public void resetBuffer() {      this.content.reset();   }   @Override   public void reset() {      super.reset();      this.content.reset();   }   /**    * Return the status code as specified on the response.    */   public int getStatusCode() {      return this.statusCode;   }   /**    * Return the cached response content as a byte array.    */   public byte[] getContentAsByteArray() {      return this.content.toByteArray();   }   /**    * Return an {@link InputStream} to the cached content.    * @since 4.2    */   public InputStream getContentInputStream() {      return this.content.getInputStream();   }   /**    * Return the current size of the cached content.    * @since 4.2    */   public int getContentSize() {      return this.content.size();   }   /**    * Copy the complete cached body content to the response.    * @since 4.2    */   public void copyBodyToResponse() throws IOException {      copyBodyToResponse(true);   }   /**    * Copy the cached body content to the response.    * @param complete whether to set a corresponding content length    * for the complete cached body content    * @since 4.2    */   protected void copyBodyToResponse(boolean complete) throws IOException {      if (this.content.size() > 0) {         HttpServletResponse rawResponse = (HttpServletResponse) getResponse();         if ((complete || this.contentLength != null) && !rawResponse.isCommitted()) {            rawResponse.setContentLength(complete ? this.content.size() : this.contentLength);            this.contentLength = null;         }         this.content.writeTo(rawResponse.getOutputStream());         this.content.reset();         if (complete) {            super.flushBuffer();         }      }   }   private class ResponseServletOutputStream extends ServletOutputStream {      @Override      public void write(int b) throws IOException {         content.write(b);      }      @Override      public void write(byte[] b, int off, int len) throws IOException {         content.write(b, off, len);      }   }   private class ResponsePrintWriter extends PrintWriter {      public ResponsePrintWriter(String characterEncoding) throws UnsupportedEncodingException {         super(new OutputStreamWriter(content, characterEncoding));      }      @Override      public void write(char buf[], int off, int len) {         super.write(buf, off, len);         super.flush();      }      @Override      public void write(String s, int off, int len) {         super.write(s, off, len);         super.flush();      }      @Override      public void write(int c) {         super.write(c);         super.flush();      }   }}

而ContentCachingRequestWrapper的解决思路也是差不多,我这里就不展开了,有兴趣的可以直接查看源码。

作者:加点代码调调味
原文链接:https://juejin.im/post/5e6aee11e51d452703137ce6

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值