Żądanie serwletu HTTP powoduje utratę parametrów z treści POST po jednokrotnym przeczytaniu


86

Próbuję uzyskać dostęp do dwóch parametrów żądania http w filtrze Java Servlet, nic nowego, ale byłem zaskoczony, gdy okazało się, że parametry zostały już zużyte! Z tego powodu nie jest już dostępny w łańcuchu filtrów.

Wydaje się, że dzieje się tak tylko wtedy, gdy parametry znajdują się w treści żądania POST (na przykład przesłanie formularza).

Czy jest sposób, aby odczytać parametry i NIE ZUŻYĆ ich?

Jak dotąd znalazłem tylko to odniesienie: Filtr serwletów używający request.getParameter traci dane formularza .

Dzięki!


2
może pokazać fragment kodu, jak to robisz?
Pavel Veller

Czy otrzymałeś metodę getInputStream () lub getReader ()? Wygląda na to, że są tymi, które będą kolidować z realizacją getParameter ()
evanwong

Odpowiedzi:


111

Nawiasem mówiąc, alternatywnym sposobem rozwiązania tego problemu jest nieużywanie łańcucha filtrów i zamiast tego zbudowanie własnego komponentu przechwytującego, być może przy użyciu aspektów, które mogą działać na przeanalizowanej treści żądania. Prawdopodobnie będzie to również bardziej wydajne, ponieważ konwertujesz żądanie InputStreamna własny obiekt modelu tylko raz.

Jednak nadal uważam, że rozsądne jest czytanie treści żądania więcej niż raz, zwłaszcza gdy żądanie przechodzi przez łańcuch filtrów. Zwykle używałbym łańcuchów filtrów do pewnych operacji, które chcę zachować w warstwie HTTP, oddzielone od składników usługi.

Jak zasugerował Will Hartung , osiągnąłem to, rozszerzając HttpServletRequestWrapper, konsumując żądanie InputStreami zasadniczo buforując bajty.

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

Teraz treść żądania można odczytać więcej niż raz, opakowując oryginalne żądanie przed przekazaniem go przez łańcuch filtrów:

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

To rozwiązanie umożliwi również wielokrotne odczytywanie treści żądania za pomocą tych getParameterXXXmetod, ponieważ wywołanie bazowe to getInputStream(), które oczywiście odczyta buforowane żądanie InputStream.

Edytować

Dla nowszej wersji ServletInputStreaminterfejsu. Trzeba zapewnić realizację od kilku bardziej podobne metody isReady, setReadListeneritd. Patrz to pytanie , jak przewidziano w komentarzu poniżej.


5
Czy to prawda? Podstawowym wywołaniem jest getInputStream () w pierwotnym żądaniu , którego bajty zostały już odczytane. Żądanie bazowe nie ma informacji na temat opakowania, więc skąd ma wiedzieć, aby wywołać metodę getInputStream () opakowania?
Frans

1
Aby być precyzyjnym, getInputStreamjest wywoływany na moim opakowaniu, ponieważ jest to ServletRequestinstancja, którą przekazuję do łańcucha filtrów. Jeśli nadal masz wątpliwości, przeczytaj kod źródłowy ServletRequestWrapperi ServletRequestinterfejs.
pestrella

1
Gdybym mógł zrobić to +100, zrobiłbym to. Próbowałem, żeby to działało dobrze przez 3-4 godziny. Dziękuję za jasny przykład i wyjaśnienie! Cieszę się, że znalazłem ten post!
Doug

20
Jakieś sugestie, jak to działa z Servlet-api 3.0+? ServletInputStream ma teraz streszczenie isReady(). isFinished()i setReadListener()radzić sobie z nieblokującym IO, które musi zostać zaimplementowane. Myślę, że ReadListener może pozostać puste, ale nie wiem, co z tym zrobić isFinished()i / lub isReady().
Eric B.

12
@EricB. w każdym razie dzięki. Później znalazłem rozwiązanie dla nowszego interfejsu API, po prostu wkleiłem tutaj na wypadek, gdyby ktoś był zainteresowany. stackoverflow.com/questions/29208456/…
dcc

37

Wiem, że się spóźniłem, ale to pytanie wciąż było dla mnie istotne, a ten post SO był jednym z największych hitów w Google. Idę dalej i opublikuję moje rozwiązanie w nadziei, że ktoś inny może zaoszczędzić kilka godzin.

W moim przypadku musiałem rejestrować wszystkie żądania i odpowiedzi w ich ciałach. Używając Spring Framework, odpowiedź jest całkiem prosta, po prostu użyj ContentCachingRequestWrapper i 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

        }

    }

}

3
To nie zadziałało dla mnie. Obie requestBodyi responseBodybyły puste struny
Abhijith Madhav

5
Mój błąd. Robiłem chain.doFilter(request, response);zamiastchain.doFilter(requestWrapper, responseWrapper);
Abhijith Madhav

5
Te ContentCaching*Wrapperzajęcia mają drogie cenę spożywania strumień wejściowy więc „buforowanie” odbywa się za pomocą metody getContentAsByteArray, ale ta klasa nie jest buforowanie strumienia wejściowego, które mogą być wymagane przez inne filtry w łańcuchu filtrów (co jest moim przypadek użycia). Imho, to nieoczekiwane zachowanie klasy cache'owania treści, dlatego podniosłem tę poprawę w wiosennym zespole jira.spring.io/browse/SPR-16028
Federico Piazza

Możesz użyć AbstractRequestLoggingFilterSpringa, gdzie większość pracy jest już wykonana przez Spring i wystarczy zastąpić 1 lub 2 proste metody.
ostry

1
To nie działa dla mnie od dnia spring-web-4.3.12.RELEASE. Gdy sprawdziłem źródło, znalazłem, zmienna cachedContentsłuży do przechowywania różnych treści, takich jak parametry żądania i żądanie inputStream. Jest pusty, jeśli dzwonisz getContentAsByteArray()wyłącznie. Aby uzyskać treść żądania, należy zadzwonić getInputStream(). Ale znowu, spowoduje to, że inputStream będzie niedostępny dla innych filtrów i modułu obsługi.
Ivan Huang,

7

Powyższe odpowiedzi były bardzo pomocne, ale z mojego doświadczenia nadal wynikały pewne problemy. W serwlecie tomcat 7 3.0, getParamter i getParamterValues ​​również musiały zostać nadpisane. Rozwiązanie tutaj obejmuje zarówno parametry zapytania get, jak i post-body. Pozwala na łatwe uzyskanie surowego sznurka.

Podobnie jak inne rozwiązania wykorzystuje Apache commons-io i Googles Guava.

W tym rozwiązaniu metody getParameter * nie zgłaszają wyjątku IOException, ale używają super.getInputStream () (w celu pobrania treści), który może zgłosić wyjątek IOException. Łapię to i wrzucam runtimeException. To nie jest takie miłe.

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

To jest świetne! Próbowałem to rozgryźć od kilku dni i to działa z serwletem 3.1. Jedno pytanie: dlaczego ty decode(getPostBodyAsString(), result);w getParameterMap()? To tworzy parametr z kluczem = treść żądania i wartością = null, co jest dość dziwne.
orlade

Zamiast przechodzić przez całą analizę ciągów, dlaczego nie wywołujesz super.getParameterMap()swojego getParameterMap? Co i tak da ci mapę <String, String[]>.
Ean V

6

Jedynym sposobem byłoby samodzielne skonsumowanie całego strumienia wejściowego w filtrze, pobranie z niego tego, co chcesz, a następnie utworzenie nowego InputStream dla czytanej treści i umieszczenie tego InputStream w ServletRequestWrapper (lub HttpServletRequestWrapper).

Wadą jest to, że będziesz musiał sam przeanalizować ładunek, standard nie udostępnia ci tej możliwości.

Uzupełnienia -

Jak powiedziałem, musisz spojrzeć na HttpServletRequestWrapper.

W filtrze kontynuujesz, wywołując FilterChain.doFilter (żądanie, odpowiedź).

W przypadku trywialnych filtrów żądanie i odpowiedź są takie same, jak te przekazane do filtra. Nie musi tak być. Możesz je zastąpić własnymi prośbami i / lub odpowiedziami.

HttpServletRequestWrapper jest specjalnie zaprojektowany, aby to ułatwić. Przekazujesz pierwotne żądanie, a następnie możesz przechwycić wszystkie połączenia. Tworzysz własną podklasę this i zastępujesz metodę getInputStream inną własną. Nie możesz zmienić strumienia wejściowego oryginalnego żądania, więc zamiast tego masz to opakowanie i zwracasz własny strumień wejściowy.

Najprostszym przypadkiem jest użycie pierwotnego strumienia wejściowego żądań do bufora bajtowego, zrobienie na nim dowolnej magii, a następnie utworzenie nowego ByteArrayInputStream z tego bufora. To jest to, co jest zwracane w opakowaniu, które jest przekazywane do metody FilterChain.doFilter.

Będziesz musiał utworzyć podklasę ServletInputStream i utworzyć inny wrapper dla swojego ByteArrayInputStream, ale to też nie jest wielka sprawa.


Nie mogę odczytać InputStream i przywrócić go później, nie ma metod get / set do bezpośredniego dostępu do strumienia. Twoja propozycja wydaje się dobra, ale nie wiem, jak ją wdrożyć.
amuniz

4

Ja też miałem ten sam problem i uważam, że poniższy kod jest prostszy i działa dla mnie,

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

w filtrze klasy java,

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

Daj mi znać, jeśli masz jakieś pytania


3

To jest w zasadzie odpowiedź Lathy'ego, ALE zaktualizowana pod kątem nowszych wymagań dla ServletInputStream.

Mianowicie (dla ServletInputStream) należy zaimplementować:

public abstract boolean isFinished();

public abstract boolean isReady();

public abstract void setReadListener(ReadListener var1);

To jest edytowany obiekt Lathy'ego

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

i gdzieś (??) znalazłem to (które jest klasą pierwszej klasy, która zajmuje się "dodatkowymi" metodami.

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

Ostatecznie próbowałem tylko rejestrować żądania. A powyższe, połączone razem elementy pomogły mi stworzyć poniższe.

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

Proszę wziąć ten kod z przymrużeniem oka.

NAJWAŻNIEJSZY „test” dotyczy tego, czy POST działa z ładunkiem. To właśnie ujawni problemy związane z „podwójnym czytaniem”.

pseudo przykładowy kod

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

Możesz zamienić „MyCustomObject” na zwykły ole „Object”, jeśli chcesz tylko przetestować.

Ta odpowiedź pochodzi z kilku różnych postów i przykładów SOF, ale zebranie jej w całość zajęło trochę czasu, więc mam nadzieję, że pomoże ona przyszłemu czytelnikowi.

Proszę, zagłosuj na odpowiedź Lathy'ego przed moją. Bez niego nie zaszedłbym tak daleko.

Poniżej znajduje się jeden / kilka wyjątków, które otrzymałem podczas pracy nad tym.

Funkcja getReader () została już wywołana dla tego żądania

Wygląda na to, że są tutaj niektóre z miejsc, które „pożyczyłem”:

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 ma do tego wbudowaną obsługę za pomocą 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;
}

Niestety nadal nie będziesz w stanie odczytać ładunku bezpośrednio z żądania, ale parametr komunikatu ciągu będzie zawierał ładunek, więc możesz go pobrać stamtąd w następujący sposób:

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


Miałem nadzieję, że użyję twojego rozwiązania do wygenerowania dziennika audytu, ale muszę zarejestrować, czy żądanie się powiodło, czy mogę podłączyć się do odpowiedzi http i uzyskać kod z tej klasy.
jonesy

1

Samo nadpisywanie getInputStream()nie działało w moim przypadku. Moja implementacja serwera wydaje się analizować parametry bez wywoływania tej metody. Nie znalazłem innego sposobu, ale ponownie zaimplementowałem wszystkie cztery metody getParameter *. Oto kod getParameterMap(używany klient Apache Http i biblioteka 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;
}


Prawdopodobnie używasz Tomcat 7 lub nowszego z Servlet 3.0? Czy masz również kod dla pozostałych 3 metod?
Srebro

Pozostałe 3 metody po prostu wywołują getParameterMap () i pobierają potrzebną wartość.
30

0

Jeśli masz kontrolę nad żądaniem, możesz ustawić typ zawartości na binary / octet-stream . Pozwala to na zapytania o parametry bez zużywania strumienia wejściowego.

Jednak może to być specyficzne dla niektórych serwerów aplikacji. Testowałem tylko kocura, molo wydaje się zachowywać tak samo zgodnie z https://stackoverflow.com/a/11434646/957103 .


0

Metoda getContentAsByteArray () klasy Spring ContentCachingRequestWrapper odczytuje treść wielokrotnie, ale metody getInputStream () i getReader () tej samej klasy nie odczytują treści wielokrotnie:

„Ta klasa buforuje treść żądania, konsumując InputStream. Jeśli odczytujemy InputStream w jednym z filtrów, inne kolejne filtry w łańcuchu filtrów nie mogą go już odczytać. Z powodu tego ograniczenia ta klasa nie jest odpowiednia we wszystkich sytuacje ”.

W moim przypadku bardziej ogólnym rozwiązaniem, które rozwiązało ten problem, było dodanie następujących trzech klas do mojego projektu rozruchowego Spring (i wymaganych zależności do pliku 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);
    }
}

Dodałem również do pom następujące zależności:

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

Kursowy i pełny kod źródłowy znajduje się tutaj: https://www.baeldung.com/spring-reading-httpservletrequest-multiple-times


-1

możesz użyć łańcucha filtrów serwletów, ale zamiast tego użyć oryginalnego, możesz utworzyć własne żądanie, które Twoje własne żądania rozszerza HttpServletRequestWrapper.


1
Wygląda na to, że link do samouczka zawiera teraz wirusa.
Fasth

-2

Przede wszystkim nie powinniśmy czytać parametrów w filtrze. Zwykle nagłówki są odczytywane w filtrze, aby wykonać kilka zadań uwierzytelniania. Powiedziawszy, że można w całości odczytać treść HttpRequest w filtrze lub przechwytywaczu, używając CharStreams:

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

W ogóle nie wpływa to na kolejne odczyty.


tak. Jeśli zrobisz to raz, request.getReader()zwróci czytnik, który zawiera tylko pusty ciąg przy kolejnych odczytach.
christophetd

1
Pracowałbym w przypadku nadpisania metod getReader () i getInputStream (), aby użyć tego nowego ciała jako źródła.
Rodrigo Borba
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.