La solicitud Http Servlet pierde parámetros del cuerpo POST después de leerlo una vez


86

Estoy tratando de acceder a dos parámetros de solicitud http en un filtro de servlet de Java, nada nuevo aquí, ¡pero me sorprendió descubrir que los parámetros ya se han consumido! Debido a esto, ya no está disponible en la cadena de filtros.

Parece que esto solo ocurre cuando los parámetros vienen en un cuerpo de solicitud POST (un formulario de envío, por ejemplo).

¿Hay alguna forma de leer los parámetros y NO consumirlos?

Hasta ahora, solo he encontrado esta referencia: el filtro de servlet usando request.getParameter pierde datos de formulario .

¡Gracias!


2
tal vez mostrar un fragmento de código de cómo lo estás haciendo?
Pavel Veller

¿Obtuvo getInputStream () o getReader ()? Parece que son los que interferirán con la ejecución de getParameter ()
evanwong

Respuestas:


111

Además, una forma alternativa de resolver este problema es no usar la cadena de filtros y, en su lugar, construir su propio componente interceptor, quizás usando aspectos, que pueden operar en el cuerpo de la solicitud analizada. También es probable que sea más eficiente ya que solo está convirtiendo la solicitud InputStreamen su propio objeto modelo una vez.

Sin embargo, sigo pensando que es razonable querer leer el cuerpo de la solicitud más de una vez, especialmente cuando la solicitud se mueve a través de la cadena de filtros. Por lo general, usaría cadenas de filtros para ciertas operaciones que quiero mantener en la capa HTTP, desacopladas de los componentes del servicio.

Como sugirió Will Hartung, logré esto extendiendo HttpServletRequestWrapper, consumiendo la solicitud InputStreamy esencialmente almacenando en caché los bytes.

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

Ahora, el cuerpo de la solicitud se puede leer más de una vez envolviendo la solicitud original antes de pasarla por la cadena 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);
  }
}

Esta solución también le permitirá leer el cuerpo de la solicitud varias veces a través de los getParameterXXXmétodos porque la llamada subyacente es getInputStream(), que por supuesto leerá la solicitud en caché InputStream.

Editar

Para una versión más reciente de la ServletInputStreaminterfaz. Debe proporcionar la implementación de algunos métodos más como isReady, setReadListeneretc. Consulte esta pregunta como se indica en el comentario a continuación.


5
¿Es eso cierto? La llamada subyacente es getInputStream () en la solicitud original , de la cual ya habrá leído los bytes. La solicitud subyacente no tiene conocimiento de su contenedor, entonces, ¿cómo sabría llamar al getInputStream () del contenedor?
Frans

1
Para ser precisos, getInputStreamse llama a mi contenedor, ya que esta es la ServletRequestinstancia que paso a la cadena de filtros. Si aún tiene dudas, lea el código fuente ServletRequestWrappery la ServletRequestinterfaz.
pestrella

1
Si pudiera hacer esto +100, lo haría. He intentado que esto funcione correctamente durante 3-4 horas. ¡Gracias por su claro ejemplo y explicación! ¡Me alegro de haber encontrado esta publicación!
Doug

20
¿Alguna sugerencia de cómo hacer que esto funcione con Servlet-api 3.0+? El ServletInputStream ahora tiene resumen isReady(). isFinished()y setReadListener()para hacer frente a las E / S sin bloqueo que deben implementarse. Creo que ReadListener podría dejarse en blanco, pero no estoy seguro de qué hacer al respecto isFinished()y / o isReady().
Eric B.

12
@EricB. gracias de cualquier manera. Más tarde encontré la solución para la interfaz api más nueva, solo pegada aquí en caso de que alguien esté interesado. stackoverflow.com/questions/29208456/…
dcc

37

Sé que llego tarde, pero esta pregunta seguía siendo relevante para mí y esta publicación SO fue uno de los principales éxitos en Google. Voy a publicar mi solución con la esperanza de que alguien más pueda ahorrar un par de horas.

En mi caso, necesitaba registrar todas las solicitudes y respuestas con sus cuerpos. Usando Spring Framework, la respuesta es bastante simple, solo use ContentCachingRequestWrapper y 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
Esto no funcionó para mí. Ambos requestBodyy responseBodyeran cadenas vacías
Abhijith Madhav

5
Mi error. Estaba haciendo un en chain.doFilter(request, response);lugar de unchain.doFilter(requestWrapper, responseWrapper);
Abhijith Madhav

5
Las ContentCaching*Wrapperclases tienen el costoso precio de consumir el flujo de entrada, por lo que el "almacenamiento en caché" se realiza a través del método, getContentAsByteArraypero esta clase no almacena en caché el flujo de entrada que podrían necesitar otros filtros en la cadena de filtros (que es mi caso de uso). En mi humilde opinión, este es un comportamiento no esperado de una clase de almacenamiento en caché de contenido, por lo tanto, planteé esta mejora en el equipo de primavera jira.spring.io/browse/SPR-16028
Federico Piazza

Puede usar AbstractRequestLoggingFilterdesde Spring, donde Spring ya realiza la mayor parte del trabajo y solo necesita anular 1 o 2 métodos simples.
duro

1
Esto no funciona para mí a partir de spring-web-4.3.12.RELEASE. Cuando verifiqué la fuente, encontré que la variable cachedContentse usa para almacenar varios contenidos, como los parámetros de solicitud y la solicitud inputStream. Está vacío si llama getContentAsByteArray()únicamente. Para obtener el cuerpo de la solicitud hay que llamar getInputStream(). Pero nuevamente, esto hará que inputStream no esté disponible para otros filtros y el controlador.
Ivan Huang

7

Las respuestas anteriores fueron muy útiles, pero aún tuve algunos problemas en mi experiencia. En el servlet 3.0 de Tomcat 7, getParamter y getParamterValues ​​también tuvieron que sobrescribirse. La solución aquí incluye tanto parámetros get-query como post-body. Permite obtener cuerdas sin procesar fácilmente.

Como las otras soluciones, usa Apache commons-io y Googles Guava.

En esta solución, los métodos getParameter * no lanzan IOException pero usan super.getInputStream () (para obtener el cuerpo) que puede lanzar IOException. Lo atrapo y lanzo runtimeException. No es tan agradable.

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

¡Esto es genial! He estado tratando de resolver esto durante días y funciona con el servlet 3.1. Una pregunta: ¿por qué haces decode(getPostBodyAsString(), result);en getParameterMap()? Eso crea un parámetro con key = request body y value = null, lo cual es bastante extraño.
orlade

En lugar de pasar por todo el análisis de cadenas, ¿por qué no llama super.getParameterMap()a su getParameterMap? Lo que te dará un mapa de <String, String[]>todos modos.
Ean V

6

La única forma sería que usted mismo consumiera todo el flujo de entrada en el filtro, tome lo que quiera de él y luego cree un nuevo InputStream para el contenido que lea y coloque ese InputStream en un ServletRequestWrapper (o HttpServletRequestWrapper).

La desventaja es que tendrá que analizar la carga útil usted mismo, el estándar no pone esa capacidad a su disposición.

Adenda --

Como dije, debe mirar HttpServletRequestWrapper.

En un filtro, continúa llamando a FilterChain.doFilter (solicitud, respuesta).

Para los filtros triviales, la solicitud y la respuesta son las mismas que las que se pasaron al filtro. Ese no tiene por qué ser el caso. Puede reemplazarlos con sus propias solicitudes y / o respuestas.

HttpServletRequestWrapper está diseñado específicamente para facilitar esto. Le pasa la solicitud original y luego puede interceptar todas las llamadas. Usted crea su propia subclase de esto y reemplaza el método getInputStream con uno propio. No puede cambiar el flujo de entrada de la solicitud original, por lo que tiene este contenedor y devuelve su propio flujo de entrada.

El caso más simple es consumir el flujo de entrada de las solicitudes originales en un búfer de bytes, hacer la magia que desee en él y luego crear un nuevo ByteArrayInputStream a partir de ese búfer. Esto es lo que se devuelve en su contenedor, que se pasa al método FilterChain.doFilter.

Deberá crear una subclase de ServletInputStream y hacer otro contenedor para su ByteArrayInputStream, pero eso tampoco es gran cosa.


No puedo leer InputStream y restaurarlo después, no hay métodos get / set para acceder directamente a la transmisión. Su propuesta parece buena, pero no veo cómo implementarla.
amuniz

4

Yo también tuve el mismo problema y creo que el código a continuación es más simple y funciona para mí.

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

en la clase filter java,

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

Por favor avíseme si tiene alguna duda


3

Entonces, esta es básicamente la respuesta de Lathy PERO actualizada para los requisitos más nuevos para ServletInputStream.

Es decir (para ServletInputStream), uno tiene que implementar:

public abstract boolean isFinished();

public abstract boolean isReady();

public abstract void setReadListener(ReadListener var1);

Este es el objeto de Lathy editado

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

y en alguna parte (??) encontré esto (que es una clase de primera clase que se ocupa de los métodos "extra".

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

En última instancia, solo estaba tratando de registrar las solicitudes. Y las piezas anteriores unidas con frankenstein me ayudaron a crear lo siguiente.

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, tome este código con cautela.

La "prueba" MÁS importante es si un POST funciona con una carga útil. Esto es lo que expondrá los problemas de "lectura doble".

pseudo código de ejemplo

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

Puede reemplazar "MyCustomObject" con "Object" simple y simple si solo desea probar.

Esta respuesta está extraída de varias publicaciones y ejemplos diferentes de SOF ... pero tomó un tiempo reunirlo todo, así que espero que ayude a un futuro lector.

Por favor, vote la respuesta de Lathy antes que la mía. No podría haber llegado tan lejos sin él.

A continuación se muestra una / algunas de las excepciones que obtuve mientras resolvía esto.

getReader () ya ha sido llamado para esta solicitud

Parece que algunos de los lugares que "tomé prestados" están aquí:

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 tiene soporte incorporado para esto con un 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;
}

Desafortunadamente, aún no podrá leer la carga útil directamente de la solicitud, pero el parámetro de mensaje de cadena incluirá la carga útil para que pueda tomarla desde allí de la siguiente manera:

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


Esperaba usar su solución para generar un registro de auditoría, pero necesito registrar si la solicitud fue exitosa, ¿puedo conectarme a la respuesta http y obtener el código dentro de esta clase?
jonesy

1

Simplemente sobrescribir getInputStream()no funcionó en mi caso. La implementación de mi servidor parece analizar los parámetros sin llamar a este método. No encontré ninguna otra forma, pero también volví a implementar los cuatro métodos getParameter *. Aquí está el código de getParameterMap(Apache Http Client y la biblioteca Google Guava utilizada):

@Override
public Map<String, String[]> getParameterMap() {
    Iterable<NameValuePair> params = URLEncodedUtils.parse(getQueryString(), NullUtils.UTF8);

    try {
        String cts = getContentType();
        if (cts != null) {
            ContentType ct = ContentType.parse(cts);
            if (ct.getMimeType().equals(ContentType.APPLICATION_FORM_URLENCODED.getMimeType())) {
                List<NameValuePair> postParams = URLEncodedUtils.parse(IOUtils.toString(getReader()), NullUtils.UTF8);
                params = Iterables.concat(params, postParams);
            }
        }
    } catch (IOException e) {
        throw new IllegalStateException(e);
    }
    Map<String, String[]> result = toMap(params);
    return result;
}

public static Map<String, String[]> toMap(Iterable<NameValuePair> body) {
    Map<String, String[]> result = new LinkedHashMap<>();
    for (NameValuePair e : body) {
        String key = e.getName();
        String value = e.getValue();
        if (result.containsKey(key)) {
            String[] newValue = ObjectArrays.concat(result.get(key), value);
            result.remove(key);
            result.put(key, newValue);
        } else {
            result.put(key, new String[] {value});
        }
    }
    return result;
}

1
Jetty tiene este problema desafortunadamente, grepcode.com/file/repo1.maven.org/maven2/org.eclipse.jetty/…
mikeapr4

Probablemente esté utilizando Tomcat 7 o con Servlet 3.0. ¿Tiene también el código para los otros 3 métodos?
Silver

Otros 3 métodos simplemente llaman a getParameterMap () y obtienen el valor necesario.
30 de

0

Si tiene control sobre la solicitud, puede establecer el tipo de contenido en binary / octet-stream . Esto permite consultar los parámetros sin consumir el flujo de entrada.

Sin embargo, esto puede ser específico de algunos servidores de aplicaciones. Solo probé tomcat, jetty parece comportarse de la misma manera de acuerdo con https://stackoverflow.com/a/11434646/957103 .


0

El método getContentAsByteArray () de la clase Spring ContentCachingRequestWrapper lee el cuerpo varias veces, pero los métodos getInputStream () y getReader () de la misma clase no leen el cuerpo varias veces:

"Esta clase almacena en caché el cuerpo de la solicitud consumiendo InputStream. Si leemos InputStream en uno de los filtros, otros filtros posteriores en la cadena de filtros ya no pueden leerlo. Debido a esta limitación, esta clase no es adecuada en todos situaciones ".

En mi caso, la solución más general que resolvió este problema fue agregar las siguientes tres clases a mi proyecto de arranque de Spring (y las dependencias requeridas al archivo 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);
    }
}

También agregué las siguientes dependencias a 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>

Un tutorial y el código fuente completo se encuentra aquí: https://www.baeldung.com/spring-reading-httpservletrequest-multiple-times


-1

puede usar la cadena de filtros de servlets, pero en su lugar use la original, puede crear su propia solicitud yourownrequests extiende HttpServletRequestWrapper.


1
Parece que el enlace al tutorial contiene un virus ahora.
ayuno

-2

En primer lugar, no debemos leer parámetros dentro del filtro. Por lo general, los encabezados se leen en el filtro para realizar algunas tareas de autenticación. Habiendo dicho eso, uno puede leer el cuerpo de HttpRequest completamente en el Filtro o Interceptor usando CharStreams:

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

Esto no afecta en absoluto las lecturas posteriores.


sí lo hace. Si hace esto una vez, request.getReader()devolverá un lector que solo contiene una cadena vacía en las lecturas posteriores.
christophetd

1
Trabajaría en caso de sobrescribir los métodos getReader () y getInputStream () para usar este nuevo cuerpo como fuente.
Rodrigo Borba
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.