Yêu cầu Http Servlet mất các thông số từ nội dung POST sau khi đọc nó một lần


86

Tôi đang cố gắng truy cập hai tham số yêu cầu http trong bộ lọc Java Servlet, không có gì mới ở đây, nhưng rất ngạc nhiên khi thấy rằng các tham số đã được sử dụng! Bởi vì điều này, nó không có sẵn trong chuỗi bộ lọc nữa.

Có vẻ như điều này chỉ xảy ra khi các tham số xuất hiện trong phần thân yêu cầu POST (ví dụ: gửi biểu mẫu).

Có cách nào để đọc các thông số và KHÔNG sử dụng chúng không?

Cho đến nay tôi chỉ tìm thấy tham chiếu này: Bộ lọc Servlet sử dụng request.getParameter làm mất dữ liệu Biểu mẫu .

Cảm ơn!


2
có thể hiển thị một đoạn mã về cách bạn đang làm điều đó?
Pavel Veller

Bạn có nhận được getInputStream () hoặc getReader () không? Có vẻ như họ là những người mà sẽ gây trở ngại cho việc thực hiện getParameter ()
evanwong

Câu trả lời:


111

Ngoài ra, một cách thay thế để giải quyết vấn đề này là không sử dụng chuỗi bộ lọc và thay vào đó xây dựng thành phần đánh chặn của riêng bạn, có thể sử dụng các khía cạnh, có thể hoạt động trên phần thân yêu cầu được phân tích cú pháp. Nó cũng có thể sẽ hiệu quả hơn vì bạn chỉ chuyển đổi yêu cầu InputStreamthành đối tượng mô hình của riêng bạn một lần.

Tuy nhiên, tôi vẫn nghĩ là hợp lý khi muốn đọc phần thân yêu cầu nhiều lần, đặc biệt khi yêu cầu di chuyển qua chuỗi bộ lọc. Tôi thường sử dụng chuỗi bộ lọc cho các hoạt động nhất định mà tôi muốn giữ lại ở lớp HTTP, được tách ra khỏi các thành phần dịch vụ.

Theo gợi ý của Will Hartung, tôi đã đạt được điều này bằng cách mở rộng HttpServletRequestWrapper, sử dụng yêu cầu InputStreamvà về cơ bản là bộ nhớ đệm các byte.

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

Giờ đây, phần thân yêu cầu có thể được đọc nhiều lần bằng cách gói yêu cầu ban đầu trước khi chuyển nó qua chuỗi bộ lọc:

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

Giải pháp này cũng sẽ cho phép bạn đọc phần thân yêu cầu nhiều lần thông qua các getParameterXXXphương thức bởi vì lời gọi cơ bản là getInputStream(), tất nhiên sẽ đọc yêu cầu được lưu trong bộ nhớ cache InputStream.

Biên tập

Đối với phiên bản ServletInputStreamgiao diện mới hơn . Bạn cần cung cấp triển khai một số phương pháp khác như isReady, setReadListenerv.v. Tham khảo câu hỏi này như được cung cấp trong bình luận bên dưới.


5
Có đúng như vậy không? Lệnh gọi cơ bản là getInputStream () theo yêu cầu ban đầu , trong đó bạn đã đọc các byte. Yêu cầu cơ bản không có kiến ​​thức về trình bao bọc của bạn, vì vậy làm thế nào nó có thể biết để gọi getInputStream () của trình bao bọc?
Frans

1
Nói chính xác getInputStreamlà được gọi trên trình bao bọc của tôi vì đây là ServletRequesttrường hợp mà tôi truyền vào chuỗi bộ lọc. Nếu bạn vẫn còn nghi ngờ, hãy đọc mã nguồn ServletRequestWrapperServletRequestgiao diện.
pestrella

1
Nếu tôi có thể làm cho con số này +100, tôi sẽ làm. Tôi đã cố gắng để điều này hoạt động đúng trong 3-4 giờ. Cảm ơn vì ví dụ và giải thích rõ ràng của bạn! Tôi rất vui vì tôi đã tìm thấy bài viết này!
Doug

20
Bất kỳ đề xuất nào về cách làm cho điều này hoạt động với Servlet-api 3.0+? ServletInputStream bây giờ có trừu tượng isReady(). isFinished()setReadListener()để đối phó với IO không chặn phải được thực hiện. Tôi nghĩ rằng ReadListener có thể được để trống, nhưng không chắc phải làm gì về isFinished()và / hoặc isReady().
Eric B.

12
@EricB. Dẫu sao cũng xin cảm ơn. Sau đó, tôi đã tìm thấy giải pháp cho giao diện api mới hơn, chỉ cần dán vào đây trong trường hợp ai đó quan tâm. stackoverflow.com/questions/29208456/…
dcc

37

Tôi biết mình đến muộn, nhưng câu hỏi này vẫn còn phù hợp với tôi và bài đăng SO này là một trong những bài đăng hàng đầu trên Google. Tôi sẽ tiếp tục và đăng giải pháp của mình với hy vọng rằng ai đó có thể tiết kiệm được vài giờ.

Trong trường hợp của tôi, tôi cần ghi lại tất cả các yêu cầu và phản hồi với cơ thể của họ. Sử dụng Spring Framework, câu trả lời thực sự khá đơn giản, chỉ cần sử dụng 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

        }

    }

}

3
Điều này không hiệu quả với tôi. Cả hai requestBodyresponseBodylà chuỗi rỗng
Abhijith Madhav

5
Lỗi của tôi. Tôi đã làm một chain.doFilter(request, response);thay vì mộtchain.doFilter(requestWrapper, responseWrapper);
Abhijith Madhav

5
Các ContentCaching*Wrapperlớp có cái giá đắt khi tiêu thụ luồng đầu vào vì vậy "bộ nhớ đệm" được thực hiện thông qua phương thức getContentAsByteArraynhưng lớp này không lưu vào bộ nhớ đệm luồng đầu vào mà các bộ lọc khác trong chuỗi bộ lọc có thể cần đến (đó là trường hợp sử dụng của tôi). Imho, đây không phải là hành vi được mong đợi của một lớp bộ nhớ đệm nội dung, do đó, tôi đã nêu ra cải tiến này trong đội mùa xuân jira.spring.io/browse/SPR-16028
Federico Piazza

Bạn có thể sử dụng AbstractRequestLoggingFiltertừ Spring, nơi hầu hết công việc đã được Spring thực hiện và bạn chỉ cần ghi đè 1 hoặc 2 phương thức đơn giản.
khắc nghiệt

1
Điều này không hiệu quả đối với tôi spring-web-4.3.12.RELEASE. Khi tôi kiểm tra nguồn, tôi thấy biến cachedContentđược sử dụng để lưu trữ các nội dung khác nhau như tham số yêu cầu và luồng đầu vào yêu cầu. Nó trống nếu bạn getContentAsByteArray()chỉ gọi . Để có được cơ quan yêu cầu, bạn phải gọi getInputStream(). Nhưng một lần nữa, điều này sẽ làm cho inputStream không khả dụng với các bộ lọc khác và trình xử lý.
Ivan Huang,

7

Các câu trả lời trên rất hữu ích, nhưng vẫn có một số vấn đề trong kinh nghiệm của tôi. Trên tomcat 7 servlet 3.0, getParamter và getParamterValues ​​cũng phải được ghi đè. Giải pháp ở đây bao gồm cả tham số get-query và post-body. Nó cho phép lấy chuỗi thô một cách dễ dàng.

Giống như các giải pháp khác, nó sử dụng Apache commons-io và Googles Guava.

Trong giải pháp này, các phương thức getParameter * không ném IOException nhưng chúng sử dụng super.getInputStream () (để lấy phần thân) có thể ném IOException. Tôi bắt nó và ném runtimeException. Nó không phải là tốt đẹp như vậy.

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

Điều đó thật tuyệt! Tôi đã cố gắng tìm ra điều này trong nhiều ngày và điều này hoạt động với servlet 3.1. Một câu hỏi: tại sao bạn làm decode(getPostBodyAsString(), result);trong getParameterMap()? Điều đó tạo ra một tham số với key = request body và value = null, khá kỳ quặc.
orlade,

Thay vì xem qua tất cả các chuỗi phân tích cú pháp, tại sao bạn không gọi super.getParameterMap()trong của bạn getParameterMap? Điều này sẽ cung cấp cho bạn một bản đồ về mọi thứ <String, String[]>.
Ean V

6

Cách duy nhất là bạn có thể tự mình sử dụng toàn bộ luồng đầu vào trong bộ lọc, lấy những gì bạn muốn từ nó, sau đó tạo một InputStream mới cho nội dung bạn đọc và đưa InputStream đó vào ServletRequestWrapper (hoặc HttpServletRequestWrapper).

Nhược điểm là bạn sẽ phải tự phân tích cú pháp tải trọng, tiêu chuẩn không cung cấp khả năng đó cho bạn.

Addenda -

Như tôi đã nói, bạn cần nhìn vào HttpServletRequestWrapper.

Trong một bộ lọc, bạn tiếp tục bằng cách gọi FilterChain.doFilter (yêu cầu, phản hồi).

Đối với các bộ lọc tầm thường, yêu cầu và phản hồi giống như những yêu cầu được chuyển vào bộ lọc. Đó không phải là trường hợp. Bạn có thể thay thế những yêu cầu đó bằng yêu cầu và / hoặc phản hồi của riêng bạn.

HttpServletRequestWrapper được thiết kế đặc biệt để hỗ trợ điều này. Bạn chuyển nó theo yêu cầu ban đầu và sau đó bạn có thể chặn tất cả các cuộc gọi. Bạn tạo lớp con của riêng mình cho lớp này và thay thế phương thức getInputStream bằng một phương thức của riêng bạn. Bạn không thể thay đổi luồng đầu vào của yêu cầu ban đầu, vì vậy thay vào đó, bạn có trình bao bọc này và trả lại luồng đầu vào của riêng bạn.

Trường hợp đơn giản nhất là sử dụng luồng đầu vào yêu cầu ban đầu vào một bộ đệm byte, làm bất cứ điều gì bạn muốn trên nó, sau đó tạo một ByteArrayInputStream mới từ bộ đệm đó. Đây là những gì được trả về trong trình bao bọc của bạn, được chuyển đến phương thức FilterChain.doFilter.

Bạn sẽ cần phân lớp ServletInputStream và tạo một trình bao bọc khác cho ByteArrayInputStream của mình, nhưng đó cũng không phải là vấn đề lớn.


Tôi không thể quản lý để đọc InputStream và khôi phục nó sau đó, không có phương thức get / set nào để truy cập trực tiếp vào luồng. Đề xuất của bạn có vẻ tốt, nhưng tôi không thấy làm thế nào để thực hiện nó.
amuniz

4

Tôi cũng gặp vấn đề tương tự và tôi tin rằng đoạn mã dưới đây đơn giản hơn và nó phù hợp với tôi,

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

trong lớp java bộ lọc,

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

Vui lòng cho tôi biết nếu bạn có bất kỳ thắc mắc nào


3

Vì vậy, về cơ bản đây là câu trả lời của Lathy NHƯNG được cập nhật cho các yêu cầu mới hơn cho ServletInputStream.

Cụ thể là (đối với ServletInputStream), người ta phải triển khai:

public abstract boolean isFinished();

public abstract boolean isReady();

public abstract void setReadListener(ReadListener var1);

Đây là đối tượng của Lathy đã được chỉnh sửa

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

và ở đâu đó (??) Tôi đã tìm thấy điều này (là lớp hạng nhất xử lý các phương thức "bổ sung".

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

Cuối cùng, tôi chỉ cố gắng ghi lại các yêu cầu. Và các mảnh ghép lại với nhau ở trên đã giúp tôi tạo ra phần bên dưới.

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

Vui lòng mang mã này với một hạt muối.

"Bài kiểm tra" quan trọng NHẤT là liệu POST có hoạt động với tải trọng hay không. Đây là những gì sẽ phơi bày các vấn đề "đọc hai lần".

mã ví dụ giả

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

Bạn có thể thay thế "MyCustomObject" bằng "Object" đơn giản nếu bạn chỉ muốn thử nghiệm.

Câu trả lời này được đúc kết từ một số bài đăng và ví dụ SOF khác nhau..nhưng phải mất một lúc để tổng hợp tất cả lại vì vậy tôi hy vọng nó sẽ giúp ích cho người đọc trong tương lai.

Hãy ủng hộ câu trả lời của Lathy trước của tôi. Tôi đã không thể đi xa đến mức này nếu không có nó.

Dưới đây là một / một số trường hợp ngoại lệ tôi nhận được khi giải quyết vấn đề này.

getReader () đã được gọi cho yêu cầu này

Có vẻ như một số địa điểm tôi đã "mượn" ở đây:

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


3

Spring đã hỗ trợ tích hợp cho việc này với 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;
}

Rất tiếc, bạn vẫn sẽ không thể đọc tải trọng trực tiếp từ yêu cầu, nhưng tham số thông báo Chuỗi sẽ bao gồm tải trọng để bạn có thể lấy nó từ đó như sau:

String body = message.substring(message.indexOf("{"), message.lastIndexOf("]"));


Tôi đã hy vọng sử dụng giải pháp của bạn để tạo nhật ký kiểm tra nhưng tôi cần ghi lại xem yêu cầu có thành công hay không, tôi có thể kết nối vào phản hồi http và lấy mã trong lớp này không.
jonesy

1

Chỉ ghi đè getInputStream()không hoạt động trong trường hợp của tôi. Việc triển khai máy chủ của tôi dường như phân tích cú pháp các tham số mà không gọi phương thức này. Tôi không tìm thấy bất kỳ cách nào khác, nhưng cũng triển khai lại cả bốn phương thức getParameter *. Đây là mã của getParameterMap(Apache Http Client và thư viện Google Guava được sử dụng):

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

1
Jetty có vấn đề này thật không may, grepcode.com/file/repo1.maven.org/maven2/org.eclipse.jetty/...
mikeapr4

Có thể bạn đang sử dụng Tomcat 7 trở lên với Servlet 3.0? Bạn có mã cho 3 phương pháp khác không?
Bạc

3 phương thức khác chỉ cần gọi getParameterMap () và tìm nạp giá trị cần thiết.
30 giờ

0

Nếu bạn có quyền kiểm soát yêu cầu, bạn có thể đặt loại nội dung thành binary / octet-stream . Điều này cho phép truy vấn các tham số mà không tiêu tốn luồng đầu vào.

Tuy nhiên, điều này có thể dành riêng cho một số máy chủ ứng dụng. Tôi chỉ thử nghiệm tomcat, cầu cảng dường như hoạt động theo cùng một cách theo https://stackoverflow.com/a/11434646/957103 .


0

Phương thức getContentAsByteArray () của lớp Spring ContentCachingRequestWrapper đọc phần thân nhiều lần, nhưng các phương thức getInputStream () và getReader () của cùng một lớp không đọc phần thân nhiều lần:

"Lớp này lưu trữ nội dung yêu cầu bằng cách sử dụng InputStream. Nếu chúng tôi đọc InputStream trong một trong các bộ lọc, thì các bộ lọc tiếp theo khác trong chuỗi bộ lọc sẽ không thể đọc được nữa. Vì giới hạn này, lớp này hoàn toàn không phù hợp tình huống. "

Trong trường hợp của tôi, giải pháp chung hơn đã giải quyết vấn đề này là thêm ba lớp sau vào dự án khởi động mùa xuân của tôi (và các phụ thuộc bắt buộc vào tệp 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);
    }
}

Tôi cũng đã thêm các phụ thuộc sau vào 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>

Mã nguồn hoàn chỉnh và đầy đủ nằm ở đây: https://www.baeldung.com/spring-reading-httpservletrequest-multiple-times


-1

bạn có thể sử dụng chuỗi bộ lọc servlet, nhưng thay vào đó hãy sử dụng chuỗi gốc, bạn có thể tạo yêu cầu của riêng mình theo yêu cầu mở rộng HttpServletRequestWrapper.


1
Có vẻ như liên kết đến hướng dẫn có chứa một loại vi rút bây giờ.
fasth

-2

Trước hết, chúng ta không nên đọc các thông số trong bộ lọc. Thông thường các tiêu đề được đọc trong bộ lọc để thực hiện một số tác vụ xác thực. Đã nói rằng người ta có thể đọc hoàn toàn nội dung HttpRequest trong Bộ lọc hoặc Bộ đánh chặn bằng cách sử dụng CharStreams:

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

Điều này hoàn toàn không ảnh hưởng đến các lần đọc tiếp theo.


vâng, nó có. Nếu bạn làm điều này một lần, request.getReader()sẽ trả về một trình đọc chỉ chứa một chuỗi trống trong các lần đọc tiếp theo.
christophetd

1
Tôi sẽ làm việc trong trường hợp ghi đè các phương thức getReader () và getInputStream () để sử dụng phần thân mới này làm nguồn.
Rodrigo Borba
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.