A solicitação do servlet Http perde os parâmetros do corpo do POST após lê-la uma vez

86

Estou tentando acessar dois parâmetros de solicitação http em um filtro Java Servlet, nada de novo aqui, mas fiquei surpreso ao descobrir que os parâmetros já foram consumidos! Por causa disso, ele não está mais disponível na cadeia de filtros.

Parece que isso só ocorre quando os parâmetros vêm em um corpo de solicitação POST (um envio de formulário, por exemplo).

Existe uma maneira de ler os parâmetros e NÃO consumi-los?

Até agora, encontrei apenas esta referência: Filtro de servlet usando request.getParameter perde dados do formulário .

Obrigado!

amuniz
fonte
2
talvez mostre um fragmento de código de como você está fazendo isso?
Pavel Veller
Você obteve getInputStream () ou getReader ()? Parece que são eles que irão interferir na execução de getParameter ()
evanwong

Respostas:

111

Como um aparte, uma maneira alternativa de resolver este problema é não usar a cadeia de filtros e, em vez disso, construir seu próprio componente interceptor, talvez usando aspectos, que podem operar no corpo da solicitação analisado. Provavelmente, também será mais eficiente, pois você só está convertendo a solicitação InputStreamem seu próprio objeto de modelo uma vez.

No entanto, ainda acho que é razoável querer ler o corpo da solicitação mais de uma vez, especialmente à medida que a solicitação passa pela cadeia de filtros. Normalmente, eu usaria cadeias de filtros para certas operações que desejo manter na camada HTTP, desacopladas dos componentes de serviço.

Conforme sugerido por Will Hartung , consegui isso estendendo HttpServletRequestWrapper, consumindo a solicitação InputStreame essencialmente armazenando os bytes em cache.

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

Agora, o corpo da solicitação pode ser lido mais de uma vez envolvendo a solicitação original antes de passá-la pela cadeia de filtros:

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

Essa solução também permitirá que você leia o corpo da solicitação várias vezes por meio dos getParameterXXXmétodos, pois a chamada subjacente é getInputStream(), que, é claro, lerá a solicitação em cache InputStream.

Editar

Para uma versão mais recente da ServletInputStreaminterface. Você precisa fornecer a implementação de mais alguns métodos isReady, como , setReadListeneretc. Consulte esta questão conforme fornecido no comentário abaixo.

Pestrella
fonte
5
Isso é verdade? A chamada subjacente é getInputStream () na solicitação original , da qual você já leu os bytes. A solicitação subjacente não tem conhecimento de seu invólucro, então como ele saberia chamar getInputStream () do invólucro?
Frans,
1
Para ser mais preciso, getInputStreamé chamado no meu invólucro, pois é a ServletRequestinstância que passo para a cadeia de filtros. Se você ainda estiver em dúvida, leia o código-fonte do ServletRequestWrappere a ServletRequestinterface.
pestrella
1
Se eu pudesse fazer isso +100, eu faria. Estou tentando fazer isso funcionar direito por 3-4 horas. Obrigado pelo seu exemplo claro e explicação! Estou feliz por ter encontrado este post!
Doug
20
Alguma sugestão de como fazer isso funcionar com o Servlet-api 3.0+? O ServletInputStream agora possui resumo isReady(). isFinished()e setReadListener()para lidar com IO sem bloqueio que deve ser implementado. Estou pensando que o ReadListener pode ser deixado em branco, mas não tenho certeza do que fazer isFinished()e / ou isReady().
Eric B.
12
@EricB. obrigado mesmo assim. Mais tarde, encontrei a solução para a interface api mais recente, apenas colei aqui caso alguém esteja interessado. stackoverflow.com/questions/29208456/…
dcc
37

Eu sei que estou atrasado, mas esta questão ainda era relevante para mim e este post do SO foi um dos maiores sucessos no Google. Vou postar minha solução na esperança de que outra pessoa economize algumas horas.

No meu caso, precisei registrar todas as solicitações e respostas com seus corpos. Usando Spring Framework, a resposta é bastante simples, basta usar ContentCachingRequestWrapper e 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

        }

    }

}
Mikk
fonte
3
Isso não funcionou para mim. Ambos requestBodye responseBodyeram strings vazias
Abhijith Madhav
5
Meu erro. Eu estava fazendo um em chain.doFilter(request, response);vez de umchain.doFilter(requestWrapper, responseWrapper);
Abhijith Madhav
5
As ContentCaching*Wrapperclasses têm o preço caro de consumir o fluxo de entrada, então o "armazenamento em cache" é feito por meio do método, getContentAsByteArraymas essa classe não está armazenando em cache o fluxo de entrada que pode ser necessário para outros filtros na cadeia de filtros (que é meu caso de uso). Imho, este é um comportamento não esperado de uma classe de cache de conteúdo, portanto levantei essa melhoria no time de primavera jira.spring.io/browse/SPR-16028
Federico Piazza
Você pode usar a AbstractRequestLoggingFilterpartir do Spring, onde a maior parte do trabalho já é feito pelo Spring e você só precisa substituir 1 ou 2 métodos simples.
duro
1
Isso não funciona para mim a partir de spring-web-4.3.12.RELEASE. Ao verificar a fonte, descobri que a variável cachedContenté usada para armazenar vários conteúdos, como parâmetros de solicitação e inputStream de solicitação. Está vazio se você ligar getContentAsByteArray()apenas. Para obter o corpo da solicitação, você deve ligar getInputStream(). Mas, novamente, isso tornará o inputStream indisponível para outros filtros e o manipulador.
Ivan Huang,
7

As respostas acima foram muito úteis, mas ainda apresentavam alguns problemas em minha experiência. No servlet 3.0 do tomcat 7, getParamter e getParamterValues ​​também tiveram que ser substituídos. A solução aqui inclui os parâmetros get-query e o post-body. Ele permite obter cordas brutas facilmente.

Como as outras soluções, ele usa Apache commons-io e Googles Guava.

Nesta solução, os métodos getParameter * não lançam IOException, mas usam super.getInputStream () (para obter o corpo), que pode lançar IOException. Eu pego e lanço runtimeException. Não é tão bom.

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
fonte
Isso é ótimo! Estou tentando descobrir isso há dias, e isso funciona com o servlet 3.1. Uma pergunta: por que você faz decode(getPostBodyAsString(), result);em getParameterMap()? Isso cria um parâmetro com key = request body e value = null, o que é muito estranho.
orlade
Em vez de passar por toda a análise de string, por que você não chama o super.getParameterMap()seu getParameterMap? O que lhe dará um mapa de <String, String[]>qualquer maneira.
Ean V
6

A única maneira seria você consumir todo o fluxo de entrada sozinho no filtro, pegar o que quiser dele e, em seguida, criar um novo InputStream para o conteúdo lido e colocar esse InputStream em um ServletRequestWrapper (ou HttpServletRequestWrapper).

A desvantagem é que você mesmo terá que analisar a carga útil, o padrão não disponibiliza esse recurso para você.

Addenda -

Como eu disse, você precisa olhar para HttpServletRequestWrapper.

Em um filtro, você continua chamando FilterChain.doFilter (solicitação, resposta).

Para filtros triviais, a solicitação e a resposta são iguais às passadas para o filtro. Isso não precisa ser o caso. Você pode substituí-los por suas próprias solicitações e / ou respostas.

HttpServletRequestWrapper é projetado especificamente para facilitar isso. Você passa a solicitação original e pode interceptar todas as chamadas. Você cria sua própria subclasse disso e substitui o método getInputStream por um seu. Você não pode alterar o fluxo de entrada da solicitação original, então, em vez disso, você tem este wrapper e retorna seu próprio fluxo de entrada.

O caso mais simples é consumir o fluxo de entrada das solicitações originais em um buffer de bytes, fazer o que quiser e criar um novo ByteArrayInputStream a partir desse buffer. Isso é o que é retornado em seu wrapper, que é passado para o método FilterChain.doFilter.

Você precisará criar uma subclasse de ServletInputStream e fazer outro wrapper para seu ByteArrayInputStream, mas isso também não é grande coisa.

Will Hartung
fonte
Não consigo ler o InputStream e restaurá-lo depois, não há métodos get / set para acesso direto ao stream. Sua proposta parece boa, mas não vejo como implementá-la.
amuniz
4

Eu também tive o mesmo problema e acredito que o código abaixo é mais simples e está funcionando para mim,

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

na classe de filtro java,

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

Por favor, deixe-me saber se você tiver alguma dúvida

Lathy
fonte
3

Portanto, esta é basicamente a resposta de Lathy, MAS atualizada para requisitos mais recentes para ServletInputStream.

A saber (para ServletInputStream), é necessário implementar:

public abstract boolean isFinished();

public abstract boolean isReady();

public abstract void setReadListener(ReadListener var1);

Este é o objeto editado de Lathy

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

public class RequestWrapper extends HttpServletRequestWrapper {

    private String _body;

    public RequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        _body = "";
        BufferedReader bufferedReader = request.getReader();
        String line;
        while ((line = bufferedReader.readLine()) != null){
            _body += line;
        }
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {

        CustomServletInputStream kid = new CustomServletInputStream(_body.getBytes());
        return kid;
    }

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

e em algum lugar (??) eu encontrei isso (que é uma classe de primeira classe que lida com os métodos "extras".

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

No final das contas, eu estava apenas tentando registrar as solicitações. E as peças combinadas de frankensteined acima me ajudaram a criar o abaixo.

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

Por favor, aceite este código com cautela.

O "teste" MAIS importante é se um POST funciona com uma carga útil. Isso é o que irá expor os problemas de "leitura dupla".

código de pseudo exemplo

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

Você pode substituir "MyCustomObject" por "Object" simples se quiser apenas testar.

Esta resposta vem de vários posts e exemplos diferentes de SOF ... mas demorou um pouco para reunir tudo, então espero que ajude um futuro leitor.

Por favor, vote a favor da resposta de Lathy antes da minha. Eu não poderia ter chegado tão longe sem ele.

Abaixo está uma / algumas das exceções que recebi enquanto trabalhava nisso.

getReader () já foi chamado para esta solicitação

Parece que alguns dos lugares de onde "peguei emprestado" estão aqui:

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

O Spring tem suporte integrado para isso com 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;
}

Infelizmente, você ainda não conseguirá ler a carga útil diretamente da solicitação, mas o parâmetro String message incluirá a carga útil para que você possa pegá-la de lá da seguinte maneira:

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

Markoorn
fonte
Eu esperava usar sua solução para gerar um log de auditoria, mas preciso registrar se a solicitação foi bem-sucedida, posso ligar para a resposta http e obter o código dentro desta classe.
jonesy
1

Apenas substituir de getInputStream()não funcionou no meu caso. Minha implementação de servidor parece analisar parâmetros sem chamar esse método. Eu não encontrei outra maneira, mas reimplemente os quatro métodos getParameter * também. Aqui está o código de getParameterMap(Apache Http Client e biblioteca Google Guava usados):

@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;
}
30h
fonte
1
Jetty tem esse problema, infelizmente, grepcode.com/file/repo1.maven.org/maven2/org.eclipse.jetty/…
mikeapr4
Você provavelmente está usando o Tomcat 7 ou superior com Servlet 3.0? Você tem o código para os outros 3 métodos também?
Prata de
Outros 3 métodos apenas chamam getParameterMap () e buscam o valor necessário.
30 de
0

Se você tiver controle sobre a solicitação, poderá definir o tipo de conteúdo como binário / fluxo de octeto . Isso permite consultar os parâmetros sem consumir o fluxo de entrada.

No entanto, isso pode ser específico para alguns servidores de aplicativos. Eu só testei o tomcat, o jetty parece se comportar da mesma maneira de acordo com https://stackoverflow.com/a/11434646/957103 .

Olivier.Roger
fonte
0

O método getContentAsByteArray () da classe Spring ContentCachingRequestWrapper lê o corpo várias vezes, mas os métodos getInputStream () e getReader () da mesma classe não lêem o corpo várias vezes:

"Esta classe armazena em cache o corpo da solicitação consumindo o InputStream. Se lermos o InputStream em um dos filtros, outros filtros subsequentes na cadeia de filtros não poderão mais lê-lo. Devido a essa limitação, esta classe não é adequada em todos situações. "

No meu caso, a solução mais geral que resolveu esse problema foi adicionar as três classes a seguir ao meu projeto de inicialização Spring (e as dependências necessárias ao arquivo 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);
    }
}

Também adicionei as seguintes dependências ao 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>

Um código-fonte tutural e completo está localizado aqui: https://www.baeldung.com/spring-reading-httpservletrequest-multiple-times

Veli-Matti Sorvala
fonte
-1

você pode usar a cadeia de filtro de servlet, mas, em vez de usar o original, pode criar sua própria solicitação yourownrequests estende HttpServletRequestWrapper.

Zhengwei Zhan
fonte
1
Parece que o link para o tutorial contém um vírus agora.
dia
-2

Em primeiro lugar, não devemos ler os parâmetros do filtro. Normalmente, os cabeçalhos são lidos no filtro para fazer algumas tarefas de autenticação. Dito isto, pode-se ler o corpo HttpRequest completamente no Filtro ou Interceptor usando CharStreams:

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

Isso não afeta as leituras subsequentes.

Ashoka
fonte
sim. Se você fizer isso uma vez, request.getReader()retornará um leitor que contém apenas uma string vazia nas leituras subsequentes.
christophetd
1
Eu trabalharia no caso de substituir os métodos getReader () e getInputStream () para usar esse novo corpo como fonte.
Rodrigo Borba