Http Servlet请求在读取一次后会从POST主体中丢失参数

我想在Java Servletfilter中访问两个http请求参数,这里没有什么新东西,但很惊讶的发现参数已经被使用了! 正因为如此,它在过滤链中不再可用。

看来只有当参数进入POST请求体(例如表单提交)时才会发生这种情况。

有没有办法读取参数,而不是消耗它们?

到目前为止,我只find了这个参考: 使用request.getParameter的Servletfilter失去了表单数据 。

谢谢!

另外,解决这个问题的另一种方法是不使用filter链,而是build立自己的拦截器组件,或许使用可以在parsing的请求体上操作的方面。 它也可能更有效,因为只是将请求InputStream转换为您自己的模型对象一次。

不过,我仍然认为,当请求通过filter链时,多次读取请求体是合理的。 我通常使用filter链来进行特定的操作,我想保留在HTTP层,从服务组件中分离出来。

正如Will Hartung所build议的那样,我通过扩展HttpServletRequestWrapper来实现这个function,消费了InputStream请求并实质上caching了字节。

 public class MultiReadHttpServletRequest extends HttpServletRequestWrapper { private ByteArrayOutputStream cachedBytes; public MultiReadHttpServletRequest(HttpServletRequest request) { super(request); } @Override public ServletInputStream getInputStream() throws IOException { if (cachedBytes == null) cacheInputStream(); return new CachedServletInputStream(); } @Override public BufferedReader getReader() throws IOException{ return new BufferedReader(new InputStreamReader(getInputStream())); } private void cacheInputStream() throws IOException { /* Cache the inputstream in order to read it multiple times. For * convenience, I use apache.commons IOUtils */ cachedBytes = new ByteArrayOutputStream(); IOUtils.copy(super.getInputStream(), cachedBytes); } /* An inputstream which reads the cached request body */ public class CachedServletInputStream extends ServletInputStream { private ByteArrayInputStream input; public CachedServletInputStream() { /* create a new input stream from the cached request body */ input = new ByteArrayInputStream(cachedBytes.toByteArray()); } @Override public int read() throws IOException { return input.read(); } } } 

现在,请求主体可以通过在传递filter链之前包装原始请求来多次读取:

 public class MyFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { /* wrap the request in order to read the inputstream multiple times */ MultiReadHttpServletRequest multiReadRequest = new MultiReadHttpServletRequest((HttpServletRequest) request); /* here I read the inputstream and do my thing with it; when I pass the * wrapped request through the filter chain, the rest of the filters, and * request handlers may read the cached inputstream */ doMyThing(multiReadRequest.getInputStream()); //OR anotherUsage(multiReadRequest.getReader()); chain.doFilter(multiReadRequest, response); } } 

这个解决scheme也允许你通过getParameterXXX方法多次读取请求体,因为底层的调用是getInputStream() ,当然它会读取caching的请求InputStream

我知道我迟到了,但这个问题仍然与我有关,这个SOpost是谷歌的顶级点击之一。 我会继续发布我的解决scheme,希望别人可以节省几个小时。

在我的情况下,我需要logging所有的请求和回应与他们的身体。 使用Spring框架的答案其实很简单,只需使用ContentCachingRequestWrapper和ContentCachingResponseWrapper 。

 import org.springframework.web.util.ContentCachingRequestWrapper; import org.springframework.web.util.ContentCachingResponseWrapper; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class LoggingFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void destroy() { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest) request); ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response); try { chain.doFilter(requestWrapper, responseWrapper); } finally { String requestBody = new String(requestWrapper.getContentAsByteArray()); String responseBody = new String(responseWrapper.getContentAsByteArray()); // Do not forget this line after reading response content or actual response will be empty! responseWrapper.copyBodyToResponse(); // Write request and response body, headers, timestamps etc. to log files } } } 

唯一的方法是让你在filter中自己使用整个inputstream,从中获取所需内容,然后为读取的内容创build一个新的InputStream,并将该InputStream放入ServletRequestWrapper(或HttpServletRequestWrapper)中。

缺点是你必须自己parsing有效载荷,这个标准并没有使你能够使用这个function。

附录 –

正如我所说,你需要看看HttpServletRequestWrapper。

在filter中,您继续调用FilterChain.doFilter(request,response)。

对于简单的filter,请求和响应与传递给filter的相同。 情况并非如此。 你可以用你自己的请求和/或响应来replace那些。

HttpServletRequestWrapper是专门为了方便这个。 你把原始请求传给它,然后你可以拦截所有的呼叫。 您可以创build自己的子类,并用您自己的方法replacegetInputStream方法。 您不能更改原始请求的inputstream,所以您可以使用此包装并返回自己的inputstream。

最简单的情况是将原始请求inputstream消费到一个字节缓冲区,做任何你想要的魔法,然后从该缓冲区创build一个新的ByteArrayInputStream。 这是你的包装器中传递给FilterChain.doFilter方法的内容。

您需要inheritanceServletInputStream并为ByteArrayInputStream创build另一个包装器,但这也不算什么。

以上答案非常有帮助,但是我的经验还是有一些问题。 在tomcat 7 servlet 3.0上,getParamter和getParamterValues也必须被覆盖。 这里的解决scheme包括get-query参数和post-body。 它允许轻松获得原始string。

像其他解决scheme一样,它使用Apache commons-io和Googles Guava。

在这个解决scheme中,getParameter *方法不会抛出IOException,而是使用super.getInputStream()(获取正文),这可能会抛出IOExceptionexception。 我赶上它,并抛出runtimeException。 这不是很好。

 import com.google.common.collect.Iterables; import com.google.common.collect.ObjectArrays; import org.apache.commons.io.IOUtils; import org.apache.http.NameValuePair; import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.entity.ContentType; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; /** * Purpose of this class is to make getParameter() return post data AND also be able to get entire * body-string. In native implementation any of those two works, but not both together. */ public class MultiReadHttpServletRequest extends HttpServletRequestWrapper { public static final String UTF8 = "UTF-8"; public static final Charset UTF8_CHARSET = Charset.forName(UTF8); private ByteArrayOutputStream cachedBytes; private Map<String, String[]> parameterMap; public MultiReadHttpServletRequest(HttpServletRequest request) { super(request); } public static void toMap(Iterable<NameValuePair> inputParams, Map<String, String[]> toMap) { for (NameValuePair e : inputParams) { String key = e.getName(); String value = e.getValue(); if (toMap.containsKey(key)) { String[] newValue = ObjectArrays.concat(toMap.get(key), value); toMap.remove(key); toMap.put(key, newValue); } else { toMap.put(key, new String[]{value}); } } } @Override public ServletInputStream getInputStream() throws IOException { if (cachedBytes == null) cacheInputStream(); return new CachedServletInputStream(); } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(getInputStream())); } private void cacheInputStream() throws IOException { /* Cache the inputStream in order to read it multiple times. For * convenience, I use apache.commons IOUtils */ cachedBytes = new ByteArrayOutputStream(); IOUtils.copy(super.getInputStream(), cachedBytes); } @Override public String getParameter(String key) { Map<String, String[]> parameterMap = getParameterMap(); String[] values = parameterMap.get(key); return values != null && values.length > 0 ? values[0] : null; } @Override public String[] getParameterValues(String key) { Map<String, String[]> parameterMap = getParameterMap(); return parameterMap.get(key); } @Override public Map<String, String[]> getParameterMap() { if (parameterMap == null) { Map<String, String[]> result = new LinkedHashMap<String, String[]>(); decode(getQueryString(), result); decode(getPostBodyAsString(), result); parameterMap = Collections.unmodifiableMap(result); } return parameterMap; } private void decode(String queryString, Map<String, String[]> result) { if (queryString != null) toMap(decodeParams(queryString), result); } private Iterable<NameValuePair> decodeParams(String body) { Iterable<NameValuePair> params = URLEncodedUtils.parse(body, UTF8_CHARSET); try { String cts = getContentType(); if (cts != null) { ContentType ct = ContentType.parse(cts); if (ct.getMimeType().equals(ContentType.APPLICATION_FORM_URLENCODED.getMimeType())) { List<NameValuePair> postParams = URLEncodedUtils.parse(IOUtils.toString(getReader()), UTF8_CHARSET); params = Iterables.concat(params, postParams); } } } catch (IOException e) { throw new IllegalStateException(e); } return params; } public String getPostBodyAsString() { try { if (cachedBytes == null) cacheInputStream(); return cachedBytes.toString(UTF8); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); } } /* An inputStream which reads the cached request body */ public class CachedServletInputStream extends ServletInputStream { private ByteArrayInputStream input; public CachedServletInputStream() { /* create a new input stream from the cached request body */ input = new ByteArrayInputStream(cachedBytes.toByteArray()); } @Override public int read() throws IOException { return input.read(); } } @Override public String toString() { String query = dk.bnr.util.StringUtil.nullToEmpty(getQueryString()); StringBuilder sb = new StringBuilder(); sb.append("URL='").append(getRequestURI()).append(query.isEmpty() ? "" : "?" + query).append("', body='"); sb.append(getPostBodyAsString()); sb.append("'"); return sb.toString(); } } 

只是覆盖getInputStream()在我的情况下不起作用。 我的服务器实现似乎parsing参数,而无需调用此方法。 我没有find任何其他的方式,但重新实现全部四个getParameter *方法。 这里是getParameterMap (Apache Http客户端和谷歌Guava库使用)的代码:

 @Override public Map<String, String[]> getParameterMap() { Iterable<NameValuePair> params = URLEncodedUtils.parse(getQueryString(), NullUtils.UTF8); try { String cts = getContentType(); if (cts != null) { ContentType ct = ContentType.parse(cts); if (ct.getMimeType().equals(ContentType.APPLICATION_FORM_URLENCODED.getMimeType())) { List<NameValuePair> postParams = URLEncodedUtils.parse(IOUtils.toString(getReader()), NullUtils.UTF8); params = Iterables.concat(params, postParams); } } } catch (IOException e) { throw new IllegalStateException(e); } Map<String, String[]> result = toMap(params); return result; } public static Map<String, String[]> toMap(Iterable<NameValuePair> body) { Map<String, String[]> result = new LinkedHashMap<>(); for (NameValuePair e : body) { String key = e.getName(); String value = e.getValue(); if (result.containsKey(key)) { String[] newValue = ObjectArrays.concat(result.get(key), value); result.remove(key); result.put(key, newValue); } else { result.put(key, new String[] {value}); } } return result; } 

我也有同样的问题,我相信下面的代码更简单,它为我工作,

 public class MultiReadHttpServletRequest extends HttpServletRequestWrapper { private String _body; public MultiReadHttpServletRequest(HttpServletRequest request) throws IOException { super(request); _body = ""; BufferedReader bufferedReader = request.getReader(); String line; while ((line = bufferedReader.readLine()) != null){ _body += line; } } @Override public ServletInputStream getInputStream() throws IOException { final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(_body.getBytes()); return new ServletInputStream() { public int read() throws IOException { return byteArrayInputStream.read(); } }; } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(this.getInputStream())); } } 

在filterjava类中,

  HttpServletRequest properRequest = ((HttpServletRequest) req); MultiReadHttpServletRequest wrappedRequest = new MultiReadHttpServletRequest(properRequest); req = wrappedRequest; inputJson = IOUtils.toString(req.getReader()); System.out.println("body"+inputJson); 

如果您有任何疑问,请让我知道

看看(或使用)Spring的AbstractRequestLoggingFilter

如果您可以控制请求,则可以将内容types设置为二进制/八位字节stream 。 这允许查询参数而不消耗inputstream。

但是,这可能是特定于某些应用程序服务器的。 我只testing了tomcat,jetty似乎按照https://stackoverflow.com/a/11434646/957103相同的方式。;

首先,我们不应该读取filter中的参数。 通常在filter中读取头文件以执行less量身份validation任务。 话虽如此,可以通过使用CharStreams在Filter或Interceptor中完全读取HttpRequest主体:

 String body = com.google.common.io.CharStreams.toString(request.getReader()); 

这根本不影响后续的读取。

你可以使用servletfilter链,而是使用原来的,你可以创build自己的请求yourownrequests扩展HttpServletRequestWrapper。