Запрос HTTP-сервлета теряет параметры из тела POST после его однократного чтения

86

Я пытаюсь получить доступ к двум параметрам HTTP-запроса в фильтре сервлета Java, здесь ничего нового, но с удивлением обнаружил, что параметры уже были использованы! Из-за этого он больше не доступен в цепочке фильтров.

Кажется, что это происходит только тогда, когда параметры входят в тело запроса POST (например, отправка формы).

Есть ли способ прочитать параметры и НЕ использовать их?

Пока я нашел только эту ссылку: Фильтр сервлетов с использованием request.getParameter теряет данные формы .

Благодарность!

Амуниз
источник
2
может показать фрагмент кода, как вы это делаете?
Павел Веллер
Вы получили getInputStream () или getReader ()? Похоже, именно они будут мешать выполнению getParameter ()
evanwong

Ответы:

112

В качестве альтернативы, альтернативный способ решения этой проблемы - не использовать цепочку фильтров и вместо этого создать собственный компонент-перехватчик, возможно, используя аспекты, которые могут работать с проанализированным телом запроса. Это также, вероятно, будет более эффективным, поскольку вы только конвертируете запросInputStream в свой собственный объект модели.

Однако я по-прежнему считаю разумным читать тело запроса более одного раза, особенно когда запрос проходит через цепочку фильтров. Я обычно использую цепочки фильтров для определенных операций, которые я хочу сохранить на уровне HTTP, независимо от компонентов службы.

Как было предложено Уиллом Хартунгом, я добился этого путем расширения HttpServletRequestWrapper, использования запроса InputStreamи, по сути, кэширования байтов.

public class MultiReadHttpServletRequest extends HttpServletRequestWrapper {
  private ByteArrayOutputStream cachedBytes;

  public MultiReadHttpServletRequest(HttpServletRequest request) {
    super(request);
  }

  @Override
  public ServletInputStream getInputStream() throws IOException {
    if (cachedBytes == null)
      cacheInputStream();

      return new CachedServletInputStream();
  }

  @Override
  public BufferedReader getReader() throws IOException{
    return new BufferedReader(new InputStreamReader(getInputStream()));
  }

  private void cacheInputStream() throws IOException {
    /* Cache the inputstream in order to read it multiple times. For
     * convenience, I use apache.commons IOUtils
     */
    cachedBytes = new ByteArrayOutputStream();
    IOUtils.copy(super.getInputStream(), cachedBytes);
  }

  /* An inputstream which reads the cached request body */
  public class CachedServletInputStream extends ServletInputStream {
    private ByteArrayInputStream input;

    public CachedServletInputStream() {
      /* create a new input stream from the cached request body */
      input = new ByteArrayInputStream(cachedBytes.toByteArray());
    }

    @Override
    public int read() throws IOException {
      return input.read();
    }
  }
}

Теперь тело запроса можно прочитать более одного раза, обернув исходный запрос перед его передачей через цепочку фильтров:

public class MyFilter implements Filter {
  @Override
  public void doFilter(ServletRequest request, ServletResponse response,
        FilterChain chain) throws IOException, ServletException {

    /* wrap the request in order to read the inputstream multiple times */
    MultiReadHttpServletRequest multiReadRequest = new MultiReadHttpServletRequest((HttpServletRequest) request);

    /* here I read the inputstream and do my thing with it; when I pass the
     * wrapped request through the filter chain, the rest of the filters, and
     * request handlers may read the cached inputstream
     */
    doMyThing(multiReadRequest.getInputStream());
    //OR
    anotherUsage(multiReadRequest.getReader());
    chain.doFilter(multiReadRequest, response);
  }
}

Это решение также позволит вам несколько раз прочитать тело запроса с помощью getParameterXXXметодов, потому что это базовый вызов getInputStream(), который, конечно, будет читать кешированный запрос InputStream.

редактировать

Для более новой версии ServletInputStreamинтерфейса. Вам необходимо предоставить реализацию еще нескольких методов, например isReady, setReadListenerи т.д. Обратитесь к этому вопросу, как указано в комментарии ниже.

пестрелла
источник
5
Это правда? Базовым вызовом является getInputStream () по исходному запросу , байты которого вы уже прочитали. Базовый запрос ничего не знает о вашей оболочке, так как он узнает, что нужно вызвать getInputStream () оболочки?
Франс,
1
Чтобы быть точным, getInputStreamвызывается в моей оболочке, поскольку это ServletRequestэкземпляр, который я передаю в цепочку фильтров. Если вы все еще сомневаетесь, прочтите исходный код ServletRequestWrapperи ServletRequestинтерфейс.
pestrella 03
1
Если бы я мог сделать это +100, я бы это сделал. Я пытался заставить это работать правильно в течение 3-4 часов. Спасибо за ясный пример и объяснение! Я рад, что нашел этот пост!
Дуг
20
Есть предложения, как заставить эту работу работать с Servlet-api 3.0+? ServletInputStream теперь имеет абстрактный isReady(). isFinished()и setReadListener()иметь дело с неблокирующим вводом-выводом, который должен быть реализован. Я думаю, что ReadListener можно оставить пустым, но я не знаю, что делать isFinished()и / или isReady().
Эрик Б.
12
@EricB. Спасибо, в любом случае. Позже я нашел решение для нового интерфейса api, просто вставил сюда, если кому-то интересно. stackoverflow.com/questions/29208456/…
dcc
37

Я знаю, что опаздываю, но этот вопрос все еще был актуален для меня, и этот пост SO был одним из самых популярных в Google. Я собираюсь опубликовать свое решение в надежде, что кто-то еще может сэкономить пару часов.

В моем случае мне нужно было регистрировать все запросы и ответы с их телами. Используя Spring Framework, ответ на самом деле довольно прост, просто используйте ContentCachingRequestWrapper и ContentCachingResponseWrapper .

import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class LoggingFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void destroy() {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest) request);
        ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response);

        try {
            chain.doFilter(requestWrapper, responseWrapper);
        } finally {

            String requestBody = new String(requestWrapper.getContentAsByteArray());
            String responseBody = new String(responseWrapper.getContentAsByteArray());
            // Do not forget this line after reading response content or actual response will be empty!
            responseWrapper.copyBodyToResponse();

            // Write request and response body, headers, timestamps etc. to log files

        }

    }

}
Микк
источник
3
У меня это не сработало. И те, requestBodyи другие responseBodyбыли пустыми строками
Абхиджит Мадхав
5
Виноват. Я делал chain.doFilter(request, response);вместоchain.doFilter(requestWrapper, responseWrapper);
Абайджит Мадхава
5
В ContentCaching*Wrapperклассах имеют дорогую цену потребляя входной поток , так что «кэширование» осуществляются с помощью метода , getContentAsByteArrayно этот класс не кэшировать входной поток , который может быть необходим другими фильтрами в цепочке фильтров (это мой случай использования). Imho, это неожиданное
Federico Piazza
Вы можете использовать AbstractRequestLoggingFilterиз Spring, где большая часть работы уже сделана Spring, и вам нужно только переопределить 1 или 2 простых метода.
суровый
1
У меня это не работает spring-web-4.3.12.RELEASE. Когда я проверил источник, я обнаружил, что переменная cachedContentиспользуется для хранения различного содержимого, такого как параметры запроса и inputStream запроса. Пусто, если звонить в getContentAsByteArray()одиночку. Для получения тела запроса необходимо позвонить getInputStream(). Но опять же, это сделает inputStream недоступным для других фильтров и обработчика.
Иван Хуанг
7

Приведенные выше ответы были очень полезны, но в моем опыте все еще были проблемы. В сервлете Tomcat 7 3.0 также пришлось перезаписать getParamter и getParamterValues. Решение здесь включает в себя как параметры запроса получения, так и тело сообщения. Это позволяет легко получить необработанную строку.

Как и другие решения, он использует 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();
    }
}
Arberg
источник
Это круто! Я пытался понять это несколько дней, и это работает с сервлетом 3.1. Один вопрос: почему вы делаете decode(getPostBodyAsString(), result);в getParameterMap()? Это создает параметр с ключом = тело запроса и значение = ноль, что довольно странно.
orlade
Вместо того, чтобы выполнять весь синтаксический анализ строк, почему вы не вызываете super.getParameterMap()свой getParameterMap? Что в <String, String[]>любом случае даст вам карту .
Ean V
6

Единственный способ - использовать весь входной поток самостоятельно в фильтре, взять из него то, что вы хотите, а затем создать новый InputStream для прочитанного вами содержимого и поместить этот InputStream в ServletRequestWrapper (или HttpServletRequestWrapper).

Обратной стороной является то, что вам придется анализировать полезную нагрузку самостоятельно, стандарт не делает эту возможность доступной для вас.

Дополнения -

Как я уже сказал, вам нужно посмотреть HttpServletRequestWrapper.

В фильтре вы продолжаете, вызывая FilterChain.doFilter (запрос, ответ).

Для тривиальных фильтров запрос и ответ такие же, как те, которые передаются фильтру. Это не должно быть так. Вы можете заменить их своими собственными запросами и / или ответами.

HttpServletRequestWrapper специально разработан для этого. Вы передаете ему исходный запрос, а затем можете перехватывать все вызовы. Вы создаете свой собственный подкласс этого и заменяете метод getInputStream одним из ваших собственных. Вы не можете изменить входной поток исходного запроса, поэтому вместо этого у вас есть эта оболочка и вы возвращаете свой собственный входной поток.

Самый простой случай - использовать исходный входной поток запросов в байтовый буфер, делать с ним все, что угодно, а затем создавать новый ByteArrayInputStream из этого буфера. Это то, что возвращается в вашей оболочке, которая передается методу FilterChain.doFilter.

Вам нужно будет создать подкласс ServletInputStream и создать другую оболочку для своего ByteArrayInputStream, но это тоже не имеет большого значения.

Уилл Хартунг
источник
Мне не удается прочитать InputStream и восстановить его после, нет методов get / set для прямого доступа к потоку. Ваше предложение кажется хорошим, но я не вижу, как его реализовать.
amuniz
4

У меня тоже была такая же проблема, и я считаю, что приведенный ниже код более простой и работает для меня,

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

Пожалуйста, дайте мне знать, если у вас возникнут вопросы

Лати
источник
3

Итак, это в основном ответ Лати, НО обновленный для новых требований для ServletInputStream.

А именно (для ServletInputStream) необходимо реализовать:

public abstract boolean isFinished();

public abstract boolean isReady();

public abstract void setReadListener(ReadListener var1);

Это отредактированный объект Лати

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

В конечном итоге я просто пытался регистрировать запросы. И приведенные выше фрагменты, соединенные вместе франкенштейном, помогли мне создать нижеследующее.

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 на обычный старый Object, если хотите просто протестировать.

Этот ответ основан на нескольких различных сообщениях и примерах SOF ... но потребовалось время, чтобы собрать все воедино, поэтому я надеюсь, что это поможет будущему читателю.

Пожалуйста, проголосуйте за ответ Лати перед моим. Без этого я бы не продвинулся так далеко.

Ниже приведено одно / некоторые из исключений, которые я получил во время работы над этим.

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

GranadaCoder
источник
3

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 будет включать полезную нагрузку, поэтому вы можете получить ее оттуда следующим образом:

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

Маркоорн
источник
Я надеялся использовать ваше решение для создания журнала аудита, но мне нужно записать, был ли запрос успешным, могу ли я подключиться к HTTP-ответу и получить код в этом классе.
jonesy
1

Просто перезапись 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;
}
30-е
источник
1
К сожалению, у Jetty есть эта проблема, grepcode.com/file/repo1.maven.org/maven2/org.eclipse.jetty/…
mikeapr4 01
Вы, вероятно, используете Tomcat 7 или более позднюю версию с Servlet 3.0? У вас есть код для остальных трех методов?
Silver
Остальные 3 метода просто вызывают getParameterMap () и получают необходимое значение.
30
0

Если у вас есть контроль над запросом, вы можете установить тип контента на двоичный / октетный поток. . Это позволяет запрашивать параметры без использования входного потока.

Однако это может быть специфическим для некоторых серверов приложений. Я тестировал только tomcat, причал, похоже, ведет себя так же, в соответствии с qaru.site/questions/435 / ... / 11434646/957103 .

Оливье, Роджер
источник
0

Метод getContentAsByteArray () класса Spring ContentCachingRequestWrapper читает тело несколько раз, но методы getInputStream () и getReader () того же класса не читают тело несколько раз:

"Этот класс кэширует тело запроса, используя InputStream. Если мы читаем InputStream в одном из фильтров, то другие последующие фильтры в цепочке фильтров больше не смогут его читать. Из-за этого ограничения этот класс не подходит для всех ситуации ".

В моем случае более общее решение, которое решило эту проблему, заключалось в добавлении следующих трех классов в мой загрузочный проект Spring (и необходимых зависимостей для файла 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

Вели-Матти Сорвала
источник
-1

вы можете использовать цепочку фильтров сервлетов, но вместо этого использовать исходную, вы можете создать свой собственный запрос. yourownrequests extends HttpServletRequestWrapper.

Чжэнвэй Чжань
источник
1
Похоже, ссылка на учебник теперь содержит вирус.
fasth
-2

Прежде всего, мы не должны читать параметры в фильтре. Обычно заголовки читаются в фильтре для выполнения нескольких задач аутентификации. Сказав, что можно полностью прочитать тело HttpRequest в фильтре или перехватчике, используя CharStreams:

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

Это никак не влияет на последующие чтения.

ашока
источник
Да. Если вы сделаете это один раз, request.getReader()вернет читатель, который будет содержать только пустую строку при последующих чтениях.
christophetd
1
Я бы работал в случае перезаписи методов getReader () и getInputStream (), чтобы использовать это новое тело в качестве источника.
Родриго Борба