【问题标题】:Http Servlet request lose params from POST body after read it onceHttp Servlet 请求在读取一次后从 POST 正文中丢失参数
【发布时间】:2012-04-29 22:31:37
【问题描述】:

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

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

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

到目前为止,我只找到了这个参考:Servlet Filter using request.getParameter loses Form data

谢谢!

【问题讨论】:

  • 也许会显示一个代码片段来说明你是如何做到的?
  • 你得到了getInputStream()还是getReader()?似乎它们会干扰 getParameter() 的执行

标签: java servlets parameters httprequest servlet-filters


【解决方案1】:

顺便说一句,解决此问题的另一种方法是不使用过滤器链,而是构建自己的拦截器组件,也许使用方面,它可以对解析的请求体进行操作。它也可能更有效,因为您只需将请求 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 接口。您需要提供更多方法的实现,例如 isReadysetReadListener 等。请参阅下面的评论中提供的 question

【讨论】:

  • 这是真的吗?底层调用是原始请求上的getInputStream(),您已经读取了其中的字节。底层请求不知道你的包装器,那么它怎么知道调用包装器的 getInputStream() 呢?
  • 准确地说,getInputStream 是在 my 包装器上调用的,因为这是我传递到过滤器链中的 ServletRequest 实例。如果您仍有疑问,请阅读ServletRequestWrapperServletRequest 接口的源代码。
  • 如果我能做到+100,我会的。我一直试图让它正常工作 3-4 小时。感谢您提供清晰的示例和解释!我很高兴找到这篇文章!
  • 有什么建议可以使用 Servlet-api 3.0+ 吗? ServletInputStream 现在有抽象isReady()isFinished()setReadListener() 处理必须实现的非阻塞 IO。我在想 ReadListener 可以留空,但不确定如何处理 isFinished() 和/或 isReady()
  • @EricB。不管怎么说,还是要谢谢你。后来我找到了较新的api接口的解决方案,只是在这里粘贴以防有人感兴趣。 stackoverflow.com/questions/29208456/…
【解决方案2】:

我知道我迟到了,但这个问题对我来说仍然很重要,而且这篇 SO 帖子是 Google 的热门文章之一。我将继续发布我的解决方案,希望其他人可以节省几个小时。

就我而言,我需要用他们的身体记录所有请求和响应。使用 Spring Framework 答案其实很简单,只需使用 ContentCachingRequestWrapperContentCachingResponseWrapper

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

        }

    }

}

【讨论】:

  • 这对我不起作用。 requestBodyresponseBody 都是空字符串
  • 我的错误。我在做 chain.doFilter(request, response); 而不是 chain.doFilter(requestWrapper, responseWrapper);
  • ContentCaching*Wrapper 类具有消耗输入流的昂贵价格,因此“缓存”是通过方法 getContentAsByteArray 完成的,但此类不缓存其他可能需要的输入流过滤器链中的过滤器(这是我的用例)。恕我直言,这是内容缓存类的意外行为,因此我在 spring 团队中提出了这一改进jira.spring.io/browse/SPR-16028
  • 您可以使用 Spring 中的AbstractRequestLoggingFilter,其中大部分工作已经由 Spring 完成,您只需覆盖 1 或 2 个简单方法。
  • spring-web-4.3.12.RELEASE 开始,这对我不起作用。在查看源代码时,我发现变量cachedContent 用于存储各种内容,例如请求参数和请求输入流。如果你单独打电话给getContentAsByteArray(),它是空的。要获取请求正文,您必须致电getInputStream()。但同样,这会使 inputStream 对其他过滤器和处理程序不可用。
【解决方案3】:

所以这基本上是 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 帖子和示例中得出的。但花了一段时间才将它们整合在一起,所以我希望它对未来的读者有所帮助。

请先投票赞成 Lathy 的回答。没有它,我不可能走到这一步。

以下是我在解决此问题时遇到的一个/一些例外情况。

已经为此请求调用了getReader()

看起来我“借来”的一些地方在这里:

http://slackspace.de/articles/log-request-body-with-spring-boot/

https://github.com/c0nscience/spring-web-logging/blob/master/src/main/java/org/zalando/springframework/web/logging/LoggingFilter.java

https://howtodoinjava.com/servlets/httpservletrequestwrapper-example-read-request-body/

https://www.oodlestechnologies.com/blogs/How-to-create-duplicate-object-of-httpServletRequest-object

https://github.com/c0nscience/spring-web-logging/blob/master/src/main/java/org/zalando/springframework/web/logging/LoggingFilter.java

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;
    }
}

【讨论】:

  • @KnockingHeads。方式方式方式太多的巫毒。 :(
【解决方案4】:

以上答案非常有帮助,但在我的经验中仍然存在一些问题。在 tomcat 7 servlet 3.0 上,getParamter 和 getParamterValues 也必须被覆盖。这里的解决方案包括get-query参数和post-body。它允许轻松获取原始字符串。

与其他解决方案一样,它使用 Apache commons-io 和 Googles Guava。

在此解决方案中,getParameter* 方法不会抛出 IOException,但它们使用 super.getInputStream()(获取主体)可能会抛出 IOException。我抓住它并抛出 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();
    }
}

【讨论】:

  • 这太棒了!几天来我一直试图解决这个问题,这适用于 servlet 3.1。一个问题:你为什么在getParameterMap()中做decode(getPostBodyAsString(), result);?这会创建一个 key = request body 和 value = null 的参数,这很奇怪。
  • 与其解析所有的字符串,为什么不在getParameterMap 中调用super.getParameterMap()?无论如何,这会给你一张&lt;String, String[]&gt; 的地图。
  • 所以我也遇到了 paramMap 的一些问题。请参阅我的回答和 2021 年 1 月的“追加”。那里有一条神奇的线(来自这个问题的另一个答案)..这可能是不必手动保留paramertMaps的“触发器”。代码的神奇部分 super.getParameterMap(); // 在 ContentCachingRequestWrapper 中初始化缓存。这是重要的呼叫,以便在 REST 控制器上填充“@RequestParam Map 参数”
【解决方案5】:

唯一的方法是您自己在过滤器中使用整个输入流,从中获取您想要的内容,然后为您读取的内容创建一个新的 InputStream,并将该 InputStream 放入 ServletRequestWrapper(或HttpServletRequestWrapper)。

缺点是您必须自己解析有效负载,标准并没有为您提供该功能。

附录--

正如我所说,您需要查看 HttpServletRequestWrapper。

在过滤器中,您可以继续调用 FilterChain.doFilter(request, response)。

对于普通过滤器,请求和响应与传入过滤器的请求和响应相同。不一定是这样。您可以将这些替换为您自己的请求和/或响应。

HttpServletRequestWrapper 是专门为实现这一点而设计的。您将原始请求传递给它,然后您可以拦截所有调用。您创建自己的子类,并用您自己的方法替换 getInputStream 方法。您无法更改原始请求的输入流,因此您可以使用此包装器并返回您自己的输入流。

最简单的情况是将原始请求输入流消耗到字节缓冲区中,对它做任何你想做的事情,然后从该缓冲区创建一个新的 ByteArrayInputStream。这是您的包装器中返回的内容,它被传递给 FilterChain.doFilter 方法。

您需要继承 ServletInputStream 并为您的 ByteArrayInputStream 创建另一个包装器,但这也没什么大不了的。

【讨论】:

  • 我无法读取 InputStream 并在之后恢复它,没有直接访问流的 get/set 方法。您的提议似乎不错,但我不知道如何实施。
【解决方案6】:

我也有同样的问题,我相信下面的代码更简单,它对我有用,

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);

如果您有任何疑问,请告诉我

【讨论】:

    【解决方案7】:

    我为任何格式的请求正文找到了很好的解决方案。 我测试了application/x-www-form-urlencodedapplication/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) {
            }
    
        }
    
    }
    

    【讨论】:

    • 您对“super.getParameterMap();”的调用对我来说是魔法。
    • 我更新了我的答案......但是你的答案(我赞成)有一条神奇的线......这有助于我的代码。并在您的代码旁边放置一个很好的注释。谢谢。 super.getParameterMap(); // 在 ContentCachingRequestWrapper 中初始化缓存
    • @granadaCoder 你应该点击 trought to super 方法,你会看到在做什么
    • @AndrewSneck 非常感谢!它适用于 application/x-www-form-urlencoded 和 application/json
    【解决方案8】:

    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("]"));

    【讨论】:

    • 我希望使用您的解决方案生成审核日志,但我需要记录请求是否成功,我可以挂钩 http 响应并获取此类中的代码。
    【解决方案9】:

    在我的情况下,仅覆盖 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;
    }
    

    【讨论】:

    • 您可能正在使用 Tomcat 7 或更高版本的 Servlet 3.0?你还有其他 3 种方法的代码吗?
    • 其他 3 个方法只需调用 getParameterMap() 并获取所需的值。
    • 所以我也遇到了 paramMap 的一些问题。请参阅我的回答和 2021 年 1 月的“追加”。那里有一条神奇的线(来自这个问题的另一个答案)..这可能是不必手动保留 paramertMaps 的“触发器”。代码的神奇部分 super.getParameterMap(); // 在 ContentCachingRequestWrapper 中初始化缓存。这是重要的呼叫,以便在 REST 控制器上填充“@RequestParam Map 参数”
    【解决方案10】:

    如果您可以控制请求,则可以将内容类型设置为 binary/octet-stream。这允许在不消耗输入流的情况下查询参数。

    但是,这可能特定于某些应用程序服务器。我只测试了tomcat,根据https://stackoverflow.com/a/11434646/957103,jetty的行为似乎是一样的。

    【讨论】:

      【解决方案11】:

      Spring类ContentCachingRequestWrapper的getContentAsByteArray()方法会多次读取body,但是同一个类的getInputStream()和getReader()方法不会多次读取body:

      "这个类通过消费InputStream来缓存请求体,如果我们在其中一个filter中读取了InputStream,那么filter链中的其他后续filter就不能再读取了。由于这个限制,这个类不适用于所有情况。”

      在我的情况下,解决这个问题的更通用的解决方案是将以下三个类添加到我的 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();
          }
      }
      

      ContentCachingFilter.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

      【讨论】:

        【解决方案12】:

        您可以使用 servlet 过滤器链,但使用原来的过滤器链,您可以创建自己的请求 yourownrequests extends HttpServletRequestWrapper。

        【讨论】:

        • 教程链接现在似乎包含病毒。
        【解决方案13】:

        首先我们不应该读取过滤器中的参数。通常在过滤器中读取标头以执行少量身份验证任务。话虽如此,使用 CharStreams 可以在 Filter 或 Interceptor 中完全读取 HttpRequest 正文:

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

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

        【讨论】:

        • 是的。如果您这样做一次,request.getReader() 将在后续读取中返回一个仅包含空字符串的读取器。
        • 我会在覆盖 getReader() 和 getInputStream() 方法以使用这个新主体作为源的情况下工作。
        猜你喜欢
        • 1970-01-01
        • 2022-01-02
        • 2014-11-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-12-04
        • 1970-01-01
        相关资源
        最近更新 更多