目录

问题

之前项目上传文件都是使用的 Apache 的 ServletFileUpload 组件,同事开启了 Multipart 方式上传后增加了一个新的上传文件接口,导致之前的上传接口不可用。

Spring 中 MultipartResolver 和 ServletFileUpload 上传组件冲突的问题

同事新增了 Multipart 上传逻辑:

spring:
    servlet:
    multipart:
        enabled: true
for (MultipartFile file : files) {
    if (file.isEmpty()) {
    continue; //next pls
  }

  byte[] bytes = file.getBytes();
  Path path = Paths.get(UPLOADED_FOLDER + file.getOriginalFilename());
  Files.write(path, bytes);
}

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

val isMultipart = ServletFileUpload.isMultipartContent(request)
if (isMultipart) {
    val upload = ServletFileUpload()
    val iterator = upload.getItemIterator(request)
    while (iterator.hasNext()) {
        val item = iterator.next()
        if (!item.isFormField) {
            storageFile(uploadedFiles, item)
        }
    }
}

Spring上传机制分析

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

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

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 只能被读取一次,后面又交给ServletFileUpload再读,iter.hasNext() 就会返回false), 最后将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 + "]");
            }
        }
    }

冲突分析

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

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

解决方法

1.全局统一使用一个解析方式(统一使用 ServletFileUpload 或者 MultipartResolver 方式);

2.继承 CommonsMultipartResolver 实现自定义的 MultipartResolver, 覆写isMultipart方法, 仅部分 url 的上传请求走我们自定义的 MultipartResolver 处理器,保证新老逻辑兼容。

实现自定义MultipartResolver,重写 CommonsMultipartResolver 的 isMultipart 方法:

@Configuration
public class MyMultipartResolver extends CommonsMultipartResolver {

    /**
     * 这里是处理Multipart http的方法。如果这个返回值为true,那么Multipart http body就会MyMultipartResolver 消耗掉.如果这里返回false
     * 那么就会交给后面的自己写的处理函数处理例如刚才ServletFileUpload 所在的函数
     * @see org.springframework.web.multipart.commons.CommonsMultipartResolver#isMultipart(javax.servlet.http.HttpServletRequest)
     */
    @Override
    public boolean isMultipart(HttpServletRequest request) {
        // 这里通过url判断走哪里,兼容MultipartResolver 或者 ServletFileUpload
        if (request.getRequestURI().contains("mgt/document/upload")||request.getRequestURI().contains("/modules/document.html")) {
            return false;
        }
        return super.isMultipart(request);
    }
}

参考资料