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
