Spring 中 MultipartResolver 和 ServletFileUpload 上传组件冲突的问题
问题
之前项目上传文件都是使用的 Apache 的 ServletFileUpload 组件,同事开启了 Multipart 方式上传后增加了一个新的上传文件接口,导致之前的上传接口不可用。
同事新增了 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);
}
}