目录
问题背景及解决方法
原理思考
方法追溯
代码解析
resolveMultipart
parseRequest
MultipartParsingResult和DefaultMultipartHttpServletRequest
保留CommonsMultipartResolver配置获取文件
结语
最近项目中有两个地方用到了文件上传的功能,前一个地方使用的是WebUploader开源框架来上传的,后一个地方是使用的Pdf.js开源框架保存正文的实现文件上传,项目的Controller层使用的是springmvc,并且在springmvc.xml中配置了文件上传解析器:
下面是第一个文件上传的Controller层方法,MultipartFile file能够接收到文件:
@RequestMapping(value = "/firstFileUpload")
@ResponseBody
public String firstFileUpload(HttpServletRequest request,MultipartFile file){
doSomething();
}
但是第二个文件上传的controller层方法中MultipartFile file却获取不到文件,调试了很久,最终放弃,只能用其它方法来获取文件。在网上搜索,基本上都说是要用Apache的commons-fileupload框架,使用方法如下:
一、在pom.xml中添加jar包依赖:
commons-io commons-io 2.5 commons-fileupload commons-fileupload 1.3.1 二、controller层中的代码:
@RequestMapping(value = "/fileUpload") @ResponseBody public void fileUpload(HttpServletRequest request) { //创建文件解析器 DiskFileItemFactory factory = new DiskFileItemFactory(); ServletFileUpload upload = new ServletFileUpload(factory); try { List
items = upload.parseRequest(request); //解析request请求 //遍历items,对其中的非form字段(就是文件了)进行操作(上传) for (FileItem item : items){ if (!item.isFormField()){ try { uploadFile(item.getName(),item.get()); //文件上传 } catch (IOException e) { e.printStackTrace(); } } } } catch (FileUploadException e) { e.printStackTrace(); } } 三、FileItem常用方法介绍:
1. boolean isFormField()
isFormField方法用于判断FileItem类对象封装的数据是一个普通文本表单字段,还是一个文件表单字段,如果是普通表单字段则返回true,否则返回false。因此,可以使用该方法判断是否为普通表单域,还是文件上传表单域。
2. String getName()
getName方法用于获得文件上传字段中的文件名。注意IE或FireFox中获取的文件名是不一样的,IE中是绝对路径,FireFox中只是文件名。
3. String getFieldName()
getFieldName方法用于返回表单标签name属性的值。如上例中的value。4. void write(File file)
write方法用于将FileItem对象中保存的主体内容保存到某个指定的文件中。如果FileItem对象中的主体内容是保存在某个临时文件中,该方法顺利完成后,临时文件有可能会被清除。该方法也可将普通表单字段内容写入到一个文件中,但它主要用途是将上传的文件内容保存在本地文件系统中。
5. byte[] get();
将文件内容以byte[]的形式返回。
6. String getContentType()
getContentType 方法用于获得上传文件的类型,即表单字段元素描述头属性“Content-Type”的值,如“image/jpeg”。如果FileItem类对象对应的是普通表单字段,该方法将返回null。7. InputStream getInputStream()
以流的形式返回上传文件的数据内容。8. long getSize()
返回该上传文件的大小(以字节为单位)。
在修改了代码之后,发现List
我并没有研究上面为什么第一种上传MultipartFile file能获取文件,而第二种不能,估计应该是不同框架的提交方式不同吧(可能性不大),也可能与springmvc的DispatcherServlet对请求的拦截处理有关,如果后面有时间再研究一下吧。
我思考的是为什么删除了CommonsMultipartResolver之后upload.parseRequest(request);就有值了。这里我从DispatcherServlet中简单的追溯了一下对文件的拦截解析过程:(所有的请求首先都要经过DispatcherServlet)
DispatcherServlet的父类FrameworkServlet中有doGet()和doPost()方法,都是调用的自己的processRequest方法:
而processRequest方法中最终调用的doService方法
而doService中又调用的是doDispatch方法
而doDispatch方法中调用了checkMultipart方法
checkMultipart方法中调用了multipartResolver.resolveMultipart
这个multipartResolver就是MultipartResolver类,是个接口,有个实现类就是CommonsMultipartResolver,所以上面就是调用的CommonsMultipartResolver的resolveMultipart(request)方法,那就看看下面这个方法的具体实现。
可以看到方法最终返回了一个DefaultMultipartHttpServletRequest对象,这个类就是对HttpServletRequest进行了拦截解析后的包装类,解析完了就把HttpServletRequest中的所有信息放到了DefaultMultipartHttpServletRequest中,同时提供了自己的访问方法。最终你在controller层中用HttpServletRequest request接收到的就是这个DefaultMultipartHttpServletRequest。
其中if中的代码是实现懒加载的情况,就是不立即解析request,把解析动作放到了initializeMultipart()这个方法中,程序员可以在需要解析的时候调用这个方法;而else中的代码就是立即对request进行解析。
@Override
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());
}
}
再来看看parseRequest()方法。这里真相了,try中的List
try里面还有一行return parseFileItems(fileItems, encoding)。就是把文件放到一个MultipartParsingResult中,MultipartParsingResult和DefaultMultipartHttpServletRequest存储文件的字段相同。
protected MultipartParsingResult parseRequest(HttpServletRequest request) throws MultipartException {
String encoding = determineEncoding(request);
FileUpload fileUpload = prepareFileUpload(encoding);
try {
List fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);
return parseFileItems(fileItems, encoding);
}
catch (FileUploadBase.SizeLimitExceededException ex) {
throw new MaxUploadSizeExceededException(fileUpload.getSizeMax(), ex);
}
catch (FileUploadBase.FileSizeLimitExceededException ex) {
throw new MaxUploadSizeExceededException(fileUpload.getFileSizeMax(), ex);
}
catch (FileUploadException ex) {
throw new MultipartException("Failed to parse multipart servlet request", ex);
}
}
上面提到两者结构差不多,这里参考MultipartParsingResult就可以了。显然,DefaultMultipartHttpServletRequest提供了获取文件信息、请求参数更为简单的方法。直接调用getMultipartFiles方法(在DefaultMultipartHttpServletRequest中是调用getMultiFileMap方法)就可以获取文件数据,返回一个MultiValueMap
protected static class MultipartParsingResult {
private final MultiValueMap multipartFiles;
private final Map multipartParameters;
private final Map multipartParameterContentTypes;
public MultipartParsingResult(MultiValueMap mpFiles,
Map mpParams, Map mpParamContentTypes) {
this.multipartFiles = mpFiles;
this.multipartParameters = mpParams;
this.multipartParameterContentTypes = mpParamContentTypes;
}
public MultiValueMap getMultipartFiles() {
return this.multipartFiles;
}
public Map getMultipartParameters() {
return this.multipartParameters;
}
public Map getMultipartParameterContentTypes() {
return this.multipartParameterContentTypes;
}
}
@RequestMapping(value = "/fileUpload")
@ResponseBody
public void fileUpload(HttpServletRequest request) {
//将request强转,因为拦截过后request的实际类型是DefaultMultipartHttpServletRequest
DefaultMultipartHttpServletRequest fileRequest = (DefaultMultipartHttpServletRequest)request;
//获取文件Map
MultiValueMap fileMap = fileRequest.getMultiFileMap();
//遍历所有的文件,每个List代表一个文件,get(0)就可以了
for(Map.Entry> entry : fileMap.entrySet()){
List value = entry.getValue();
MultipartFile multipartFile = value.get(0);
try {
service.fileUpload(multipartFile.getBytes()); //将文件二进制上传到磁盘或数据库
} catch (IOException e) {
e.printStackTrace();
}
}
就我个人而言,觉得还是保留springmvc的CommonsMultipartResolver解析器配置,这样在某些情况你就可以直接在controller层通过MultipartFile来接受文件,可以利用springmvc写好的方法。如果你还是自己通过commons-fileupload来解析request获取文件,那么所有文件上传的操作都要使用它,同时你还要判断哪些FileItem是不是表单字段,感觉还是要麻烦一些的。
喜欢的朋友麻烦点个赞!