Il s'agit donc essentiellement de la réponse de Lathy MAIS mise à jour pour les nouvelles exigences de ServletInputStream.
A savoir (pour ServletInputStream), il faut implémenter :
public abstract boolean isFinished();
public abstract boolean isReady();
public abstract void setReadListener(ReadListener var1);
C'est l'objet de la Lathy éditée
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()));
}
}
et quelque part ( ??) j'ai trouvé ceci (qui est une classe de première classe qui traite les méthodes "supplémentaires".
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 fin de compte, j'essayais juste d'enregistrer les demandes. Et les morceaux rassemblés ci-dessus m'ont aidé à créer ce qui suit.
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;
}
}
Veuillez prendre ce code avec un grain de sel.
Le "test" le plus important est de savoir si un POST fonctionne avec une charge utile. C'est ce qui exposera les problèmes de "double lecture".
pseudo-code d'exemple
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;
}
}
Vous pouvez remplacer "MyCustomObject" par un simple "Object" si vous voulez simplement faire un test.
Cette réponse a été élaborée à partir de plusieurs articles et exemples sur les FOS, mais il m'a fallu du temps pour tout rassembler et j'espère que cela aidera un futur lecteur.
Veuillez upvoter la réponse de Lathy avant la mienne. Je n'aurais pas pu aller aussi loin sans elle.
Vous trouverez ci-dessous une ou plusieurs des exceptions que j'ai rencontrées en travaillant sur ce sujet.
getReader() a déjà été appelé pour cette requête
On dirait que certains des endroits que j'ai "empruntés" sont ici :
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
Janvier 2021 APPEND.
J'ai appris à mes dépens que le code ci-dessus ne fonctionne PAS pour les produits suivants
x-www-form-urlencoded
Prenons l'exemple ci-dessous :
@CrossOrigin
@ResponseBody
@PostMapping(path = "/mypath", consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE})
public ResponseEntity myMethodName(@RequestParam Map<String, String> parameters
) {
/* DO YOU GET ANY PARAMETERS HERE? Or are they empty because of logging/auditing filter ?*/
return new ResponseEntity(HttpStatus.OK);
}
J'ai dû passer en revue plusieurs des autres exemples ici.
J'ai créé un "wrapper" qui fonctionne explicitement pour APPLICATION_FORM_URLENCODED_VALUE.
import org.apache.commons.io.IOUtils;
import org.springframework.http.MediaType;
import org.springframework.web.util.ContentCachingRequestWrapper;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* Makes a "copy" of the HttpRequest so the body can be accessed more than 1 time.
* WORKS WITH APPLICATION_FORM_URLENCODED_VALUE
* See : https://stackoverflow.com/questions/44182370/why-do-we-wrap-httpservletrequest-the-api-provides-an-httpservletrequestwrappe/44187955#44187955
*/
public final class AppFormUrlEncodedSpecificContentCachingRequestWrapper extends ContentCachingRequestWrapper {
public static final String ERROR_MSG_CONTENT_TYPE_NOT_SUPPORTED = "ContentType not supported. (Input ContentType(s)=\"%1$s\", Supported ContentType(s)=\"%2$s\")";
public static final String ERROR_MSG_PERSISTED_CONTENT_CACHING_REQUEST_WRAPPER_CONSTRUCTOR_FAILED = "AppFormUrlEncodedSpecificContentCachingRequestWrapper constructor failed";
private static final org.slf4j.Logger LOGGER = org.slf4j.LoggerFactory.getLogger(AppFormUrlEncodedSpecificContentCachingRequestWrapper.class);
private byte[] body;
private ServletInputStream inputStream;
public AppFormUrlEncodedSpecificContentCachingRequestWrapper(HttpServletRequest request) {
super(request);
super.getParameterMap(); // init cache in ContentCachingRequestWrapper. THIS IS THE VITAL CALL so that "@RequestParam Map<String, String> parameters" are populated on the REST Controller. See https://stackoverflow.com/questions/10210645/http-servlet-request-lose-params-from-post-body-after-read-it-once/64924380#64924380
String contentType = request.getContentType();
/* EXPLICTLY check for APPLICATION_FORM_URLENCODED_VALUE and allow nothing else */
if (null == contentType || !contentType.equalsIgnoreCase(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) {
IllegalArgumentException ioex = new IllegalArgumentException(String.format(ERROR_MSG_CONTENT_TYPE_NOT_SUPPORTED, contentType, MediaType.APPLICATION_FORM_URLENCODED_VALUE));
LOGGER.error(ERROR_MSG_PERSISTED_CONTENT_CACHING_REQUEST_WRAPPER_CONSTRUCTOR_FAILED, ioex);
throw ioex;
}
try {
loadBody(request);
} catch (IOException ioex) {
throw new RuntimeException(ioex);
}
}
private void loadBody(HttpServletRequest request) throws IOException {
body = IOUtils.toByteArray(request.getInputStream());
inputStream = new CustomServletInputStream(this.getBody());
}
private byte[] getBody() {
return body;
}
@Override
public ServletInputStream getInputStream() throws IOException {
if (inputStream != null) {
return inputStream;
}
return super.getInputStream();
}
}
Notez la réponse d'Andrew Sneck sur cette même page. C'est à peu près cela : https://programmersought.com/article/23981013626/
Je n'ai pas eu le temps d'harmoniser les deux implémentations ci-dessus (mes deux implémentations).
J'ai donc créé une usine pour "choisir" entre les deux :
import org.springframework.http.MediaType;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.IOException;
/**
* Factory to return different concretes of HttpServletRequestWrapper. APPLICATION_FORM_URLENCODED_VALUE needs a different concrete.
*/
public class HttpServletRequestWrapperFactory {
public static final String ERROR_MSG_HTTP_SERVLET_REQUEST_WRAPPER_FACTORY_CREATE_HTTP_SERVLET_REQUEST_WRAPPER_FAILED = "HttpServletRequestWrapperFactory createHttpServletRequestWrapper FAILED";
public static HttpServletRequestWrapper createHttpServletRequestWrapper(final HttpServletRequest request) {
HttpServletRequestWrapper returnItem = null;
if (null != request) {
String contentType = request.getContentType();
if (null != contentType && contentType.equalsIgnoreCase(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) {
returnItem = new AppFormUrlEncodedSpecificContentCachingRequestWrapper(request);
} else {
try {
returnItem = new PersistedBodyRequestWrapper(request);
} catch (IOException ioex) {
throw new RuntimeException(ERROR_MSG_HTTP_SERVLET_REQUEST_WRAPPER_FACTORY_CREATE_HTTP_SERVLET_REQUEST_WRAPPER_FAILED, ioex);
}
}
}
return returnItem;
}
}
Ci-dessous se trouve l'"autre" qui fonctionne avec JSON, etc. C'est l'autre concret que la Factory peut sortir. Je l'ai mis ici pour que mon Jan 2021 APPEND soit cohérent Je ne sais pas si le code ci-dessous est parfaitement cohérent avec ma réponse originale :
import org.springframework.http.MediaType;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Map;
/**
* Makes a "copy" of the HttpRequest so the body can be accessed more than 1 time.
* See : https://stackoverflow.com/questions/44182370/why-do-we-wrap-httpservletrequest-the-api-provides-an-httpservletrequestwrappe/44187955#44187955
* DOES NOT WORK WITH APPLICATION_FORM_URLENCODED_VALUE
*/
public final class PersistedBodyRequestWrapper extends HttpServletRequestWrapper {
public static final String ERROR_MSG_CONTENT_TYPE_NOT_SUPPORTED = "ContentType not supported. (ContentType=\"%1$s\")";
public static final String ERROR_MSG_PERSISTED_BODY_REQUEST_WRAPPER_CONSTRUCTOR_FAILED = "PersistedBodyRequestWrapper constructor FAILED";
private static final org.slf4j.Logger LOGGER = org.slf4j.LoggerFactory.getLogger(PersistedBodyRequestWrapper.class);
private String persistedBody;
private final Map<String, String[]> parameterMap;
public PersistedBodyRequestWrapper(final HttpServletRequest request) throws IOException {
super(request);
String contentType = request.getContentType();
/* Allow everything EXCEPT APPLICATION_FORM_URLENCODED_VALUE */
if (null != contentType && contentType.equalsIgnoreCase(MediaType.APPLICATION_FORM_URLENCODED_VALUE)) {
IllegalArgumentException ioex = new IllegalArgumentException(String.format(ERROR_MSG_CONTENT_TYPE_NOT_SUPPORTED, MediaType.APPLICATION_FORM_URLENCODED_VALUE));
LOGGER.error(ERROR_MSG_PERSISTED_BODY_REQUEST_WRAPPER_CONSTRUCTOR_FAILED, ioex);
throw ioex;
}
parameterMap = request.getParameterMap();
this.persistedBody = "";
BufferedReader bufferedReader = request.getReader();
String line;
while ((line = bufferedReader.readLine()) != null) {
this.persistedBody += line;
}
}
@Override
public ServletInputStream getInputStream() throws IOException {
CustomServletInputStream csis = new CustomServletInputStream(this.persistedBody.getBytes(StandardCharsets.UTF_8));
return csis;
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
@Override
public Map<String, String[]> getParameterMap() {
return this.parameterMap;
}
}