1、spring框架记录日志导致
在 logging.level.root 为debug或trace 级别下, org.springframework.web.servlet.DispatcherServlet#logRequest中会调用 request.getParameterMap()
此时会消耗 inputstream ,导致在controller中获取不到 inputstream
It will be empty if it's already consumed beforehand. This will be implicitly done whenever you call getParameter()
, getParameterValues()
, getParameterMap()
, getReader()
, etc on the HttpServletRequest
. Make sure that you don't call any of those kind of methods which by themselves need to gather information from the request body before calling getInputStream()
. If your servlet isn't doing that, then start checking the servlet filters which are mapped on the same URL pattern
org.springframework.core.log.LogFormatUtils#traceDebug public static void traceDebug(Log logger, Function<Boolean, String> messageFactory) { if (logger.isDebugEnabled()) { boolean traceEnabled = logger.isTraceEnabled(); //日志级别是否到trace级别 String logMessage = messageFactory.apply(traceEnabled); if (traceEnabled) { logger.trace(logMessage); } else { logger.debug(logMessage); } } } org.springframework.web.servlet.DispatcherServlet#logRequest private void logRequest(HttpServletRequest request) { //debug、trace级别生效,导致inputstream为空 LogFormatUtils.traceDebug(logger, traceOn -> { String params; if (isEnableLoggingRequestDetails()) { params = request.getParameterMap().entrySet().stream() .map(entry -> entry.getKey() + ":" + Arrays.toString(entry.getValue())) .collect(Collectors.joining(", ")); } else { params = (request.getParameterMap().isEmpty() ? "" : "masked"); }
如果 content-type为 application/x-www-form-urlencoded 且 logging.level.root 为info级别 则可以获取到
如果 content-type为 application/multipart/form-data; 且 logging.level.root 为info级别 仍旧不可以获取到
org.springframework.web.servlet.DispatcherServlet#doDispatch protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; boolean multipartRequestParsed = false; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try { ModelAndView mv = null; Exception dispatchException = null; try { //检测是否为文件上传,如果满足条件则进行解析 processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest != request); // Determine handler for the current request. mappedHandler = getHandler(processedRequest); if (mappedHandler == null) { noHandlerFound(processedRequest, response); return; } org.springframework.web.multipart.support.StandardMultipartHttpServletRequest#parseRequest private void parseRequest(HttpServletRequest request) { try { Collection<Part> parts = request.getParts(); //消费inputstream this.multipartParameterNames = new LinkedHashSet<>(parts.size()); MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<>(parts.size()); for (Part part : parts) { String headerValue = part.getHeader(HttpHeaders.CONTENT_DISPOSITION); ContentDisposition disposition = ContentDisposition.parse(headerValue); String filename = disposition.getFilename(); if (filename != null) { if (filename.startsWith("=?") && filename.endsWith("?=")) { filename = MimeDelegate.decode(filename); } files.add(part.getName(), new StandardMultipartFile(part, filename)); } else { this.multipartParameterNames.add(part.getName()); } } setMultipartFiles(files); } catch (Throwable ex) { handleParseFailure(ex); } }
设置 spring.servlet.multipart.enabled=false(默认为开),便可以获取到,但是就没法采用 MultipartFile file
其实没有必要, file.getInputStream() 就可以获取到输入流,但是该流是必须上传完毕以后才可以获取,其实读的是本地的缓存问题
注意:只要指定的级别不包含 logRequest 所在的包,都不会因为框架记录日志而影响
对于 application/x-www-form-urlencoded 用 @RequestBody 总是可以获取到,因为框架会根据 getParameterMap进行重建
org.springframework.web.method.support.InvocableHandlerMethod#invokeForRequest /** * Invoke the method after resolving its argument values in the context of the given request. * <p>Argument values are commonly resolved through * {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}. * The {@code providedArgs} parameter however may supply argument values to be used directly, * i.e. without argument resolution. Examples of provided argument values include a * {@link WebDataBinder}, a {@link SessionStatus}, or a thrown exception instance. * Provided argument values are checked before argument resolvers. * <p>Delegates to {@link #getMethodArgumentValues} and calls {@link #doInvoke} with the * resolved arguments. * @param request the current request * @param mavContainer the ModelAndViewContainer for this request * @param providedArgs "given" arguments matched by type, not resolved * @return the raw value returned by the invoked method * @throws Exception raised if no suitable argument resolver can be found, * or if the method raised an exception * @see #getMethodArgumentValues * @see #doInvoke */ @Nullable public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { //计算要调用的controller的参数 Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs); if (logger.isTraceEnabled()) { logger.trace("Arguments: " + Arrays.toString(args)); } return doInvoke(args); } //计算 @RequestBody 注解的参数, 调用 getBody方法 org.springframework.http.server.ServletServerHttpRequest#getBody @Override public InputStream getBody() throws IOException { if (isFormPost(this.servletRequest)) { //从 request.getParameterMap(); 重新计算body,因为调用 request.getParameterMap body会被消费 return getBodyFromServletRequestParameters(this.servletRequest); } else { //否则返回真实 InputStream, 注意这个 InputStream, 有可能已经被消费了,所以有可能为可 return this.servletRequest.getInputStream(); } } org.springframework.http.server.ServletServerHttpRequest#getBodyFromServletRequestParameters /** * Use {@link javax.servlet.ServletRequest#getParameterMap()} to reconstruct the * body of a form 'POST' providing a predictable outcome as opposed to reading * from the body, which can fail if any other code has used the ServletRequest * to access a parameter, thus causing the input stream to be "consumed". */ private static InputStream getBodyFromServletRequestParameters(HttpServletRequest request) throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(1024); Writer writer = new OutputStreamWriter(bos, FORM_CHARSET); //根据参数map重新生成body //注意 因为map中包含get参数,所以生成的body中也包含get参数,所以 @RequestBody 并不一定完全等于真实body参数 Map<String, String[]> form = request.getParameterMap(); for (Iterator<String> nameIterator = form.keySet().iterator(); nameIterator.hasNext();) { String name = nameIterator.next(); List<String> values = Arrays.asList(form.get(name)); for (Iterator<String> valueIterator = values.iterator(); valueIterator.hasNext();) { String value = valueIterator.next(); writer.write(URLEncoder.encode(name, FORM_CHARSET.name())); if (value != null) { writer.write('='); writer.write(URLEncoder.encode(value, FORM_CHARSET.name())); if (valueIterator.hasNext()) { writer.write('&'); } } } if (nameIterator.hasNext()) { writer.append('&'); } } writer.flush(); return new ByteArrayInputStream(bos.toByteArray()); }
2、过滤器导致
参考:
https://github.com/spring-projects/spring-framework/issues/24176