springmvc文件上传,upload.parseRequest(request)获取文件为空解决办法:不删除CommonsMultipartResolver

目录

问题背景及解决方法

原理思考

方法追溯

代码解析

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 items = upload.parseRequest(request);的长度一直为0。后面想着是不是springmvc.xml中配置的CommonsMultipartResolver拦截了请求,然后删除了相关配置,items中就有值了,问题解决,确实是CommonsMultipartResolver对request进行了拦截的操作

原理思考

我并没有研究上面为什么第一种上传MultipartFile  file能获取文件,而第二种不能,估计应该是不同框架的提交方式不同吧(可能性不大),也可能与springmvc的DispatcherServlet对请求的拦截处理有关,如果后面有时间再研究一下吧。

我思考的是为什么删除了CommonsMultipartResolver之后upload.parseRequest(request);就有值了。这里我从DispatcherServlet中简单的追溯了一下对文件的拦截解析过程:(所有的请求首先都要经过DispatcherServlet)

方法追溯

DispatcherServlet的父类FrameworkServlet中有doGet()和doPost()方法,都是调用的自己的processRequest方法:

springmvc文件上传,upload.parseRequest(request)获取文件为空解决办法:不删除CommonsMultipartResolver_第1张图片

而processRequest方法中最终调用的doService方法

springmvc文件上传,upload.parseRequest(request)获取文件为空解决办法:不删除CommonsMultipartResolver_第2张图片

而doService中又调用的是doDispatch方法

springmvc文件上传,upload.parseRequest(request)获取文件为空解决办法:不删除CommonsMultipartResolver_第3张图片

而doDispatch方法中调用了checkMultipart方法

springmvc文件上传,upload.parseRequest(request)获取文件为空解决办法:不删除CommonsMultipartResolver_第4张图片

checkMultipart方法中调用了multipartResolver.resolveMultipart

springmvc文件上传,upload.parseRequest(request)获取文件为空解决办法:不删除CommonsMultipartResolver_第5张图片

这个multipartResolver就是MultipartResolver类,是个接口,有个实现类就是CommonsMultipartResolver,所以上面就是调用的CommonsMultipartResolver的resolveMultipart(request)方法,那就看看下面这个方法的具体实现。

代码解析

resolveMultipart

可以看到方法最终返回了一个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

再来看看parseRequest()方法。这里真相了,try中的List fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);就是用的Apache的commons-fileupload开源框架。再思考之前代码的封装,其实不难发现,CommonsMultipartResolver就是帮你提前解析了request,然后把结果封装到了自己的DefaultMultipartHttpServletRequest类中,再传到你的控制层。而DefaultMultipartHttpServletRequest的内部属性方法肯定与原来的HttpServletRequest不同,而commons-fileupload又是对原始HttpServletRequest的解析,所以你控制层里面通过commons-fileupload去解析一个封装过的DefaultMultipartHttpServletRequest,肯定解析不到。

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

上面提到两者结构差不多,这里参考MultipartParsingResult就可以了。显然,DefaultMultipartHttpServletRequest提供了获取文件信息、请求参数更为简单的方法。直接调用getMultipartFiles方法(在DefaultMultipartHttpServletRequest中是调用getMultiFileMap方法)就可以获取文件数据,返回一个MultiValueMap所以显然还有另一种获取文件的方法,而且不用去掉CommonsMultipartResolver拦截器设置。

	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;
		}
	}

保留CommonsMultipartResolver配置获取文件

@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是不是表单字段,感觉还是要麻烦一些的。

喜欢的朋友麻烦点个赞!

你可能感兴趣的:(前端)