一、SpringMVC文件上传源码分析前言(这部分我觉得原作者写的很好)
该如何研究SpringMVC的文件上传的源码呢?
研究源码并不是仅仅知道程序是怎样运行的,而应该从宏观的角度、不同的立场去看待问题。以SpringMVC文件上传的源码为例(麻雀虽小,五脏俱全),我们应该从下面几个方面去分析和研究:
- 文件上传的基本规则:以什么样的格式来传输数据?
- get or post ?
- 与其他字段如何共存 ?
- 多文件上传?
- 站在apache fileupload的角度
- apache fileupload目的是想写一个通用的解析文件上传的jar包,可以供所有的java web框架来方便使用
- 它对外应该提供哪些API来方便外界使用?它又需要外界的哪些参数?
- 哪些内容应该是它做的?
- 哪些内容不应该由它来做?
- 站在SpringMVC框架的角度
- 它不再重复造轮子,使用其他一些jar包即可
- 定义自己的方便用户使用的接口,如MultipartFile、MultipartResolver,来屏蔽掉底层所使用的jar包
- 需要将底层jar包和自己的接口结合起来
- 如果我们都以上述方式要求自己,则源码分析才更有价值和意义,不然我们永远就只能处在类与类跳转的迷雾中
二、apache fileupload源码分析
- 文件上传格式
先来看下含有文件上传时的表单提交是怎样的格式
<form action="/upload/request" method="POST" enctype="multipart/form-data" id="requestForm">
<input type="file" name="myFile">
<input type="text" name="user">
<input type="text" name="password">
<input type="submit" value="提交">
</form>
form表单提交内容如下
从上面可以看到,含有文件上传的格式是这样组织的。
- 文件类型字段
------WebKitFormBoundaryCvop2jTxU5F6lj6G(分隔符)
Content-Disposition: form-data; name="myFile"; filename="资产型号规格模板1.xlsx"
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
(换行)
(文件内容)
- 其他类型字段
------WebKitFormBoundaryCvop2jTxU5F6lj6G(分隔符)
Content-Disposition: form-data; name="user"
(换行)
lg
结束
------WebKitFormBoundaryCvop2jTxU5F6lj6G--(分隔符加上--)
对于上面的文件内容,chrome浏览器是不显示的,换成firefox可以看到,如下图所示
同时我们还可以注意到,不同的浏览器,分隔符是不一样的,在请求头
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryCvop2jTxU5F6lj6G
中指明了分隔符的内容。
- 文件上传注意点:
- 一定是post提交,如果换成get提交,则浏览器默认仅仅把文件名作为属性值来上传,不会上传文件内容,如下
- form表单中一定不要忘了添加
enctype="multipart/form-data"
否则的话,浏览器则不是按照上述的格式来来传递数据的。
上述两点才能保证浏览器正常的进行文件上传。
- apache fileupload的解析
有了上述文件上传的组织格式,我们就需要合理的设计后台的解析方式,下面来看下apache fileupload的使用。先来看下整体的流程图 apache fileUpload整体流程图
- Servlets and Portlets
apache fileupload分Servlets and Portlets
两种情形来处理。Servlet我们很熟悉,而Portlets我也没用过,可自行去搜索。
- 判断request是否是Multipart
对于HttpServletRequest来说,另一个不再说明,自行查看源码,判断规则如下:
- 是否是post请求
- contentType是否以multipart/开头
见源码:
public static final boolean isMultipartContent(
HttpServletRequest request) {
if (!POST_METHOD.equalsIgnoreCase(request.getMethod())) {
return false;
}
return FileUploadBase.isMultipartContent(new ServletRequestContext(request));
}
public static final boolean isMultipartContent(RequestContext ctx) {
String contentType = ctx.getContentType();
if (contentType == null) {
return false;
}
if (contentType.toLowerCase(Locale.ENGLISH).startsWith(MULTIPART)) {
return true;
}
return false;
}
- 对request封装成RequestContext
servlet的输入参数为HttpServletRequest,Portlets的输入参数为ActionRequest,数据来源不同,为了统一方便后面的数据处理,引入了RequestContext接口,来统一一下目标数据的获取。
接口RequestContext的实现类:
- ServletRequestContext
- PortletRequestContext
此时RequestContext就作为了数据源,不再与HttpServletRequest和ActionRequest打交道。
上述的实现过程是由FileUpload的子类ServletFileUpload和PortletFileUpload分别完成包装的。
父类FileUpload的子类:
- ServletFileUpload
- PortletFileUpload
源码展示如下: - ServletFileUpload类
public List<FileItem> parseRequest(HttpServletRequest request)
throws FileUploadException {
return parseRequest(new ServletRequestContext(request));
}```
- PortletFileUpload类
```public List<FileItem> parseRequest(ActionRequest request)
throws FileUploadException {
return parseRequest(new PortletRequestContext(request));
}
上述的parseRequest便完成了整个request的解析过程,内容如下:
public List<FileItem> parseRequest(RequestContext ctx)
throws FileUploadException {
List<FileItem> items = new ArrayList<FileItem>();
boolean successful = false;
try {
FileItemIterator iter = getItemIterator(ctx);
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 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
}
}
}
}
}
分以下两个大步骤:
- 根据RequestContext数据源得到解析后的数据集合 FileItemIterator
- 遍历FileItemIterator中的每个item,类型为FileItemStreamImpl,使用FileItemFactory工厂类来将每个FileItemStreamImpl转化成最终的FileItem
- 由RequestContext数据源得到解析后的数据集合 FileItemIterator
- FileItemIterator内容如下:
public interface FileItemIterator {
boolean hasNext() throws FileUploadException, IOException;
FileItemStream next() throws FileUploadException, IOException;
}
这就是一个轮询器,可以假想成FileItemStream的集合,实际上不是,后面会进行介绍
- FileItemStream则是之前上传文件格式内容
------WebKitFormBoundary77tsMdWQBKrQOSsV
Content-Disposition: form-data; name="user"
lg
或者
------WebKitFormBoundary77tsMdWQBKrQOSsV
Content-Disposition: form-data; name="myFile"; filename="萌芽.jpg"
Content-Type: image/jpeg
(文件内容)
的封装,代码如下
public interface FileItemStream extends FileItemHeadersSupport {
/*流中包含了数值或者文件的内容*/
InputStream openStream() throws IOException;
String getContentType();
/*用来存放文件名,不是文件字段则为null*/
String getName();
/*对应input标签中的name属性*/
String getFieldName();
/*标识该字段是否是一般的form字段还是文件字段*/
boolean isFormField();
}
然后我们来具体看下由RequestContext如何解析成一个FileItemIterator的:
public FileItemIterator getItemIterator(RequestContext ctx)
throws FileUploadException, IOException {
try {
return new FileItemIteratorImpl(ctx);
} catch (FileUploadIOException e) {
// unwrap encapsulated SizeException
throw (FileUploadException) e.getCause();
}
}
new了一个FileItemIteratorImpl,来看下具体的过程:
FileItemIteratorImpl(RequestContext ctx)
throws FileUploadException, IOException {
if (ctx == null) {
throw new NullPointerException("ctx parameter");
}
String contentType = ctx.getContentType();
if ((null == contentType)
|| (!contentType.toLowerCase(Locale.ENGLISH).startsWith(MULTIPART))) {
throw new InvalidContentTypeException(
format("the request doesn't contain a %s or %s stream, content type header is %s",
MULTIPART_FORM_DATA, MULTIPART_MIXED, contentType));
}
InputStream input = ctx.getInputStream();
@SuppressWarnings("deprecation") // still has to be backward compatible
final int contentLengthInt = ctx.getContentLength();
final long requestSize = UploadContext.class.isAssignableFrom(ctx.getClass())
// Inline conditional is OK here CHECKSTYLE:OFF
? ((UploadContext) ctx).contentLength()
: contentLengthInt;
// CHECKSTYLE:ON
if (sizeMax >= 0) {
if (requestSize != -1 && requestSize > sizeMax) {
throw new SizeLimitExceededException(
format("the request was rejected because its size (%s) exceeds the configured maximum (%s)",
Long.valueOf(requestSize), Long.valueOf(sizeMax)),
requestSize, sizeMax);
}
input = new LimitedInputStream(input, sizeMax) {
@Override
protected void raiseError(long pSizeMax, long pCount)
throws IOException {
FileUploadException ex = new SizeLimitExceededException(
format("the request was rejected because its size (%s) exceeds the configured maximum (%s)",
Long.valueOf(pCount), Long.valueOf(pSizeMax)),
pCount, pSizeMax);
throw new FileUploadIOException(ex);
}
};
}
String charEncoding = headerEncoding;
if (charEncoding == null) {
charEncoding = ctx.getCharacterEncoding();
}
boundary = getBoundary(contentType);
if (boundary == null) {
throw new FileUploadException("the request was rejected because no multipart boundary was found");
}
notifier = new MultipartStream.ProgressNotifier(listener, requestSize);
try {
multi = new MultipartStream(input, boundary, notifier);
} catch (IllegalArgumentException iae) {
throw new InvalidContentTypeException(
format("The boundary specified in the %s header is too long", CONTENT_TYPE), iae);
}
multi.setHeaderEncoding(charEncoding);
skipPreamble = true;
findNextItem();
}
- 要点:
- contentType进行判断,是否以multipart开头
- 判断整个请求流的数据大小是否超过sizeMax最大设置
- 获取重要的分隔符boundary信息
- 封装了request请求流的数据,包装为MultipartStream类型
- 也可以设置通知器,来通知流的读取进度
这里可以看到FileItemIteratorImpl并不是FileItemStreamImpl的集合
,其实是FileItemIteratorImpl内部包含了一个FileItemStreamImpl属性。FileItemIteratorImpl的一些重要属性和方法如下:
/*总的数据流*/
private final MultipartStream multi;
/*通知器*/
private final MultipartStream.ProgressNotifier notifier;
/*分隔符*/
private final byte[] boundary;
/*当前已解析到的FileItemStreamImpl对象*/
private FileItemStreamImpl currentItem;
public boolean hasNext() throws FileUploadException, IOException {
if (eof) {
return false;
}
if (itemValid) {
return true;
}
try {
return findNextItem();
} catch (FileUploadIOException e) {
// unwrap encapsulated SizeException
throw (FileUploadException) e.getCause();
}
}
public FileItemStream next() throws FileUploadException, IOException {
if (eof || (!itemValid && !hasNext())) {
throw new NoSuchElementException();
}
itemValid = false;
return currentItem;
}
- findNextItem()方法就是创建新的FileItemStreamImpl来替代当前的FileItemStreamImpl,并更新起始位置。
- 每次调用FileItemIteratorImpl的hasNext()方法,会创建一个新的FileItemStreamImpl赋值给FileItemStreamImpl属性
- 每次调用FileItemIteratorImpl的next()方法,就会返回当前FileItemStreamImpl属性的值
- 创建的每个FileItemStreamImpl都会共享FileItemIteratorImpl的MultipartStream总流,仅仅更新了要读取的起始位置
- 遍历FileItemIterator,通过FileItemFactory工厂
将每一个item转化成FileItem对象
其他应用其实就可以遍历FileItemIteratorImpl拿到每一项FileItemStreamImpl的解析数据了。只是这时候数据
- 存储在内存中的
- 每个FileItemStreamImpl都是共享一个总的流,不能被重复读取
我们想把这些文件数据存在临时文件中,就需要使用使用FileItemFactory来进行下转化成FileItem。每个FileItem才是相互独立的,而FileItemStreamImpl则不是,每个FileItem也是对应上传文件格式中的每一项,如下
InputStream getInputStream() throws IOException;
String getContentType();
String getName();
String getFieldName();
boolean isFormField();
FileItemFactory的实现类DiskFileItemFactory即将数据存储在硬盘上,代码如下:
public static final int DEFAULT_SIZE_THRESHOLD = 10240;
/*制定了临时文件的目录*/
private File repository;
/*当数据小于该阈值时存储到内存中,超过时存储到临时文件中*/
private int sizeThreshold = DEFAULT_SIZE_THRESHOLD;
public FileItem createItem(String fieldName, String contentType,
boolean isFormField, String fileName) {
DiskFileItem result = new DiskFileItem(fieldName, contentType,
isFormField, fileName, sizeThreshold, repository);
FileCleaningTracker tracker = getFileCleaningTracker();
if (tracker != null) {
tracker.track(result.getTempFile(), result);
}
return result;
}
我们从上面可以看到,其实FileItemFactory的createItem方法,并没有为FileItem的流赋值。再回顾下上文parseRequest方法的源代码,赋值发生在这里
FileItemIterator iter = getItemIterator(ctx);
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 fileItem = fac.createItem(item.getFieldName(), item.getContentType(),
item.isFormField(), fileName);
items.add(fileItem);
try {
/*这里才是为每一个FileItem的流赋值*/
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);
}
上述FileItem的openStream()方法如下:
public OutputStream getOutputStream()
throws IOException {
if (dfos == null) {
File outputFile = getTempFile();
dfos = new DeferredFileOutputStream(sizeThreshold, outputFile);
}
return dfos;
}
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;
}
getTempFile()会根据FileItemFactory的临时文件目录配置repository,创建一个临时文件,用于上传文件。 这里又用到了commons-io包中的DeferredFileOutputStream类。
- 当数据数量小于sizeThreshold阈值时,存储在内存中
- 当数据数量大于sizeThreshold阈值时,存储到传入的临时文件中
至此,FileItem都被创建出来了,整个过程就结束了。
三、springboot文件上传前言
- 首先文件上传功能,springboot都将其实现封装在了文件上传配置类MultipartAutoConfiguration里面
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({Servlet.class, StandardServletMultipartResolver.class, MultipartConfigElement.class})
@ConditionalOnProperty(
prefix = "spring.servlet.multipart",
name = {"enabled"},
matchIfMissing = true
)
@ConditionalOnWebApplication(
type = Type.SERVLET
)
//能配置的都是以spring.servlet.multipart开头,进入MultipartProperties,可以查看能够配置的有哪些属性
@EnableConfigurationProperties({MultipartProperties.class})
public class MultipartAutoConfiguration {
private final MultipartProperties multipartProperties;
public MultipartAutoConfiguration(MultipartProperties multipartProperties) {
this.multipartProperties = multipartProperties;
}
@Bean
@ConditionalOnMissingBean({MultipartConfigElement.class, CommonsMultipartResolver.class})
public MultipartConfigElement multipartConfigElement() {
return this.multipartProperties.createMultipartConfig();
}
@Bean(
name = {"multipartResolver"}
)
@ConditionalOnMissingBean({MultipartResolver.class})
public StandardServletMultipartResolver multipartResolver() {
StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();
multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily());
return multipartResolver;
}
}
- 相关的配置设置在MultipartProperties中,其中字段就是对应的属性设置,经典字段有:
- enabled:是否开启文件上传自动配置,默认开启。
- location:上传文件的临时目录。
- maxFileSize:最大文件大小,以字节为单位,默认为1M。
- maxRequestSize:整个请求的最大容量,默认为10M。
- fileSizeThreshold:文件大小达到该阈值,将写入临时目录,默认为0,即所有文件都会直接写入磁盘临时文件中。
- resolveLazily:是否惰性处理请求,默认为false。
- SpringMVC文件上传接口设计与实现
整体的包结构 首先看下整体的包的结构,如下图
总共分成3大块,分别如下
- 一、org.springframework.web.multipart
存放Spring定义的文件上传接口以及异常,如
- MultipartException对用户抛出的解析异常(隐藏底层文件上传解析包所抛出的异常)
也就指明了,这个体系下只能抛出这种类型的异常,MaxUploadSizeExceededException是MultipartException它的子类,专门用于指定文件大小限制的异常。用户不应该看到底层文件上传解析包所抛出的异常,底层采用的文件上传解析包在解析文件上传时也会定义自己的解析异常,这时候就需要在整合这些jar包时,需要对解析包所抛出的异常进行转换成上述已统一定义的面向用户的异常
源码见证下:
protected MultipartParsingResult parseRequest(HttpServletRequest request) throws MultipartException {
String encoding = determineEncoding(request);
FileUpload fileUpload = prepareFileUpload(encoding);
try {
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);
}
}
FileUploadBase.SizeLimitExceededException、FileUploadException 都是底层解析包apache fileupload解析时抛出的异常
,在这里要进行try catch 处理,然后将这些异常转化成SpringMVC自定义的异常MaxUploadSizeExceededException、MultipartException
- MultipartFile 定义了文件解析的统一结果类型
- MultipartResolver 定义了文件解析的处理器,不同的处理器不同的解析方式
- 二、org.springframework.web.multipart.commons
用于整合apache fileupload的解析,对上述定义的接口进行实现,如
- CommonsMultipartFile实现上述MultipartFile接口,即采用apache fileupload解析的结果为CommonsMultipartFile
- CommonsMultipartResolver实现上述MultipartResolver,
- 三、org.springframework.web.multipart.support
用于整合j2ee自带的文件上传的解析,对上述定义的接口进行实现,如
- StandardMultipartFile实现上述MultipartFile接口,即采用这种方式解析的结果为StandardMultipartFile
- StandardServletMultipartResolver实现上述MultipartResolver
接下来详细看看这些源码内容
- SpringMVC自己的接口设计
- 一、MultipartResolver接口的内容:
public interface MultipartResolver {
//判断当前的HttpServletRequest是否是文件上传类型
boolean isMultipart(HttpServletRequest request);
//将HttpServletRequest转化成MultipartHttpServletRequest
MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;
//清除产生的临时文件等
void cleanupMultipart(MultipartHttpServletRequest request);
}
- 二、MultipartHttpServletRequest接口内容:
MultipartHttpServletRequest 继承了 HttpServletRequest 和 MultipartRequest,
然后就具有了下面的两个主要功能
获取文件上传的每一部分的请求头信息
HttpHeaders getRequestHeaders();
HttpHeaders getMultipartHeaders(String paramOrFileName);
这里的请求头信息就是如下内容中的 Content-Disposition: form-data; name=“myFile”; filename=“资产型号规格模板1.xlsx” Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet 等信息
获取文件上传的文件内容(每个文件信息都是MultipartFile类型)
Iterator<String> getFileNames();
MultipartFile getFile(String name);
List<MultipartFile> getFiles(String name);
Map<String, MultipartFile> getFileMap();
四、文件上传的核心流程
在SpringMVC的入口类DispatcherServlet中的doDispatch方法中,可以看到是如下的处理流程
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
boolean multipartRequestParsed = false;
try {
//略
//步骤一
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
//略
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
finally {
//略
// Clean up any resources used by a multipart request.
//步骤二
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
可以看到这里主要有两个步骤
- 步骤一 检查是否是文件上传类型,如果是则进行解析,然后将HttpServletRequest request封装成MultipartHttpServletRequest
- 步骤二 如果是文件上传,则进行资源清理,如删除上传的临时文件等
下面分别来说
- 判断并解析HttpServletRequest成MultipartHttpServletRequest:
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
if (request instanceof MultipartHttpServletRequest) {
logger.debug("Request is already a MultipartHttpServletRequest - if not in a forward, " +
"this typically results from an additional MultipartFilter in web.xml");
}
else if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) instanceof MultipartException) {
logger.debug("Multipart resolution failed for current request before - " +
"skipping re-resolution for undisturbed error rendering");
}
else {
return this.multipartResolver.resolveMultipart(request);
}
}
// If not returned before: return original request.
return request;
}
- 首先看看DispatcherServlet的multipartResolver属性是否有值,这个在初始化注入九大组件的时候就已经初始化好了:
protected void onRefresh(ApplicationContext context) {
this.initStrategies(context);
}
protected void initStrategies(ApplicationContext context) {
this.initMultipartResolver(context);
this.initLocaleResolver(context);
this.initThemeResolver(context);
this.initHandlerMappings(context);
this.initHandlerAdapters(context);
this.initHandlerExceptionResolvers(context);
this.initRequestToViewNameTranslator(context);
this.initViewResolvers(context);
this.initFlashMapManager(context);
}
当multipartResolver属性有值的时候,先调用它的boolean isMultipart(HttpServletRequest request)方法,判断当前的request是否是符合文件上传类型
,如果符合则调用它的MultipartHttpServletRequest resolveMultipart(HttpServletRequest request)方法将当前的request进行解析并且封装成MultipartHttpServletRequest类型。
有了MultipartHttpServletRequest,我们就能获取上传的文件信息了。
然后我们就可以通过2种途径来获取上传的文件。
- 途径1 直接使用MultipartHttpServletRequest request作为参数,如下
@RequestMapping(value="/test/file",method=RequestMethod.POST)
@ResponseBody
public String fileUpload(MultipartHttpServletRequest request){
Map<String, MultipartFile> files=request.getFileMap();
//使用files
return "success";
}
- 途径2 使用@RequestParam(“myFile”) 来获取文件(RequestParam里面的"myFile"是input标签的name的值而不是文件名),如下
@RequestMapping(value="/test/file",method=RequestMethod.POST)
@ResponseBody
public String fileUpload(@RequestParam("myFile") MultipartFile file){
//使用file
return "success";
}
对于途径1很好理解,对于途径2,为什么呢?
这里简单提下,对于@RequestParam注解是由RequestParamMethodArgumentResolver来进行处理的,是它进行了特殊处理,当@RequestParam修饰的类型为MultipartFile或者javax.servlet.http.Part(后面再详细说此Part)时进行特殊处理
,如下
@Override
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest webRequest)
throws Exception {
Object arg;
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
MultipartHttpServletRequest multipartRequest =
WebUtils.getNativeRequest(servletRequest, MultipartHttpServletRequest.class);
if (MultipartFile.class.equals(parameter.getParameterType())) {
assertIsMultipartRequest(servletRequest);
Assert.notNull(multipartRequest, "Expected MultipartHttpServletRequest:
is a MultipartResolver configured?");
arg = multipartRequest.getFile(name);
}
else if (isMultipartFileCollection(parameter)) {
assertIsMultipartRequest(servletRequest);
Assert.notNull(multipartRequest, "Expected MultipartHttpServletRequest:
is a MultipartResolver configured?");
arg = multipartRequest.getFiles(name);
}
else if(isMultipartFileArray(parameter)) {
assertIsMultipartRequest(servletRequest);
Assert.notNull(multipartRequest, "Expected MultipartHttpServletRequest:
is a MultipartResolver configured?");
arg = multipartRequest.getFiles(name).toArray(new MultipartFile[0]);
}
else if ("javax.servlet.http.Part".equals(parameter.getParameterType().getName())) {
assertIsMultipartRequest(servletRequest);
arg = servletRequest.getPart(name);
}
else if (isPartCollection(parameter)) {
assertIsMultipartRequest(servletRequest);
arg = new ArrayList<Object>(servletRequest.getParts());
}
else if (isPartArray(parameter)) {
assertIsMultipartRequest(servletRequest);
arg = RequestPartResolver.resolvePart(servletRequest);
}
else {
arg = null;
if (multipartRequest != null) {
List<MultipartFile> files = multipartRequest.getFiles(name);
if (!files.isEmpty()) {
arg = (files.size() == 1 ? files.get(0) : files);
}
}
if (arg == null) {
String[] paramValues = webRequest.getParameterValues(name);
if (paramValues != null) {
arg = paramValues.length == 1 ? paramValues[0] : paramValues;
}
}
}
return arg;
}
我们这里可以看到,其实也是通过MultipartHttpServletRequest的getFile等方法来获取的,同时支持数组、集合形式的参数
- 清理占用的资源,如临时文件
protected void cleanupMultipart(HttpServletRequest servletRequest) {
MultipartHttpServletRequest req = WebUtils.getNativeRequest(
servletRequest, MultipartHttpServletRequest.class);
if (req != null) {
this.multipartResolver.cleanupMultipart(req);
}
}
这里其实就是调用MultipartResolver接口的void cleanupMultipart(MultipartHttpServletRequest request)方法
至此SpringMVC已经完成了自己的文件上传框架体系,即底层不管采用何种文件解析包都是走这样的一个流程。这样的一个流程其实就是对实际业务的抽象过程。
我们在写代码的时候,经常就缺少抽象的能力,即很少抽象出各种业务逻辑的共同点。
五、整合apache fileupload对文件上传的解析
刚才说了整个文件上传的处理流程,然后我们就来看下apache fileupload是如何整合进来的。即CommonsMultipartResolver是如何实现的
- 判断一个request是否是multipart形式的
@Override
public boolean isMultipart(HttpServletRequest request) {
return (request != null && ServletFileUpload.isMultipartContent(request));
}
这里就是使用apache fileupload自己的ServletFileUpload.isMultipartContent判断方法。
这里我们可以再多想一下,功能的职责划分问题(虽然问题很简单,主要是想引导大家在写代码的时候多去思考)。
因为目前判断一个request是否是multipart形式,都是一样的,不管你是哪种解析包,为什么SpringMVC不统一进行判断,而是采用解析包的判断?
如果SpringMVC自己进行统一的判断,似乎也没什么问题。站在apache fileupload的角度来说,判断request是否是multipart形式 的确应该是它的一个功能,而不是等待外界来判断。
SpringMVC既然采用第三方的解析包,就要遵守人家解析包的判断逻辑,而不是自行判断,虽然他们目前的判断逻辑是一样的。万一后来又出来一个解析包,判断逻辑不一样呢?如果流程体系还是采用SpringMVC自己的判断,可能就没法正常解析了
- 将HttpServletRequest解析成DefaultMultipartHttpServletRequest
一旦上述判断通过了,则就需要执行解析过程(可以立即解析,也可以延迟解析),看下具体的解析过程
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 {
MultipartParsingResult parsingResult = parseRequest(request);
return new DefaultMultipartHttpServletRequest(request, parsingResult.getMultipartFiles(),
parsingResult.getMultipartParameters(), parsingResult.getMultipartParameterContentTypes());
}
}
这里大致说下过程,详细的内容去看源代码。
使用apache fileupload的ServletFileUpload对request进行解析,解析结果为List,代码如下:
List<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);
FileItem为apache fileupload自己的解析结果,需要转化为SpringMVC自己定义的MultipartFile
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) {
if (fileItem.isFormField()) {
String value;
String partEncoding = determineEncoding(fileItem.getContentType(), encoding);
if (partEncoding != null) {
try {
value = fileItem.getString(partEncoding);
}
catch (UnsupportedEncodingException ex) {
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());
}
else {
// multipart file field
CommonsMultipartFile file = new CommonsMultipartFile(fileItem);
multipartFiles.add(file.getName(), file);
}
}
return new MultipartParsingResult(multipartFiles, multipartParameters,
multipartParameterContentTypes);
}
这里有普通字段的处理和文件字段的处理。还记得上文讲的org.springframework.web.multipart.commons包的CommonsMultipartFile吗?可以看到通过new CommonsMultipartFile(fileItem),就将FileItem结果转化为了MultipartFile结果。至此就将HttpServletRequest解析成了DefaultMultipartHttpServletRequest,所以我们在使用request时,它的类型其实就是DefaultMultipartHttpServletRequest类型,
我们可以通过它来获取各种上传的文件信息。
- 清理临时文件
其实就是对所有的CommonsMultipartFile中的FileItem进行删除临时文件的操作,这个删除操作是apache fileupload自己定义的,如下
protected void cleanupFileItems(MultiValueMap<String, MultipartFile> multipartFiles) {
for (List<MultipartFile> files : multipartFiles.values()) {
for (MultipartFile file : files) {
if (file instanceof CommonsMultipartFile) {
CommonsMultipartFile cmf = (CommonsMultipartFile) file;
cmf.getFileItem().delete();
}
}
}
}
至此,SpringMVC与apache fileupload的整合完成了,其他的整合也是类似的操作。
六、流程简单总结
- 文件上传自动配置类-MultipartAutoConfiguration-MultipartProperties
• 自动配置好了 StandardServletMultipartResolver 【文件上传解析器】
• 原理步骤
- 1、请求进来使用文件上传解析器判断(isMultipart)并封装(resolveMultipart,返回MultipartHttpServletRequest)文件上传请求
- 2、参数解析器来解析请求中的文件内容封装成MultipartFile
- 3、将request中文件信息封装为一个Map;MultiValueMap<String, MultipartFile>
FileCopyUtils。实现文件流的拷贝