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