我正在尝试访问 Java Servlet 过滤器中的两个 http 请求参数,这里没有什么新东西,但惊讶地发现这些参数已经被使用了!因此,它在过滤器链中不再可用。
似乎这只发生在参数进入 POST 请求正文(例如表单提交)时。
有没有办法读取参数而不消耗它们?
到目前为止,我只找到了这个参考:Servlet Filter using request.getParameter lost Form data。
谢谢!
我正在尝试访问 Java Servlet 过滤器中的两个 http 请求参数,这里没有什么新东西,但惊讶地发现这些参数已经被使用了!因此,它在过滤器链中不再可用。
似乎这只发生在参数进入 POST 请求正文(例如表单提交)时。
有没有办法读取参数而不消耗它们?
到目前为止,我只找到了这个参考:Servlet Filter using request.getParameter lost Form data。
谢谢!
顺便说一句,解决这个问题的另一种方法是不使用过滤器链,而是构建自己的拦截器组件,也许使用方面,它可以对解析的请求体进行操作。它也可能更有效,因为您只需将请求InputStream
转换为您自己的模型对象一次。
但是,我仍然认为希望多次读取请求正文是合理的,特别是当请求通过过滤器链时。我通常会将过滤器链用于我想保留在 HTTP 层的某些操作,与服务组件分离。
正如Will Hartung所建议的那样,我通过扩展HttpServletRequestWrapper
、使用请求InputStream
并基本上缓存字节来实现这一点。
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();
}
}
}
现在可以通过在将原始请求传递给过滤器链之前包装原始请求来多次读取请求正文:
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);
}
}
此解决方案还允许您通过方法多次读取请求正文,getParameterXXX
因为底层调用getInputStream()
当然会读取缓存的请求InputStream
。
编辑
对于较新版本的ServletInputStream
界面。您需要提供更多方法的实现,例如isReady
等。请参阅下面评论中提供的setReadListener
这个问题。
我知道我迟到了,但这个问题对我来说仍然很重要,而且这篇 SO 帖子是谷歌的热门文章之一。我将继续发布我的解决方案,希望其他人可以节省几个小时。
就我而言,我需要用他们的身体记录所有请求和响应。使用 Spring Framework 答案其实很简单,只需使用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
}
}
}
因此,这基本上是 Lathy 的答案,但已针对 ServletInputStream 的新要求进行了更新。
即(对于 ServletInputStream),必须实现:
public abstract boolean isFinished();
public abstract boolean isReady();
public abstract void setReadListener(ReadListener var1);
这是编辑后的 Lathy 的对象
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
public class RequestWrapper extends HttpServletRequestWrapper {
private String _body;
public RequestWrapper(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 {
CustomServletInputStream kid = new CustomServletInputStream(_body.getBytes());
return kid;
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
}
在某个地方(??)我发现了这个(这是处理“额外”方法的一流课程。
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
public class CustomServletInputStream extends ServletInputStream {
private byte[] myBytes;
private int lastIndexRetrieved = -1;
private ReadListener readListener = null;
public CustomServletInputStream(String s) {
try {
this.myBytes = s.getBytes("UTF-8");
} catch (UnsupportedEncodingException ex) {
throw new IllegalStateException("JVM did not support UTF-8", ex);
}
}
public CustomServletInputStream(byte[] inputBytes) {
this.myBytes = inputBytes;
}
@Override
public boolean isFinished() {
return (lastIndexRetrieved == myBytes.length - 1);
}
@Override
public boolean isReady() {
// This implementation will never block
// We also never need to call the readListener from this method, as this method will never return false
return isFinished();
}
@Override
public void setReadListener(ReadListener readListener) {
this.readListener = readListener;
if (!isFinished()) {
try {
readListener.onDataAvailable();
} catch (IOException e) {
readListener.onError(e);
}
} else {
try {
readListener.onAllDataRead();
} catch (IOException e) {
readListener.onError(e);
}
}
}
@Override
public int read() throws IOException {
int i;
if (!isFinished()) {
i = myBytes[lastIndexRetrieved + 1];
lastIndexRetrieved++;
if (isFinished() && (readListener != null)) {
try {
readListener.onAllDataRead();
} catch (IOException ex) {
readListener.onError(ex);
throw ex;
}
}
return i;
} else {
return -1;
}
}
};
最终,我只是想记录请求。上面的 frankensteined 碎片帮助我创建了下面的。
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.Principal;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
//one or the other based on spring version
//import org.springframework.boot.autoconfigure.web.ErrorAttributes;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* A filter which logs web requests that lead to an error in the system.
*/
@Component
public class LogRequestFilter extends OncePerRequestFilter implements Ordered {
// I tried apache.commons and slf4g loggers. (one or the other in these next 2 lines of declaration */
//private final static org.apache.commons.logging.Log logger = org.apache.commons.logging.LogFactory.getLog(LogRequestFilter.class);
private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(LogRequestFilter.class);
// put filter at the end of all other filters to make sure we are processing after all others
private int order = Ordered.LOWEST_PRECEDENCE - 8;
private ErrorAttributes errorAttributes;
@Override
public int getOrder() {
return order;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String temp = ""; /* for a breakpoint, remove for production/real code */
/* change to true for easy way to comment out this code, remove this if-check for production/real code */
if (false) {
filterChain.doFilter(request, response);
return;
}
/* make a "copy" to avoid issues with body-can-only-read-once issues */
RequestWrapper reqWrapper = new RequestWrapper(request);
int status = HttpStatus.INTERNAL_SERVER_ERROR.value();
// pass through filter chain to do the actual request handling
filterChain.doFilter(reqWrapper, response);
status = response.getStatus();
try {
Map<String, Object> traceMap = getTrace(reqWrapper, status);
// body can only be read after the actual request handling was done!
this.getBodyFromTheRequestCopy(reqWrapper, traceMap);
/* now do something with all the pieces of information gatherered */
this.logTrace(reqWrapper, traceMap);
} catch (Exception ex) {
logger.error("LogRequestFilter FAILED: " + ex.getMessage(), ex);
}
}
private void getBodyFromTheRequestCopy(RequestWrapper rw, Map<String, Object> trace) {
try {
if (rw != null) {
byte[] buf = IOUtils.toByteArray(rw.getInputStream());
//byte[] buf = rw.getInputStream();
if (buf.length > 0) {
String payloadSlimmed;
try {
String payload = new String(buf, 0, buf.length, rw.getCharacterEncoding());
payloadSlimmed = payload.trim().replaceAll(" +", " ");
} catch (UnsupportedEncodingException ex) {
payloadSlimmed = "[unknown]";
}
trace.put("body", payloadSlimmed);
}
}
} catch (IOException ioex) {
trace.put("body", "EXCEPTION: " + ioex.getMessage());
}
}
private void logTrace(HttpServletRequest request, Map<String, Object> trace) {
Object method = trace.get("method");
Object path = trace.get("path");
Object statusCode = trace.get("statusCode");
logger.info(String.format("%s %s produced an status code '%s'. Trace: '%s'", method, path, statusCode,
trace));
}
protected Map<String, Object> getTrace(HttpServletRequest request, int status) {
Throwable exception = (Throwable) request.getAttribute("javax.servlet.error.exception");
Principal principal = request.getUserPrincipal();
Map<String, Object> trace = new LinkedHashMap<String, Object>();
trace.put("method", request.getMethod());
trace.put("path", request.getRequestURI());
if (null != principal) {
trace.put("principal", principal.getName());
}
trace.put("query", request.getQueryString());
trace.put("statusCode", status);
Enumeration headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String key = (String) headerNames.nextElement();
String value = request.getHeader(key);
trace.put("header:" + key, value);
}
if (exception != null && this.errorAttributes != null) {
trace.put("error", this.errorAttributes
.getErrorAttributes((WebRequest) new ServletRequestAttributes(request), true));
}
return trace;
}
}
请对此代码持保留态度。
最重要的“测试”是 POST 是否与有效负载一起使用。这将暴露“双重阅读”问题。
伪示例代码
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("myroute")
public class MyController {
@RequestMapping(method = RequestMethod.POST, produces = "application/json")
@ResponseBody
public String getSomethingExample(@RequestBody MyCustomObject input) {
String returnValue = "";
return returnValue;
}
}
如果您只想测试,可以将“MyCustomObject”替换为普通的 ole“Object”。
这个答案是从几个不同的 SOF 帖子和示例中得出的 frankensteined。但是花了一段时间才将它们整合在一起,所以我希望它对未来的读者有所帮助。
请在我之前支持 Lathy 的回答。没有它,我不可能走到这一步。
以下是我在解决这个问题时遇到的一个/一些例外情况。
已经为此请求调用了 getReader()
看起来我“借来”的一些地方在这里:
http://slackspace.de/articles/log-request-body-with-spring-boot/
https://howtodoinjava.com/servlets/httpservletrequestwrapper-example-read-request-body/
https://www.oodlestechnologies.com/blogs/How-to-create-duplicate-object-of-httpServletRequest-object
2021 年 1 月附录。
我已经学会了上述代码不起作用的艰难方式
x-www-form-urlencoded
考虑下面的例子:
@CrossOrigin
@ResponseBody
@PostMapping(path = "/mypath", consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE})
public ResponseEntity myMethodName(@RequestParam Map<String, String> parameters
) {
/* DO YOU GET ANY PARAMETERS HERE? Or are they empty because of logging/auditing filter ?*/
return new ResponseEntity(HttpStatus.OK);
}
我必须在这里查看其他几个示例。
我想出了一个明确适用于 APPLICATION_FORM_URLENCODED_VALUE 的“包装器”
import org.apache.commons.io.IOUtils;
import org.springframework.http.MediaType;
import org.springframework.web.util.ContentCachingRequestWrapper;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* Makes a "copy" of the HttpRequest so the body can be accessed more than 1 time.
* WORKS WITH APPLICATION_FORM_URLENCODED_VALUE
* See : https://stackoverflow.com/questions/44182370/why-do-we-wrap-httpservletrequest-the-api-provides-an-httpservletrequestwrappe/44187955#44187955
*/
public final class AppFormUrlEncodedSpecificContentCachingRequestWrapper extends ContentCachingRequestWrapper {
public static final String ERROR_MSG_CONTENT_TYPE_NOT_SUPPORTED = "ContentType not supported. (Input ContentType(s)=\"%1$s\", Supported ContentType(s)=\"%2$s\")";
public static final String ERROR_MSG_PERSISTED_CONTENT_CACHING_REQUEST_WRAPPER_CONSTRUCTOR_FAILED = "AppFormUrlEncodedSpecificContentCachingRequestWrapper constructor failed";
private static final org.slf4j.Logger LOGGER = org.slf4j.LoggerFactory.getLogger(AppFormUrlEncodedSpecificContentCachingRequestWrapper.class);
private byte[] body;
private ServletInputStream inputStream;
public AppFormUrlEncodedSpecificContentCachingRequestWrapper(HttpServletRequest request) {
super(request);
super.getParameterMap(); // init cache in ContentCachingRequestWrapper. THIS IS THE VITAL CALL so that "@RequestParam Map<String, String> parameters" are populated on the REST Controller. See https://stackoverflow.com/questions/10210645/http-servlet-request-lose-params-from-post-body-after-read-it-once/64924380#64924380
String contentType = request.getContentType();
/* EXPLICTLY check for APPLICATION_FORM_URLENCODED_VALUE and allow nothing else */
if (null == contentType || !contentType.equalsIgnoreCase(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) {
IllegalArgumentException ioex = new IllegalArgumentException(String.format(ERROR_MSG_CONTENT_TYPE_NOT_SUPPORTED, contentType, MediaType.APPLICATION_FORM_URLENCODED_VALUE));
LOGGER.error(ERROR_MSG_PERSISTED_CONTENT_CACHING_REQUEST_WRAPPER_CONSTRUCTOR_FAILED, ioex);
throw ioex;
}
try {
loadBody(request);
} catch (IOException ioex) {
throw new RuntimeException(ioex);
}
}
private void loadBody(HttpServletRequest request) throws IOException {
body = IOUtils.toByteArray(request.getInputStream());
inputStream = new CustomServletInputStream(this.getBody());
}
private byte[] getBody() {
return body;
}
@Override
public ServletInputStream getInputStream() throws IOException {
if (inputStream != null) {
return inputStream;
}
return super.getInputStream();
}
}
请注意 Andrew Sneck 在同一页面上的回答。差不多就是这样:https ://programmersought.com/article/23981013626/
我没有时间协调上述两个实现(我的两个)。
所以我创建了一个工厂来从两者中“选择”:
import org.springframework.http.MediaType;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.IOException;
/**
* Factory to return different concretes of HttpServletRequestWrapper. APPLICATION_FORM_URLENCODED_VALUE needs a different concrete.
*/
public class HttpServletRequestWrapperFactory {
public static final String ERROR_MSG_HTTP_SERVLET_REQUEST_WRAPPER_FACTORY_CREATE_HTTP_SERVLET_REQUEST_WRAPPER_FAILED = "HttpServletRequestWrapperFactory createHttpServletRequestWrapper FAILED";
public static HttpServletRequestWrapper createHttpServletRequestWrapper(final HttpServletRequest request) {
HttpServletRequestWrapper returnItem = null;
if (null != request) {
String contentType = request.getContentType();
if (null != contentType && contentType.equalsIgnoreCase(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) {
returnItem = new AppFormUrlEncodedSpecificContentCachingRequestWrapper(request);
} else {
try {
returnItem = new PersistedBodyRequestWrapper(request);
} catch (IOException ioex) {
throw new RuntimeException(ERROR_MSG_HTTP_SERVLET_REQUEST_WRAPPER_FACTORY_CREATE_HTTP_SERVLET_REQUEST_WRAPPER_FAILED, ioex);
}
}
}
return returnItem;
}
}
下面是与 JSON 等一起使用的“其他”。它是工厂可以输出的另一种具体。我把它放在这里是为了让我的 Jan 2021 APPEND 保持一致。我不知道下面的代码是否与我的原始答案完全一致:
import org.springframework.http.MediaType;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Map;
/**
* Makes a "copy" of the HttpRequest so the body can be accessed more than 1 time.
* See : https://stackoverflow.com/questions/44182370/why-do-we-wrap-httpservletrequest-the-api-provides-an-httpservletrequestwrappe/44187955#44187955
* DOES NOT WORK WITH APPLICATION_FORM_URLENCODED_VALUE
*/
public final class PersistedBodyRequestWrapper extends HttpServletRequestWrapper {
public static final String ERROR_MSG_CONTENT_TYPE_NOT_SUPPORTED = "ContentType not supported. (ContentType=\"%1$s\")";
public static final String ERROR_MSG_PERSISTED_BODY_REQUEST_WRAPPER_CONSTRUCTOR_FAILED = "PersistedBodyRequestWrapper constructor FAILED";
private static final org.slf4j.Logger LOGGER = org.slf4j.LoggerFactory.getLogger(PersistedBodyRequestWrapper.class);
private String persistedBody;
private final Map<String, String[]> parameterMap;
public PersistedBodyRequestWrapper(final HttpServletRequest request) throws IOException {
super(request);
String contentType = request.getContentType();
/* Allow everything EXCEPT APPLICATION_FORM_URLENCODED_VALUE */
if (null != contentType && contentType.equalsIgnoreCase(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) {
IllegalArgumentException ioex = new IllegalArgumentException(String.format(ERROR_MSG_CONTENT_TYPE_NOT_SUPPORTED, MediaType.APPLICATION_FORM_URLENCODED_VALUE));
LOGGER.error(ERROR_MSG_PERSISTED_BODY_REQUEST_WRAPPER_CONSTRUCTOR_FAILED, ioex);
throw ioex;
}
parameterMap = request.getParameterMap();
this.persistedBody = "";
BufferedReader bufferedReader = request.getReader();
String line;
while ((line = bufferedReader.readLine()) != null) {
this.persistedBody += line;
}
}
@Override
public ServletInputStream getInputStream() throws IOException {
CustomServletInputStream csis = new CustomServletInputStream(this.persistedBody.getBytes(StandardCharsets.UTF_8));
return csis;
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
@Override
public Map<String, String[]> getParameterMap() {
return this.parameterMap;
}
}
以上答案非常有帮助,但在我的经验中仍然存在一些问题。在 tomcat 7 servlet 3.0 上,getParamter 和 getParamterValues 也必须被覆盖。这里的解决方案包括get-query参数和post-body。它允许轻松获取原始字符串。
与其他解决方案一样,它使用 Apache commons-io 和 Googles Guava。
在此解决方案中,getParameter* 方法不会抛出 IOException,但它们使用可能抛出 IOException 的 super.getInputStream()(获取主体)。我抓住它并抛出 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();
}
}
唯一的方法是您自己在过滤器中使用整个输入流,从中获取您想要的内容,然后为您读取的内容创建一个新的 InputStream,并将该 InputStream 放入 ServletRequestWrapper(或 HttpServletRequestWrapper)。
缺点是您必须自己解析有效负载,该标准并未使您可以使用该功能。
附录——
正如我所说,您需要查看 HttpServletRequestWrapper。
在过滤器中,您继续调用 FilterChain.doFilter(request, response)。
对于普通过滤器,请求和响应与传入过滤器的请求和响应相同。不一定是这样。您可以用您自己的请求和/或响应替换它们。
HttpServletRequestWrapper 是专门为实现这一点而设计的。您将原始请求传递给它,然后您可以拦截所有调用。您创建自己的子类,并用您自己的方法替换 getInputStream 方法。您无法更改原始请求的输入流,因此您拥有此包装器并返回您自己的输入流。
最简单的情况是将原始请求输入流消耗到字节缓冲区中,对它做任何你想做的事情,然后从该缓冲区创建一个新的 ByteArrayInputStream。这是您的包装器中返回的内容,它被传递给 FilterChain.doFilter 方法。
您需要继承 ServletInputStream 并为您的 ByteArrayInputStream 创建另一个包装器,但这也不是什么大问题。
我也有同样的问题,我相信下面的代码更简单,它对我有用,
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()));
}
}
在过滤器java类中,
HttpServletRequest properRequest = ((HttpServletRequest) req);
MultiReadHttpServletRequest wrappedRequest = new MultiReadHttpServletRequest(properRequest);
req = wrappedRequest;
inputJson = IOUtils.toString(req.getReader());
System.out.println("body"+inputJson);
如果您有任何疑问,请告诉我
我为任何格式的请求正文找到了很好的解决方案。我进行了测试application/x-www-form-urlencoded
,application/json
两者都工作得很好。该问题ContentCachingRequestWrapper
仅适用于x-www-form-urlencoded
请求正文,但不适用于例如 json。我找到了 json link的解决方案。它有麻烦,它不支持x-www-form-urlencoded
。我在我的代码中加入了两者:
import org.apache.commons.io.IOUtils;
import org.springframework.web.util.ContentCachingRequestWrapper;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
public class MyContentCachingRequestWrapper extends ContentCachingRequestWrapper {
private byte[] body;
public MyContentCachingRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
super.getParameterMap(); // init cache in ContentCachingRequestWrapper
body = super.getContentAsByteArray(); // first option for application/x-www-form-urlencoded
if (body.length == 0) {
try {
body = IOUtils.toByteArray(super.getInputStream()); // second option for other body formats
} catch (IOException ex) {
body = new byte[0];
}
}
}
public byte[] getBody() {
return body;
}
@Override
public ServletInputStream getInputStream() {
return new RequestCachingInputStream(body);
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream(), getCharacterEncoding()));
}
private static class RequestCachingInputStream extends ServletInputStream {
private final ByteArrayInputStream inputStream;
public RequestCachingInputStream(byte[] bytes) {
inputStream = new ByteArrayInputStream(bytes);
}
@Override
public int read() throws IOException {
return inputStream.read();
}
@Override
public boolean isFinished() {
return inputStream.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readlistener) {
}
}
}
Spring 对此具有内置支持AbstractRequestLoggingFilter
:
@Bean
public Filter loggingFilter(){
final AbstractRequestLoggingFilter filter = new AbstractRequestLoggingFilter() {
@Override
protected void beforeRequest(final HttpServletRequest request, final String message) {
}
@Override
protected void afterRequest(final HttpServletRequest request, final String message) {
}
};
filter.setIncludePayload(true);
filter.setIncludeQueryString(false);
filter.setMaxPayloadLength(1000000);
return filter;
}
不幸的是,您仍然无法直接从请求中读取有效负载,但字符串消息参数将包含有效负载,因此您可以从那里获取它,如下所示:
String body = message.substring(message.indexOf("{"), message.lastIndexOf("]"));
只是覆盖getInputStream()
在我的情况下不起作用。我的服务器实现似乎在不调用此方法的情况下解析参数。我没有找到任何其他方法,但也重新实现了所有四个 getParameter* 方法。以下是getParameterMap
(使用 Apache Http Client 和 Google 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;
}
如果您可以控制请求,则可以将内容类型设置为binary/octet-stream。这允许在不消耗输入流的情况下查询参数。
但是,这可能特定于某些应用程序服务器。我只测试了tomcat,根据https://stackoverflow.com/a/11434646/957103 ,码头的行为似乎相同。
Spring类ContentCachingRequestWrapper的getContentAsByteArray()方法会多次读取body,但是同一个类的getInputStream()和getReader()方法不会多次读取body:
“这个类通过消费InputStream来缓存请求体。如果我们在其中一个过滤器中读取了InputStream,那么过滤器链中的其他后续过滤器就不能再读取它了。由于这个限制,这个类并不适合所有情况。”
在我的情况下,解决此问题的更通用解决方案是将以下三个类添加到我的 Spring boot 项目(以及 pom 文件所需的依赖项):
CachedBodyHttpServletRequest.java:
public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {
private byte[] cachedBody;
public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
InputStream requestInputStream = request.getInputStream();
this.cachedBody = StreamUtils.copyToByteArray(requestInputStream);
}
@Override
public ServletInputStream getInputStream() throws IOException {
return new CachedBodyServletInputStream(this.cachedBody);
}
@Override
public BufferedReader getReader() throws IOException {
// Create a reader from cachedContent
// and return it
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody);
return new BufferedReader(new InputStreamReader(byteArrayInputStream));
}
}
CachedBodyServletInputStream.java:
public class CachedBodyServletInputStream extends ServletInputStream {
private InputStream cachedBodyInputStream;
public CachedBodyServletInputStream(byte[] cachedBody) {
this.cachedBodyInputStream = new ByteArrayInputStream(cachedBody);
}
@Override
public boolean isFinished() {
try {
return cachedBodyInputStream.available() == 0;
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return false;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
throw new UnsupportedOperationException();
}
@Override
public int read() throws IOException {
return cachedBodyInputStream.read();
}
}
内容缓存过滤器.java:
@Order(value = Ordered.HIGHEST_PRECEDENCE)
@Component
@WebFilter(filterName = "ContentCachingFilter", urlPatterns = "/*")
public class ContentCachingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
System.out.println("IN ContentCachingFilter ");
CachedBodyHttpServletRequest cachedBodyHttpServletRequest = new CachedBodyHttpServletRequest(httpServletRequest);
filterChain.doFilter(cachedBodyHttpServletRequest, httpServletResponse);
}
}
我还在 pom 中添加了以下依赖项:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.0.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.0</version>
</dependency>
教程和完整的源代码位于此处: https ://www.baeldung.com/spring-reading-httpservletrequest-multiple-times
您可以使用 servlet 过滤器链,但使用原始过滤器链,您可以创建自己的请求 yourownrequests extends HttpServletRequestWrapper。
首先,我们不应该读取过滤器中的参数。通常在过滤器中读取标头以执行少量身份验证任务。话虽如此,使用 CharStreams 可以在 Filter 或 Interceptor 中完全读取 HttpRequest 正文:
String body = com.google.common.io.CharStreams.toString(request.getReader());
这根本不影响后续的读取。