
背景
起因:调用上传图片接口出现错误Required request part 'file' is not present
CURL:
shell
curl --location 'localhost:8080/file/upload' \
--form 'file=@"/C:/Users/Leaf/Pictures/头像/a.jpg"'
检查了接口传参Content-Type
类型正常,为multipart/form-data
报错异常:
shell
org.springframework.web.multipart.support.MissingServletRequestPartException: Required request part 'file' is not present
at org.springframework.web.method.annotation.RequestParamMethodArgumentResolver.handleMissingValueInternal(RequestParamMethodArgumentResolver.java:213) ~[spring-web-5.3.30.jar:5.3.30]
at org.springframework.web.method.annotation.RequestParamMethodArgumentResolver.handleMissingValue(RequestParamMethodArgumentResolver.java:193) ~[spring-web-5.3.30.jar:5.3.30]
at org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver.resolveArgument(AbstractNamedValueMethodArgumentResolver.java:114) ~[spring-web-5.3.30.jar:5.3.30]
at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:122) ~[spring-web-5.3.30.jar:5.3.30]
at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:179) ~[spring-web-5.3.30.jar:5.3.30]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:146) ~[spring-web-5.3.30.jar:5.3.30]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117) ~[spring-webmvc-5.3.30.jar:5.3.30]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895) ~[spring-webmvc-5.3.30.jar:5.3.30]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808) ~[spring-webmvc-5.3.30.jar:5.3.30]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.3.30.jar:5.3.30]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1072) ~[spring-webmvc-5.3.30.jar:5.3.30]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:965) ~[spring-webmvc-5.3.30.jar:5.3.30]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.30.jar:5.3.30]
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909) ~[spring-webmvc-5.3.30.jar:5.3.30]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:555) ~[tomcat-embed-core-9.0.82.jar:4.0.FR]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.3.30.jar:5.3.30]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:623) ~[tomcat-embed-core-9.0.82.jar:4.0.FR]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:209) ~[tomcat-embed-core-9.0.82.jar:9.0.82]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153) ~[tomcat-embed-core-9.0.82.jar:9.0.82]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) ~[tomcat-embed-websocket-9.0.82.jar:9.0.82]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178) ~[tomcat-embed-core-9.0.82.jar:9.0.82]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153) ~[tomcat-embed-core-9.0.82.jar:9.0.82]
at com.leaf.filter.RequestFilter.doFilter(RequestFilter.java:30) ~[classes/:na]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178) ~[tomcat-embed-core-9.0.82.jar:9.0.82]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153) ~[tomcat-embed-core-9.0.82.jar:9.0.82]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.3.30.jar:5.3.30]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.30.jar:5.3.30]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178) ~[tomcat-embed-core-9.0.82.jar:9.0.82]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153) ~[tomcat-embed-core-9.0.82.jar:9.0.82]
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.3.30.jar:5.3.30]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.30.jar:5.3.30]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178) ~[tomcat-embed-core-9.0.82.jar:9.0.82]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153) ~[tomcat-embed-core-9.0.82.jar:9.0.82]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.3.30.jar:5.3.30]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.30.jar:5.3.30]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178) ~[tomcat-embed-core-9.0.82.jar:9.0.82]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153) ~[tomcat-embed-core-9.0.82.jar:9.0.82]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:168) ~[tomcat-embed-core-9.0.82.jar:9.0.82]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[tomcat-embed-core-9.0.82.jar:9.0.82]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:481) ~[tomcat-embed-core-9.0.82.jar:9.0.82]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:130) ~[tomcat-embed-core-9.0.82.jar:9.0.82]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[tomcat-embed-core-9.0.82.jar:9.0.82]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-9.0.82.jar:9.0.82]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342) ~[tomcat-embed-core-9.0.82.jar:9.0.82]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:390) ~[tomcat-embed-core-9.0.82.jar:9.0.82]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-9.0.82.jar:9.0.82]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:928) ~[tomcat-embed-core-9.0.82.jar:9.0.82]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1794) ~[tomcat-embed-core-9.0.82.jar:9.0.82]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-9.0.82.jar:9.0.82]
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[tomcat-embed-core-9.0.82.jar:9.0.82]
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-9.0.82.jar:9.0.82]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-9.0.82.jar:9.0.82]
at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]
分析原因
- 代码中请求过滤器处理请求之前,调用过getParameterMap()方法,该方法是通过读取请求的输入流来获取数据的。关键点在于:Servlet 的输入流只能被读取一次;
- 代码中配置了
CommonsMultipartResolver
解析器来解析文件请求,这个解析器也是通过读取请求的输入流来获取数据,因此读取流的时候失败,因为输入流已经被消费了; - 而如果不配置 MultipartResolver ,Spring Boot 会使用默认的
StandardServletMultipartResolver
,它是基于 Servlet 3.0+ 的Part
API 实现的,而不是直接读取输入流。它的核心是request.getParts()
方法; - Servlet 容器在第一次调用
getParts()
时会解析整个请求体,将解析结果缓存在请求对象中,后续调用getParts()
时直接返回缓存的结果,而不是重新解析。
CommonsMultipartResolver
和StandardServletMultipartResolver
主要区别:
-
解析时机:
- StandardServletMultipartResolver:依赖 Servlet 容器的
getParts()
方法,只解析一次 - CommonsMultipartResolver:直接读取输入流,每次都需要重新解析
- StandardServletMultipartResolver:依赖 Servlet 容器的
-
数据存储:
- StandardServletMultipartResolver:数据由 Servlet 容器缓存
- CommonsMultipartResolver:需要自己管理数据
-
性能影响:
- StandardServletMultipartResolver:第一次解析后,后续调用都是使用缓存
- CommonsMultipartResolver:每次都需要重新解析
建议
- 如果使用 Servlet 3.0+ 环境,优先使用 StandardServletMultipartResolver
- 如果需要更细粒度的控制或自定义解析逻辑,才考虑使用 CommonsMultipartResolver
- 如果使用 CommonsMultipartResolver,要避免在解析前调用
request.getParameterMap()
源码
getParameterMap()
部分源码:
java
@Override
public Map<String,String[]> getParameterMap() {
if (parameterMap.isLocked()) {
return parameterMap;
}
Enumeration<String> enumeration = getParameterNames();
while (enumeration.hasMoreElements()) {
String name = enumeration.nextElement();
String[] values = getParameterValues(name);
parameterMap.put(name, values);
}
parameterMap.setLocked(true);
return parameterMap;
}
@Override
public Enumeration<String> getParameterNames() {
if (!parametersParsed) {
parseParameters();
}
return coyoteRequest.getParameters().getParameterNames();
}
protected void parseParameters() {
parametersParsed = true;
Parameters parameters = coyoteRequest.getParameters();
boolean success = false;
try {
// Set this every time in case limit has been changed via JMX
int maxParameterCount = getConnector().getMaxParameterCount();
if (parts != null && maxParameterCount > 0) {
maxParameterCount -= parts.size();
}
parameters.setLimit(maxParameterCount);
// getCharacterEncoding() may have been overridden to search for
// hidden form field containing request encoding
Charset charset = getCharset();
boolean useBodyEncodingForURI = connector.getUseBodyEncodingForURI();
parameters.setCharset(charset);
if (useBodyEncodingForURI) {
parameters.setQueryStringCharset(charset);
}
// Note: If !useBodyEncodingForURI, the query string encoding is
// that set towards the start of CoyoteAdapter.service()
parameters.handleQueryParameters();
if (usingInputStream || usingReader) {
success = true;
return;
}
String contentType = getContentType();
if (contentType == null) {
contentType = "";
}
int semicolon = contentType.indexOf(';');
if (semicolon >= 0) {
contentType = contentType.substring(0, semicolon).trim();
} else {
contentType = contentType.trim();
}
if ("multipart/form-data".equals(contentType)) {
parseParts(false);
success = true;
return;
}
// ...
success = true;
} finally {
if (!success) {
parameters.setParseFailedReason(FailReason.UNKNOWN);
}
}
}
// parts.add(part); 后续用默认的StandardServletMultipartResolver解析,会调用getPart()能够从缓存拿到数据
private void parseParts(boolean explicit) {
// Return immediately if the parts have already been parsed
if (parts != null || partsParseException != null) {
return;
}
Context context = getContext();
MultipartConfigElement mce = getWrapper().getMultipartConfigElement();
if (mce == null) {
if (context.getAllowCasualMultipartParsing()) {
mce = new MultipartConfigElement(null, connector.getMaxPostSize(), connector.getMaxPostSize(),
connector.getMaxPostSize());
} else {
if (explicit) {
partsParseException = new IllegalStateException(sm.getString("coyoteRequest.noMultipartConfig"));
return;
} else {
parts = Collections.emptyList();
return;
}
}
}
int maxParameterCount = getConnector().getMaxParameterCount();
Parameters parameters = coyoteRequest.getParameters();
parameters.setLimit(maxParameterCount);
boolean success = false;
try {
File location;
String locationStr = mce.getLocation();
if (locationStr == null || locationStr.length() == 0) {
location = ((File) context.getServletContext().getAttribute(ServletContext.TEMPDIR));
} else {
// If relative, it is relative to TEMPDIR
location = new File(locationStr);
if (!location.isAbsolute()) {
location = new File((File) context.getServletContext().getAttribute(ServletContext.TEMPDIR),
locationStr).getAbsoluteFile();
}
}
if (!location.exists() && context.getCreateUploadTargets()) {
log.warn(sm.getString("coyoteRequest.uploadCreate", location.getAbsolutePath(),
getMappingData().wrapper.getName()));
if (!location.mkdirs()) {
log.warn(sm.getString("coyoteRequest.uploadCreateFail", location.getAbsolutePath()));
}
}
if (!location.isDirectory()) {
parameters.setParseFailedReason(FailReason.MULTIPART_CONFIG_INVALID);
partsParseException = new IOException(sm.getString("coyoteRequest.uploadLocationInvalid", location));
return;
}
// Create a new file upload handler
DiskFileItemFactory factory = new DiskFileItemFactory();
try {
factory.setRepository(location.getCanonicalFile());
} catch (IOException ioe) {
parameters.setParseFailedReason(FailReason.IO_ERROR);
partsParseException = ioe;
return;
}
factory.setSizeThreshold(mce.getFileSizeThreshold());
ServletFileUpload upload = new ServletFileUpload();
upload.setFileItemFactory(factory);
upload.setFileSizeMax(mce.getMaxFileSize());
upload.setSizeMax(mce.getMaxRequestSize());
if (maxParameterCount > -1) {
// There is a limit. The limit for parts needs to be reduced by
// the number of parameters we have already parsed.
// Must be under the limit else parsing parameters would have
// triggered an exception.
upload.setFileCountMax(maxParameterCount - parameters.size());
}
parts = new ArrayList<>();
try {
List<FileItem> items = upload.parseRequest(new ServletRequestContext(this));
int maxPostSize = getConnector().getMaxPostSize();
int postSize = 0;
Charset charset = getCharset();
for (FileItem item : items) {
ApplicationPart part = new ApplicationPart(item, location);
parts.add(part);
if (part.getSubmittedFileName() == null) {
String name = part.getName();
if (maxPostSize >= 0) {
// Have to calculate equivalent size. Not completely
// accurate but close enough.
postSize += name.getBytes(charset).length;
// Equals sign
postSize++;
// Value length
postSize += part.getSize();
// Value separator
postSize++;
if (postSize > maxPostSize) {
parameters.setParseFailedReason(FailReason.POST_TOO_LARGE);
throw new IllegalStateException(sm.getString("coyoteRequest.maxPostSizeExceeded"));
}
}
String value = null;
try {
value = part.getString(charset.name());
} catch (UnsupportedEncodingException uee) {
// Not possible
}
parameters.addParameter(name, value);
}
}
success = true;
} catch (InvalidContentTypeException e) {
parameters.setParseFailedReason(FailReason.INVALID_CONTENT_TYPE);
partsParseException = new ServletException(e);
} catch (SizeException e) {
parameters.setParseFailedReason(FailReason.POST_TOO_LARGE);
checkSwallowInput();
partsParseException = new IllegalStateException(e);
} catch (IOException e) {
parameters.setParseFailedReason(FailReason.IO_ERROR);
partsParseException = e;
} catch (IllegalStateException e) {
// addParameters() will set parseFailedReason
checkSwallowInput();
partsParseException = e;
}
} finally {
// This might look odd but is correct. setParseFailedReason() only
// sets the failure reason if none is currently set. This code could
// be more efficient but it is written this way to be robust with
// respect to changes in the remainder of the method.
if (partsParseException != null || !success) {
parameters.setParseFailedReason(FailReason.UNKNOWN);
}
}
}
CommonsMultipartResolver
部分源码:
java
@Override
public MultipartHttpServletRequest resolveMultipart(final HttpServletRequest request) throws MultipartException {
Assert.notNull(request, "Request must not be null");
if (this.resolveLazily) {
return new DefaultMultipartHttpServletRequest(request) {
@Override
protected void initializeMultipart() {
MultipartParsingResult parsingResult = parseRequest(request);
setMultipartFiles(parsingResult.getMultipartFiles());
setMultipartParameters(parsingResult.getMultipartParameters());
setMultipartParameterContentTypes(parsingResult.getMultipartParameterContentTypes());
}
};
}
else {
MultipartParsingResult parsingResult = parseRequest(request);
return new DefaultMultipartHttpServletRequest(request, parsingResult.getMultipartFiles(),
parsingResult.getMultipartParameters(), parsingResult.getMultipartParameterContentTypes());
}
}
protected MultipartParsingResult parseRequest(HttpServletRequest request) throws MultipartException {
String encoding = determineEncoding(request);
FileUpload fileUpload = prepareFileUpload(encoding);
try {
List<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);
return parseFileItems(fileItems, encoding);
}
catch (FileUploadBase.SizeLimitExceededException ex) {
throw new MaxUploadSizeExceededException(fileUpload.getSizeMax(), ex);
}
catch (FileUploadBase.FileSizeLimitExceededException ex) {
throw new MaxUploadSizeExceededException(fileUpload.getFileSizeMax(), ex);
}
catch (FileUploadException ex) {
throw new MultipartException("Failed to parse multipart servlet request", ex);
}
}
@Override
public List<FileItem> parseRequest(HttpServletRequest request)
throws FileUploadException {
return parseRequest(new ServletRequestContext(request));
}
public List<FileItem> parseRequest(RequestContext ctx)
throws FileUploadException {
List<FileItem> items = new ArrayList<FileItem>();
boolean successful = false;
try {
FileItemIterator iter = getItemIterator(ctx);
FileItemFactory fac = getFileItemFactory();
final byte[] buffer = new byte[Streams.DEFAULT_BUFFER_SIZE];
if (fac == null) {
throw new NullPointerException("No FileItemFactory has been set.");
}
while (iter.hasNext()) {
if (items.size() == fileCountMax) {
// The next item will exceed the limit.
throw new FileCountLimitExceededException(ATTACHMENT, getFileCountMax());
}
final FileItemStream item = iter.next();
// Don't use getName() here to prevent an InvalidFileNameException.
final String fileName = ((FileItemIteratorImpl.FileItemStreamImpl) item).name;
FileItem fileItem = fac.createItem(item.getFieldName(), item.getContentType(),
item.isFormField(), fileName);
items.add(fileItem);
try {
Streams.copy(item.openStream(), fileItem.getOutputStream(), true, buffer);
} catch (FileUploadIOException e) {
throw (FileUploadException) e.getCause();
} catch (IOException e) {
throw new IOFileUploadException(format("Processing of %s request failed. %s",
MULTIPART_FORM_DATA, e.getMessage()), e);
}
final FileItemHeaders fih = item.getHeaders();
fileItem.setHeaders(fih);
}
successful = true;
return items;
} catch (FileUploadIOException e) {
throw (FileUploadException) e.getCause();
} catch (IOException e) {
throw new FileUploadException(e.getMessage(), e);
} finally {
if (!successful) {
for (FileItem fileItem : items) {
try {
fileItem.delete();
} catch (Exception ignored) {
// ignored TODO perhaps add to tracker delete failure list somehow?
}
}
}
}
}
public FileItemIterator getItemIterator(RequestContext ctx)
throws FileUploadException, IOException {
try {
return new FileItemIteratorImpl(ctx);
} catch (FileUploadIOException e) {
// unwrap encapsulated SizeException
throw (FileUploadException) e.getCause();
}
}
// ctx.getInputStream() 调用的是ServletRequestContext的getInputStream()方法来读取流
FileItemIteratorImpl(RequestContext ctx)
throws FileUploadException, IOException {
if (ctx == null) {
throw new NullPointerException("ctx parameter");
}
String contentType = ctx.getContentType();
if ((null == contentType)
|| (!contentType.toLowerCase(Locale.ENGLISH).startsWith(MULTIPART))) {
throw new InvalidContentTypeException(
format("the request doesn't contain a %s or %s stream, content type header is %s",
MULTIPART_FORM_DATA, MULTIPART_MIXED, contentType));
}
@SuppressWarnings("deprecation") // still has to be backward compatible
final int contentLengthInt = ctx.getContentLength();
final long requestSize = UploadContext.class.isAssignableFrom(ctx.getClass())
// Inline conditional is OK here CHECKSTYLE:OFF
? ((UploadContext) ctx).contentLength()
: contentLengthInt;
// CHECKSTYLE:ON
InputStream input; // N.B. this is eventually closed in MultipartStream processing
if (sizeMax >= 0) {
if (requestSize != -1 && requestSize > sizeMax) {
throw new SizeLimitExceededException(
format("the request was rejected because its size (%s) exceeds the configured maximum (%s)",
Long.valueOf(requestSize), Long.valueOf(sizeMax)),
requestSize, sizeMax);
}
// N.B. this is eventually closed in MultipartStream processing
input = new LimitedInputStream(ctx.getInputStream(), sizeMax) {
@Override
protected void raiseError(long pSizeMax, long pCount)
throws IOException {
FileUploadException ex = new SizeLimitExceededException(
format("the request was rejected because its size (%s) exceeds the configured maximum (%s)",
Long.valueOf(pCount), Long.valueOf(pSizeMax)),
pCount, pSizeMax);
throw new FileUploadIOException(ex);
}
};
} else {
input = ctx.getInputStream();
}
String charEncoding = headerEncoding;
if (charEncoding == null) {
charEncoding = ctx.getCharacterEncoding();
}
boundary = getBoundary(contentType);
if (boundary == null) {
IOUtils.closeQuietly(input); // avoid possible resource leak
throw new FileUploadException("the request was rejected because no multipart boundary was found");
}
notifier = new MultipartStream.ProgressNotifier(listener, requestSize);
try {
multi = new MultipartStream(input, boundary, notifier);
} catch (IllegalArgumentException iae) {
IOUtils.closeQuietly(input); // avoid possible resource leak
throw new InvalidContentTypeException(
format("The boundary specified in the %s header is too long", CONTENT_TYPE), iae);
}
multi.setHeaderEncoding(charEncoding);
skipPreamble = true;
findNextItem();
}
@Override
public InputStream getInputStream() throws IOException {
return request.getInputStream();
}
StandardServletMultipartResolver
部分源码:
java
@Override
public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {
return new StandardMultipartHttpServletRequest(request, this.resolveLazily);
}
public StandardMultipartHttpServletRequest(HttpServletRequest request, boolean lazyParsing)
throws MultipartException {
super(request);
if (!lazyParsing) {
parseRequest(request);
}
}
// request.getParts() 调用的是getParameterMap()中的parts数组,这时候可以拿到缓存数据
private void parseRequest(HttpServletRequest request) {
try {
Collection<Part> parts = request.getParts();
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);
}
}
解决方案
-
使用默认的
StandardServletMultipartResolver
; -
使用
CommonsMultipartResolver
,需要额外引入依赖commons-fileupload
,且手动提前解析multipart请求:java// 手动解析 multipart 请求 HttpServletRequest processedRequest = httpRequest; if (isMultipartRequest(httpRequest)) { try { processedRequest = multipartResolver.resolveMultipart(httpRequest); } catch (Exception e) { log.warn("Failed to parse multipart request", e); } } // 罪魁祸首,getParameterMap()中解析了multipart请求,消费了请求流,当请求到达Controller时,再次尝试解析而找不到文件报错 Map<String, String[]> parameterMap = httpRequest.getParameterMap(); chain.doFilter(processedRequest, response);