xiaobaoqiu Blog

Think More, Code Less

Spring上传组件冲突

最近会整理记录这一段时间组内同事以及个人遇到的一些问题。

1.背景

问题:Spring上传组件的冲突 背景:同事在原有的上传模块中增加了一个新的文件接口,导致就的上传接口不可用的故障。

新增的配置就是Spring自带的上传解析器:

1
2
3
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <property name="defaultEncoding" value="utf-8"/>
</bean>

示例代码如下:

1
2
3
ApiResult upload(@RequestParam("uploadFile") CommonsMultipartFile uploadFile) {
    ...
}

原始的上传使用的是 commons-fileupload 组件,简单示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
upload(HttpServletRequest request) {
        //1.从请求中解析出文件
        ServletFileUpload servletFileUpload = createServletFileUpload();
        List<FileItem> itemList = servletFileUpload.parseRequest(request);
        Map<String, Serializable> fields = getFileFields(itemList);
        FileItem file = (FileItem) fields.get("Filedata");

        //2.内容写本地
        File f = new File(generateFilePath(...));
        FileOutputStream out = new FileOutputStream(f);
        out.write(file.get());
}

2.Spring上传机制分析

首先我们从Spring处理请求的源头开始分析,看看Spring是如何支持文件上传的。

1.DispatcherServlet中doDispatch方法中,如果请求是文件上传,会首先将 HttpServletRequest 请求做预处理,转换为 MultipartHttpServletRequest

1
2
3
4
5
6
7
8
9
10
11
12
13
processedRequest = checkMultipart(request);

    protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
        if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {    //是文件上传请求
            if (request instanceof MultipartHttpServletRequest) {   //已经是 MultipartHttpServletRequest 了
            }
            else {
                return this.multipartResolver.resolveMultipart(request);    //转换为 MultipartHttpServletRequest
            }
        }
        // If not returned before: return original request.
        return request;
    }

这块两个问题:

1.如何判定一个请求是不是文件上传请求
2.转换为 MultipartHttpServletRequest 做了什么事

先看如何判定审核文件上传请求,就是 multipartResolver.isMultipart 的代码,跟代码会涉及到 CommonsMultipartResolver, ServletFileUpload, FileUploadBase,发现判定有两个条件

(1). 请求是 Post 请求. 参见 ServletFileUpload.isMultipartContent()
(2).请求 contentType 必须以 multipart/ 开通. 参见 FileUploadBase.isMultipartContent()

下面看第二个问题,转换 MultipartHttpServletRequest 做的什么事,这个工作由 MultipartResolver.resolveMultipart 完成,我们可以看看注释:

    /**
     * Parse the given HTTP request into multipart files and parameters,
     * and wrap the request inside a
     * {@link org.springframework.web.multipart.MultipartHttpServletRequest} object
     * that provides access to file descriptors and makes contained
     * parameters accessible via the standard ServletRequest methods.
     */
    MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;

其主要的代码实现在,主要的工作是, 首先将HttpServletRequest的inputStream最终塞入了FileItemStreamImpl的stream中,随后ServletFileUpload类逐个对FileItemStream进行处理(生成FileItem),通过Streams.copy方法对inputStream进行read操作,此时request中的inputStream被消耗(inputStream只能被读取一次), 最后将List返回,ServletFileUpload的解析方法执行完毕.

     public List<FileItem> parseRequest(RequestContext ctx) throws FileUploadException {
        List<FileItem> items = new ArrayList<FileItem>();
        FileItemIterator iter = getItemIterator(ctx);       //1. 构造FileItemStreamImpl
        ...
        while (iter.hasNext()) {                                            //2.逐个对FileItemStream进行处理
            final FileItemStream item = iter.next();
            ...
            FileItem fileItem = fac.createItem(item.getFieldName(), item.getContentType(), item.isFormField(), fileName);       //2.1 构造一个 FileItem
            items.add(fileItem);
            ...
            Streams.copy(item.openStream(), fileItem.getOutputStream(), true);      //2.2 数据读取
        }
        return items;   // 3. 返回
    }

// 下面是更详细的代码
//1.将HttpServletRequest的inputStream最终塞入了FileItemStreamImpl的stream中
FileItemIteratorImpl(RequestContext ctx) throws FileUploadException, IOException {
    InputStream input = ctx.getInputStream();
    ...
    multi = new MultipartStream(input, boundary, notifier);
    ...
    findNextItem();
}

//2.2 数据读取,看看如何将 request 的 inputstream 数据拷贝到 FileItem 中
    /**
     * Copies the contents of the given {@link InputStream} to the given {@link OutputStream}.
     */
    public static long copy(InputStream inputStream, OutputStream outputStream, boolean closeOutputStream)
            throws IOException {
        return copy(inputStream, outputStream, closeOutputStream, new byte[DEFAULT_BUFFER_SIZE]);
    }

主要代码的位置:CommonsMultipartResolver.parseRequest, FileUploadBase.parseRequest, FileItemIteratorImpl

一个需要注意的点,在DispatcherServlet 中, MultipartResolver 的Bean的名称是写死的:

    /** Well-known name for the MultipartResolver object in the bean factory for this namespace. */
    public static final String MULTIPART_RESOLVER_BEAN_NAME = "multipartResolver";

    private void initMultipartResolver(ApplicationContext context) {
        try {
            this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class);    //bean的name固定
            if (logger.isDebugEnabled()) {
                logger.debug("Using MultipartResolver [" + this.multipartResolver + "]");
            }
        }
    }

3.冲突分析

multipartResolver是一个全局的文件上传处理器,配置上 multipartResolver 这个Bean之后,全局的文件上传都会经过 multipartResolver 处理(读取并解析request的 inputstream ).而 inputstream 仅能处理一次,导致处理完的 HttpServletRequest 中的 inputStream 已经没有内容.

因此后面配置使用的 commons-fileupload 的 ServletFileUpload 无法从 request 中解析出文件上传内容.

4.问题解决

几个解决方案:

1.全局统一使用一个解析方式(统一使用 ServletFileUpload 或者 MultipartResolver 方式);
2.继承 CommonsMultipartResolver 实现自定义的 MultipartResolver, 覆写isMultipart方法, 仅部分 url 的上传请求走我们自定义的 MultipartResolver 处理器,保证新老逻辑兼容;