记录一下前段时间遇到的一个关于multipartResolver的异常,以及后面找出原因的过程。
异常如下:
2018-01-22 18:05:38.041 ERROR com.exception.ExceptionHandler.resolveException:22 -Could not Q multipart servlet request; nested exception is org.apache.commons.fileupload.FileUploadBase$IOFileUploadException: Processing of multipart/form-data request failed. null org.springframework.web.multipart.MultipartException: Could not parse multipart servlet request; nested exception is org.apache.commons.fileupload.FileUploadBase$IOFileUploadException: Processing of multipart/form-data request failed. null at org.springframework.web.multipart.commons.CommonsMultipartResolver.parseRequest(CommonsMultipartResolver.java:165) ~[spring-web-4.2.5.RELEASE.jar:4.2.5.RELEASE] at org.springframework.web.multipart.commons.CommonsMultipartResolver.resolveMultipart(CommonsMultipartResolver.java:142) ~[spring-web-4.2.5.RELEASE.jar:4.2.5.RELEASE] at org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1089) [spring-webmvc-4.2.5.RELEASE.jar:4.2.5.RELEASE] at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:928) [spring-webmvc-4.2.5.RELEASE.jar:4.2.5.RELEASE] at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:893) [spring-webmvc-4.2.5.RELEASE.jar:4.2.5.RELEASE] at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:968) [spring-webmvc-4.2.5.RELEASE.jar:4.2.5.RELEASE] at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:870) [spring-webmvc-4.2.5.RELEASE.jar:4.2.5.RELEASE] at javax.servlet.http.HttpServlet.service(HttpServlet.java:661) [servlet-api.jar:na] at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:844) [spring-webmvc-4.2.5.RELEASE.jar:4.2.5.RELEASE] at javax.servlet.http.HttpServlet.service(HttpServlet.java:742) [servlet-api.jar:na] at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) [catalina.jar:8.5.24] at org.apache.catalina.core.A
这个异常大意是说multipart/form-data传输的表单存在空值,没有办法从request的表单中读到某个值。
确定了请求本身非空值之后,去看看是不是SpringMVC接收请求并从请求中读出参数的过程中出了问题。
那么,SpringMVC是如何处理请求传过来的文件的呢?
首先,Spring提供了对文件多路上传的支持,只要注册一个名为"multipartResolver"的bean,那么后续SpringMVC的DispatcherServlet在接收到请求的时候,会判断请求是不是multipart文件。 如果是的话,就会调用"multipartResolver",将请求包装成一个MultipartHttpServletRequest对象,然后后面就可以从这个对象中取出文件来进行处理了。
Spring提供了一个对于MultipartResolver接口的实现:org.springframework.web.multipart.commons.CommonsMultipartResolver。看一下源码:
public class CommonsMultipartResolver extends CommonsFileUploadSupport implements MultipartResolver, ServletContextAware { ... }
CommonsFileUploadSupport是对于XML配置"multipartResolver"时的支持。 在XML配置multipartResolver时的配置如下:
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <!-- 默认编码 --> <property name="defaultEncoding" value="utf-8" /> <!-- 设置multipart请求所允许的最大大小,默认不限制 --> <property name="maxUploadSize" value="10485760000" /> <!-- 设置一个大小,multipart请求小于这个大小时会存到内存中,大于这个内存会存到硬盘中 --> <property name="maxInMemorySize" value="40960" /> </bean>
这些property配置会被加载到CommonsFileUploadSupport中,然后被CommonsMultipartResolver继承。
然后就是,其实CommonsMultipartResolver依赖于Apache的jar包来实现:common-fileupload。
CommonsMultipartResolver接收到请求之后,是这样对HttpServletReques进行处理的:
(CommonsMultipartResolver文件) @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 { //这里对request进行了解析 MultipartParsingResult parsingResult = parseRequest(request); return new DefaultMultipartHttpServletRequest(request, parsingResult.getMultipartFiles(), parsingResult.getMultipartParameters(), parsingResult.getMultipartParameterContentTypes()); } }
this.resolveLazily是懒加载,如果为true,会在initializeMultipart()被调用,即发起文档信息获取的时候,才去封装DefaultMultipartHttpServletRequest;如果为false,立即封装DefaultMultipartHttpServletRequest。
然后再去看一下parseRequest(request)的解析:
(CommonsMultipartResolver文件) /** * Parse the given servlet request, resolving its multipart elements. * 对servlet请求进行处理,转成multipart结构 * @param request the request to parse * @return the parsing result * @throws MultipartException if multipart resolution failed. */ protected MultipartParsingResult parseRequest(HttpServletRequest request) throws MultipartException { //从请求中读出这个请求的编码 String encoding = determineEncoding(request); //按照请求的编码,获取一个FileUpload对象,装载到CommonsFileUploadSupport的property属性都会被装入这个对象中 //prepareFileUpload是继承自CommonsFileUploadSupport的函数,会比较请求的编码和XML中配置的编码,如果不一样,会拒绝处理 FileUpload fileUpload = prepareFileUpload(encoding); try { //对请求中的multipart文件进行具体的处理 List<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request); return parseFileItems(fileItems, encoding); } catch (FileUploadBase.SizeLimitExceededException ex) { throw new MaxUploadSizeExceededException(fileUpload.getSizeMax(), ex); } catch (FileUploadException ex) { throw new MultipartException("Could not parse multipart servlet request", ex); } }
上面的((ServletFileUpload) fileUpload).parseRequest(request)解析实现如下:
(FileUploadBase文件) /** * Processes an <a href="http://www.ietf.org/rfc/rfc1867.txt">RFC 1867</a> * compliant <code>multipart/form-data</code> stream. * * @param ctx The context for the request to be parsed. * * @return A list of <code>FileItem</code> instances parsed from the * request, in the order that they were transmitted. * * @throws FileUploadException if there are problems reading/parsing * the request or storing files. */ public List<FileItem> parseRequest(RequestContext ctx) throws FileUploadException { List<FileItem> items = new ArrayList<FileItem>(); boolean successful = false; try { //从请求中取出multipart文件 FileItemFactoryFactoryFactoryator iter = getItemIterator(ctx); //获得FileItemFactory工厂,实现类为DiskFileItemFactory FileItemFactory fac = getFileItemFactory(); if (fac == null) { throw new NullPointerException("No FileItemFactory has been set."); } while (iter.hasNext()) { final FileItemStream item = iter.next(); // Don't use getName() here to prevent an InvalidFileNameException. final String fileName = ((FileItemIteratorImpl.FileItemStreamImpl) item).name; //工厂模式,获取FileItem对象,实现类是DiskFileItem FileItem fileItem = fac.createItem(item.getFieldName(), item.getContentType(), item.isFormField(), fileName); items.add(fileItem); try { Streams.copy(item.openStream(), fileItem.getOutputStream(), true); } 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 (Throwable e) { // ignore it } } } } }
到此,List对象就处理完返回了,然后再继续看对List的处理
(CommonsFileUploadSupport文件) /** * Parse the given List of Commons FileItems into a Spring MultipartParsingResult, * containing Spring MultipartFile instances and a Map of multipart parameter. * @param fileItems the Commons FileIterms to parse * @param encoding the encoding to use for form fields * @return the Spring MultipartParsingResult * @see CommonsMultipartFile#CommonsMultipartFile(org.apache.commons.fileupload.FileItem) */ protected MultipartParsingResult parseFileItems(List<FileItem> fileItems, String encoding) { MultiValueMap<String, MultipartFile> multipartFiles = new LinkedMultiValueMap<String, MultipartFile>(); Map<String, String[]> multipartParameters = new HashMap<String, String[]>(); Map<String, String> multipartParameterContentTypes = new HashMap<String, String>(); // Extract multipart files and multipart parameters. for (FileItem fileItem : fileItems) { //如果fileItem是一个表单 if (fileItem.isFormField()) { String value; String partEncoding = determineEncoding(fileItem.getContentType(), encoding); if (partEncoding != null) { try { value = fileItem.getString(partEncoding); } catch (UnsupportedEncodingException ex) { if (logger.isWarnEnabled()) { logger.warn("Could not decode multipart item '" + fileItem.getFieldName() + "' with encoding '" + partEncoding + "': using platform default"); } value = fileItem.getString(); } } else { value = fileItem.getString(); } String[] curParam = multipartParameters.get(fileItem.getFieldName()); if (curParam == null) { // simple form field multipartParameters.put(fileItem.getFieldName(), new String[] {value}); } else { // array of simple form fields String[] newParam = StringUtils.addStringToArray(curParam, value); multipartParameters.put(fileItem.getFieldName(), newParam); } multipartParameterContentTypes.put(fileItem.getFieldName(), fileItem.getContentType()); } //如果fileItem是一个multipart文件 else { // multipart file field CommonsMultipartFile file = new CommonsMultipartFile(fileItem); multipartFiles.add(file.getName(), file); if (logger.isDebugEnabled()) { logger.debug("Found multipart file [" + file.getName() + "] of size " + file.getSize() + " bytes with original filename [" + file.getOriginalFilename() + "], stored " + file.getStorageDescription()); } } } return new MultipartParsingResult(multipartFiles, multipartParameters, multipartParameterContentTypes); }
到此,MultipartParsingResult的处理就结束并返回了,然后CommonsMultipartResolver中的resolveMultipart就将其装到DefaultMultipartHttpServletRequest中并返回,处理完了。
DefaultMultipartHttpServletRequest是MultipartHttpServletRequest的实现类。
前面已经说过,maxInMemorySize的作用是**“设置一个大小,multipart请求小于这个大小时会存到内存中,大于这个内存会存到硬盘中”**。 再看一下maxInMemorySize被set到对象中的过程:
(CommonsFileUploadSupport文件) /** * Set the maximum allowed size (in bytes) before uploads are written to disk. * Uploaded files will still be received past this amount, but they will not be * stored in memory. Default is 10240, according to Commons FileUpload. * @param maxInMemorySize the maximum in memory size allowed * @see org.apache.commons.fileupload.disk.DiskFileItemFactory#setSizeThreshold */ public void setMaxInMemorySize(int maxInMemorySize) { this.fileItemFactory.setSizeThreshold(maxInMemorySize); }
CommonsFileUploadSupport中有一个fileItemFactory对象,maxInMemorySize就被set到了这个工厂类的属性SizeThreshold里。
这个fileItemFactory工厂类,会在生成fileItem对象的时候用到。 生成这个对象的过程中,会根据maxInMemorySize来判断,是将其存到内存中,还是存到硬盘中。
存储的过程在前面已经提过了:
... try { Streams.copy(item.openStream(), fileItem.getOutputStream(), true); } 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.getOutputStream()看看:
/** * Returns an {@link java.io.OutputStream OutputStream} that can * be used for storing the contents of the file. * * @return An {@link java.io.OutputStream OutputStream} that can be used * for storing the contensts of the file. * * @throws IOException if an error occurs. */ public OutputStream getOutputStream() throws IOException { if (dfos == null) { File outputFile = getTempFile(); dfos = new DeferredFileOutputStream(sizeThreshold, outputFile); } return dfos; }
再进去getTempFile():
/** * Creates and returns a {@link java.io.File File} representing a uniquely * named temporary file in the configured repository path. The lifetime of * the file is tied to the lifetime of the <code>FileItem</code> instance; * the file will be deleted when the instance is garbage collected. * * @return The {@link java.io.File File} to be used for temporary storage. */ protected File getTempFile() { if (tempFile == null) { File tempDir = repository; if (tempDir == null) { tempDir = new File(System.getProperty("java.io.tmpdir")); } String tempFileName = format("upload_%s_%s.tmp", UID, getUniqueId()); tempFile = new File(tempDir, tempFileName); } return tempFile; }
原本我以为是因为没有设置缓存到硬盘的路径,也就是repository,后来发现当没有设置repository的时候也会自动选择一个缓存路径,所以不是这个问题。
注意看这个注释:**the file will be deleted when the instance is garbage collected.**这里说了FileItem的实例声明周期,当GC的时候,存在内存里的FileItem会被GC回收掉。所以这就是为什么没有办法读到multipart/form-data对象。
把这个问题写了这么多,最后的解决方案却写的很少,看起来可能是很傻,但是是有原因的: