从multipartResolver的一个异常到multipartResolver源码分析

记录一下前段时间遇到的一个关于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是如何处理请求传过来的文件的呢?

multipartResolver处理请求的过程

DispatcherServlet转发

首先,Spring提供了对文件多路上传的支持,只要注册一个名为"multipartResolver"的bean,那么后续SpringMVC的DispatcherServlet在接收到请求的时候,会判断请求是不是multipart文件。
如果是的话,就会调用"multipartResolver",将请求包装成一个MultipartHttpServletRequest对象,然后后面就可以从这个对象中取出文件来进行处理了。

multipartResolver的装载

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的处理过程

然后就是,其实CommonsMultipartResolver依赖于Apache的jar包来实现:common-fileupload。

TIM截图20180201193231.png

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。

resolveLazily默认为false。

然后再去看一下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<FileItem>对象就处理完返回了,然后再继续看对List<FileItem>的处理

(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

前面已经说过,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;
    }

当没有设置uploadTempDir属性,也就是FileItemFactory中的repository的时候会自动选择一个缓存路径System.getProperty("java.io.tmpdir"),将上传请求落到硬盘上的这个位置。

注意看这个注释:the file will be deleted when the instance is garbage collected. 这里说了FileItem的实例声明周期,当GC的时候,存在内存里的FileItem会被GC回收掉。所以这就是为什么没有办法读到multipart/form-data对象。

bug原因和解决方案

  1. 解决频繁GC的问题。太过频繁的GC明显是出了问题了,导致请求中的文件被回收掉,报空指针。(这也是我这边解决问题的方案)
  2. 设置好maxInMemorySize和uploadTempDir两个属性,保证上传文件缓存到硬盘上,普通请求在内存中就可以了。如果涉及大量的文件上传,这个是很有必要的,不然并发高的时候,内存会被文件给占满。然后会触发GC,FileItem被回收掉之后,后面就会再去读取,就被出现我们异常中的空指针错误。
  3. 还有一种可能性,就是multipartResolver配置的时候,没有设置uploadTempDir属性。按理说这个是没有问题的,因为会默认帮你设为系统的缓存路径,这个路径通常是/tmp,这个目录所有用户都有权限读取。但是如果是生产环境,这个系统默认的缓存路径很可能会被修改过,修改了位置,或者权限。这也是为了安全的方面考虑,但是这在我们所讲的流程中,就会造成后面读取的时候,出现空指针的错误。

这些异常都不容易排查,所以需要对整个流程都清晰了之后,才容易找到问题的所在。单单看自己的代码是不能看出来的,例如权限的问题,在实际生产环境中才会遇到,也比较无奈。

我为什么要把这个问题写的这么复杂

把这个问题写了这么多,最后的解决方案却写的很少,看起来可能是很傻,但是是有原因的:

  1. 这个异常在网上没有找到有用的解决方案,也没有看到讲明白原因的
  2. 把流程理一遍,有利于后面遇到类似问题的时候,更好地解决。例如说不是报空值,而是报其他问题的时候。
  3. 最重要的一点,由于运行环境的原因,没有办法复现,也没有找到当时的堆栈信息,所以不得不整个流程都走了一遍......
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容

  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,169评论 11 349
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,565评论 18 399
  • 〔ps.可能正文和标题完全没有关系,因为实在想不到题目了,文章的脑洞开得有点大吧,朋友都说看不懂,希望能有人看懂,...
    哈拉米阅读 316评论 0 1
  • 琴音分享 2016.12.31 ——关于心理营养“生命的至重” 女儿今天问了我一个这样的问题:爸爸,如果我和妈妈同...
    王燕惠阅读 2,804评论 0 1
  • 这几天和闺密来上海看F1,今天顺便约了高中同学吃饭。 先说一下,约的是个男生,我高中的同桌,学霸级别,高中时班主任...
    2ec19d3a3f77阅读 284评论 0 0