最近会整理记录这一段时间组内同事以及个人遇到的一些问题。
1.背景
问题:Spring上传组件的冲突 背景:同事在原有的上传模块中增加了一个新的文件接口,导致就的上传接口不可用的故障。
新增的配置就是Spring自带的上传解析器:
1 2 3 |
|
示例代码如下:
1 2 3 |
|
原始的上传使用的是 commons-fileupload 组件,简单示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
2.Spring上传机制分析
首先我们从Spring处理请求的源头开始分析,看看Spring是如何支持文件上传的。
1.DispatcherServlet中doDispatch方法中,如果请求是文件上传,会首先将 HttpServletRequest 请求做预处理,转换为 MultipartHttpServletRequest
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
这块两个问题:
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
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 处理器,保证新老逻辑兼容;