先来看javax.servlet.ServletRequest中getInputStream、getReader以及getParameter的注释说明
1 /** 2 * Retrieves the body of the request as binary data using 3 * a {@link ServletInputStream}. Either this method or 4 * {@link #getReader} may be called to read the body, not both. 5 * 6 * @return a {@link ServletInputStream} object containing 7 * the body of the request 8 * 9 * @exception IllegalStateException if the {@link #getReader} method 10 * has already been called for this request 11 * 12 * @exception IOException if an input or output exception occurred 13 */ 14 public ServletInputStream getInputStream() throws IOException;
1 /** 2 * Retrieves the body of the request as character data using 3 * a <code>BufferedReader</code>. The reader translates the character 4 * data according to the character encoding used on the body. 5 * Either this method or {@link #getInputStream} may be called to read the 6 * body, not both. 7 * 8 * @return a <code>BufferedReader</code> containing the body of the request 9 * 10 * @exception UnsupportedEncodingException if the character set encoding 11 * used is not supported and the text cannot be decoded 12 * 13 * @exception IllegalStateException if {@link #getInputStream} method 14 * has been called on this request 15 * 16 * @exception IOException if an input or output exception occurred 17 * 18 * @see #getInputStream 19 */ 20 public BufferedReader getReader() throws IOException;
1 /** 2 * Returns the value of a request parameter as a <code>String</code>, 3 * or <code>null</code> if the parameter does not exist. Request parameters 4 * are extra information sent with the request. For HTTP servlets, 5 * parameters are contained in the query string or posted form data. 6 * 7 * <p>You should only use this method when you are sure the 8 * parameter has only one value. If the parameter might have 9 * more than one value, use {@link #getParameterValues}. 10 * 11 * <p>If you use this method with a multivalued 12 * parameter, the value returned is equal to the first value 13 * in the array returned by <code>getParameterValues</code>. 14 * 15 * <p>If the parameter data was sent in the request body, such as occurs 16 * with an HTTP POST request, then reading the body directly via {@link 17 * #getInputStream} or {@link #getReader} can interfere 18 * with the execution of this method. 19 * 20 * @param name a <code>String</code> specifying the name of the parameter 21 * 22 * @return a <code>String</code> representing the single value of 23 * the parameter 24 * 25 * @see #getParameterValues 26 */ 27 public String getParameter(String name);
通过注释我们可以知道,getInputStream和getReader只能调用其一,如果已经调用了一个,再去调用另一个时就会抛IllegalStateException(同一个可以被多次调用)。当body中存有参数数据时,通过getInputStream、getReader读取数据,getParameter会被影响。
tomcat对于getInputStream和getReader的处理
tomcat通过两个using标识量来实现getInputStream、getReader的互斥访问(并没有线程安全的相关处理),二者只能调用其一,只要不被关闭输入流,就可以多次调用(即便是多次调用,其实返回的也是同一个对象,只说单线程),但一旦关闭就不可以再从输入流中读取数据了(使用的应该是shutdownInputStream)。
关闭输入后,socket处于半关闭状态(使用的应该是shutdownInputStream),此后仍然可以向client写数据。
注意,getInputStream和getReader针对的是body数据,并不会涉及请求行和头信息,也是因为这样,通常我们会将token信息放在header中,在filter处理token后就不会影响到之后的流处理了。
1 public ServletInputStream getInputStream() throws IOException { 2 //getReader类似 3 if (usingReader) { 4 throw new IllegalStateException 5 (sm.getString("coyoteRequest.getInputStream.ise")); 6 } 7 //getReader类似 8 usingInputStream = true; 9 if (inputStream == null) { 10 //getReader是new CoyoteReader(inputBuffer) 11 //getParameter时调用的是getStream,只是比getInputStream少了using的相关处理, 12 //同样是inputStream = new CoyoteInputStream(inputBuffer) 13 inputStream = new CoyoteInputStream(inputBuffer); 14 } 15 return inputStream; 16 }
tomcat对于getParameter的处理
可以看到getParameter只会处理multipart/form-data、application/x-www-form-urlencoded两种Content-Type,后者就是普通的form,可以通过getParameter(String name)得到对应的value。如果是前者(多部),getParameter(String name)是得不到value的,但可以通过getPart(String name)得到Part,再进一步处理。tomcat对于两种Content-Type数据的存储是分开的,multipart/form-data存在了Collection<Part> parts,application/x-www-form-urlencoded存在了ParameterMap<String, String[]> parameterMap,getParameter时虽然会填充parts,但并不会从parts获取元素(当然,调用getPart也会解析parts,但不会解析parameters)。
注意,如果是post请求,getParameter会涉及body数据。在tomcat的实现中,getParameter会先处理url中的查询参数,然后会检查getInputStream或getReader是否被调用,如果被调用过则返回,如果未被调用,则会完全读取body数据(并没有using标记),此后如果再调用getInputStream或getReader处理body,就没有数据可读了(返回-1)。也就是说getParameter、getInputStream和getReader只能有效调用其一。
1 //如果调用过getInputStream或getReader,则不对再body处理。 2 //之前已经对url上的查询参数做了处理,getParameter可以无障碍访问url上的查询参数, 3 //之后的逻辑仅针对body 4 if (usingInputStream || usingReader) { 5 success = true; 6 return; 7 } 8 9 if( !getConnector().isParseBodyMethod(getMethod()) ) { 10 success = true; 11 return; 12 } 13 14 String contentType = getContentType(); 15 if (contentType == null) { 16 contentType = ""; 17 } 18 int semicolon = contentType.indexOf(‘;‘); 19 if (semicolon >= 0) { 20 contentType = contentType.substring(0, semicolon).trim(); 21 } else { 22 contentType = contentType.trim(); 23 } 24 //getParameter方法会处理multipart/form-data、application/x-www-form-urlencoded两种Content-Type(都是form), 25 //但getParameter只能获取到application/x-www-form-urlencoded的数据 26 if ("multipart/form-data".equals(contentType)) { 27 //多部会被放到parts容器中 28 parseParts(false); 29 success = true; 30 return; 31 } 32 if (!("application/x-www-form-urlencoded".equals(contentType))) { 33 success = true; 34 return; 35 } 36 //对application/x-www-form-urlencoded数据进一步处理,数据会存放在parameterMap中
如果我们在filter中对body进行了处理(比如将token存在了body中,需要在filter中处理token),那么在之后的流程中就无法再对body处理了,这该怎么办?
springmvc提供了一个辅助类可以解决类似问题,即org.springframework.web.util.ContentCachingRequestWrapper,其大致原理就是将body数据放在缓存中,以便后续访问。