前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >SpringMVC源码分析:POST请求中的文件处理

SpringMVC源码分析:POST请求中的文件处理

作者头像
程序员欣宸
发布于 2020-02-13 01:52:49
发布于 2020-02-13 01:52:49
1.5K00
代码可运行
举报
文章被收录于专栏:实战docker实战docker
运行总次数:0
代码可运行

本章我们来一起阅读和分析SpringMVC的部分源码,看看收到POST请求中的二进制文件后,SpingMVC框架是如何处理的;

使用了SpringMVC框架的web应用中,接收上传文件时,一般分以下三步完成:

  1. 在spring配置文件中配置一个bean:
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<bean id="multipartResolver"
      class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <property name="defaultEncoding" value="utf-8" />
    <property name="maxUploadSize" value="10485760000" />
    <property name="maxInMemorySize" value="40960" />
</bean>
  1. pom.xml中添加apache的commons-fileupload库的依赖:
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<dependency>
	<groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.3.1</version>
</dependency>
  1. 开发业务Controller的响应方法,以下代码是将POST的文件存储到应用所在的电脑上:
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@RequestMapping(value="/upload",method= RequestMethod.POST)
    public void upload(HttpServletRequest request,
                       HttpServletResponse response,
                         @RequestParam("comment") String comment,
                         @RequestParam("file") MultipartFile file) throws Exception {


        logger.info("start upload, comment [{}]", comment);


        if(null==file || file.isEmpty()){
            logger.error("file item is empty!");
            responseAndClose(response, "文件数据为空");
            return;
        }


        //上传文件路径
        String savePath = request.getServletContext().getRealPath("/WEB-INF/upload");


        //上传文件名
        String fileName = file.getOriginalFilename();


        logger.info("base save path [{}], original file name [{}]", savePath, fileName);


        //得到文件保存的名称
        fileName = mkFileName(fileName);


        //得到文件保存的路径
        String savePathStr = mkFilePath(savePath, fileName);


        logger.info("real save path [{}], real file name [{}]", savePathStr, fileName);


        File filepath = new File(savePathStr, fileName);


        //确保路径存在
        if(!filepath.getParentFile().exists()){
            logger.info("real save path is not exists, create now");
            filepath.getParentFile().mkdirs();
        }


        String fullSavePath = savePathStr + File.separator + fileName;


        //存本地
        file.transferTo(new File(fullSavePath));


        logger.info("save file success [{}]", fullSavePath);


        responseAndClose(response, "Spring MVC环境下,上传文件成功");
    }

如上所示,方法入参中的MultipartFile就是POST的文件对应的对象,调用file.transferTo方法即可将上传的文件创建到业务所需的位置;

三个疑问

虽然业务代码简单,以上几步即可完成对上传文件的接收和处理,但是有几个疑问想要弄清楚:

  1. 为什么要配置名为multipartResolver的bean;
  2. 为什么要依赖apache的commons-fileupload库;
  3. 从客户端的POST到Controller中的file.transferTo方法调用,具体做了哪些文件相关的操作?

接下来我们就一起来看看SpringMVC的源码,寻找这几个问题的答案;

Spring版本

本文涉及的Spring相关库,例如spring-core、spring-web、spring-webmvc等,都是4.0.2.RELEASE版本;

SpringMVC源码

  1. 先来看下入口类DispatcherServlet的源码,在应用初始化的时候会调用initMultipartResolver方法:
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class);
...

所以,如果配置了名为multipartResolver的bean,就会DispatcherServlet的multipartResolver保存下来;

2. 再来看一下处理POST请求时候的调用链:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
FrameworkServlet.doPost
->
FrameworkServlet.processRequest
->
DispatcherServlet.doService
->
DispatcherServlet.doDispatch
->
DispatcherServlet.checkMultipart
->
multipartResolver.resolveMultipart(request)

因此,应用收到上传文件的请求时,最终会调用multipartResolver.resolveMultipart;

第一个疑问已经解开:SpringMVC框架在处理POST请求时,会使用名为multipartResolver的bean来处理文件;

3. CommonsMultipartResolver.resolveMultipart方法中会调用parseRequest方法,我们看parseRequest方法的源码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
String encoding = this.determineEncoding(request);
FileUpload fileUpload = this.prepareFileUpload(encoding);


try {
	List<FileItem> fileItems = ((ServletFileUpload)fileUpload).parseRequest(request);
    return this.parseFileItems(fileItems, encoding);
} catch (SizeLimitExceededException var5) {
	throw new MaxUploadSizeExceededException(fileUpload.getSizeMax(), var5);
} catch (FileUploadException var6) {
	throw new MultipartException("Could not parse multipart servlet request", var6);
}

从以上代码可以发现,在调用prepareFileUpload方法的时候,相关的fileItemFactory和fileUpload对象都已经是commons-fileupload库中定义的类型了,并且最终还是调用由commons-fileupload库中的ServletFileUpload.parseRequest方法负责解析工作,构建FileItem对象;第二个疑问已经解开:SpringMVC框架在处理POST请求时,本质是调用commons-fileupload库中的API来处理的;

4. 继续关注CommonsMultipartResolver.parseRequest方法,里面调用了ServletFileUpload.parseRequest方法,最终由FileUploadBase.parseRequest方法来处理:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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
                    }
                }
            }
        }
    }

重点关注这一段:Streams.copy(item.openStream(), fileItem.getOutputStream(), true);,这是一次流的拷贝,将提交文件的inputstrem写入到一个outputstream,我们再看看getOutputStream方法的源码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public OutputStream getOutputStream()
        throws IOException {
        if (dfos == null) {
            File outputFile = getTempFile();
            dfos = new DeferredFileOutputStream(sizeThreshold, outputFile);
        }
        return dfos;
    }

原来如此,会准备一个临时文件,上传的文件通过流拷贝写入到临时文件中了;等一下,事情没那么简单!!!上面的代码中并没有直接返回文件对象outputFile,而是创建了一个DeferredFileOutputStream对象,这是个什么东西?另外sizeThreshold这个参数是干啥用的?

为了搞清楚上面两个问题,我们从Streams.copy方法开始看吧:

a. Streams.copy方法的关键代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
for (;;) {
                int res = in.read(buffer);
                if (res == -1) {
                    break;
                }
                if (res > 0) {
                    total += res;
                    if (out != null) {
                        out.write(buffer, 0, res);
                    }
                }
            }

上述代码表明,steam的copy过程中会调用OutputStream的write方法;

b. DeferredFileOutputStream类没有write方法,去看它的父类DeferredFileOutputStream的write方法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public void write(byte b[]) throws IOException
    {
        checkThreshold(b.length);
        getStream().write(b);
        written += b.length;
    }

先调用checkThreshold方法,检查***已写入长度加上即将写入的长度***是否达到threshold值,如果达到就会将thresholdExceeded设置为true,并调用thresholdReached方法;

c. thresholdReached方法源码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
protected void thresholdReached() throws IOException
    {
        if (prefix != null) {
            outputFile = File.createTempFile(prefix, suffix, directory);
        }
        FileOutputStream fos = new FileOutputStream(outputFile);
        memoryOutputStream.writeTo(fos);
        currentOutputStream = fos;
        memoryOutputStream = null;
    }

真相大白:threshold是一个阈值,如果文件比threshold小,就将文件存入内存,如果文件比threshold大就写入到磁盘中去,这显然是个处理文件时的优化手段; 注意这一行代码:currentOutputStream = fos;,原本currentOutputStream是基于内存的ByteArrayOutputStream,如果超过了threshold,就改为基于文件的FileOutputStream对象,后续再执行getStream().write(b)的时候,就不再写入到内存,而是写入到文件了; 5. 我们再回到主线:CommonsMultipartResolver,这里FileItem对象在parseFileItems方法中经过处理,被放入了CommonsMultipartFile对象中,再被放入MultipartParsingResult对象中,最后被放入DefaultMultipartHttpServletRequest对象中,返回到DispatcherServlet.doDispatch方法中,然后传递到业务的controller中处理; 6. 业务Controller的响应方法中,调用了file.transferTo方法将临时文件写入到业务指定的文件中,transferTo方法中有一行关键代码:this.fileItem.write(dest);,我们打开DiskFileItem类,看看这个write方法的源码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public void write(File file) throws Exception {
        if (isInMemory()) {
            FileOutputStream fout = null;
            try {
                fout = new FileOutputStream(file);
                fout.write(get());
            } finally {
                if (fout != null) {
                    fout.close();
                }
            }
        } else {
            File outputFile = getStoreLocation();
            if (outputFile != null) {
                // Save the length of the file
                size = outputFile.length();
                /*
                 * The uploaded file is being stored on disk
                 * in a temporary location so move it to the
                 * desired file.
                 */
                if (!outputFile.renameTo(file)) {
                    BufferedInputStream in = null;
                    BufferedOutputStream out = null;
                    try {
                        in = new BufferedInputStream(
                            new FileInputStream(outputFile));
                        out = new BufferedOutputStream(
                                new FileOutputStream(file));
                        IOUtils.copy(in, out);
                    } finally {
                        if (in != null) {
                            try {
                                in.close();
                            } catch (IOException e) {
                                // ignore
                            }
                        }
                        if (out != null) {
                            try {
                                out.close();
                            } catch (IOException e) {
                                // ignore
                            }
                        }
                    }
                }
            } else {
                /*
                 * For whatever reason we cannot write the
                 * file to disk.
                 */
                throw new FileUploadException(
                    "Cannot write uploaded file to disk!");
            }
        }
    }

如上所示,依然是对DeferredFileOutputStream对象的操作,如果数据在内存中,就写入到指定文件,否则就尝试将临时文件rename为指定文件,如果rename失败,就会读取临时文件的二进制流,再写到指定文件上去;

另外,DiskFileItem中出现的cachedContent对象,其本身也就是DeferredFileOutputStream的内存数据;

至此,第三个疑问也解开了:上传的文件如果小于指定的阈值,就会被保存在内存中,否则就存在磁盘上,留给业务代码用,业务代码在使用时通过CommonsMultipartFile对象来操作;

似乎又有一个疑问了:这些临时文件存在内存或者磁盘上,什么时候清理呢,不清理岂不是越来越多?

在DispatcherServlet.doDispatch方法中,有这么一段:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
finally {
			if (asyncManager.isConcurrentHandlingStarted()) {
				// Instead of postHandle and afterCompletion
				mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
				return;
			}
			// Clean up any resources used by a multipart request.
			if (multipartRequestParsed) {
				cleanupMultipart(processedRequest);
			}
		}

关键代码是cleanupMultipart(processedRequest);,进去跟踪发现会调用CommonsFileUploadSupport.cleanupFileItems方法,最终调用DiskFileItem.delete方法,将临时文件清理掉;

至此SpringMVC源码分析就结束了,接下来列出一些web应用的源码,作为可能用到的参考信息;

demo源码下载

文中提到的demo工程,您可以在GitHub下载,地址和链接信息如下表所示:

名称

链接

备注

项目主页

https://github.com/zq2599/blog_demos

该项目在GitHub上的主页

git仓库地址(https)

https://github.com/zq2599/blog_demos.git

该项目源码的仓库地址,https协议

git仓库地址(ssh)

git@github.com:zq2599/blog_demos.git

该项目源码的仓库地址,ssh协议

  • 这个git项目中有多个目录,本次所需的资源放在springmvcfileserver,如下图红框所示:
  • 如果您想了解如何POST二进制文件到服务端,请下载uploadfileclient这个文件夹下的客户端demo工程,如下图红框所示:
  • 如果您不想让SpringMVC处理上传的文件,而是自己去调用apache的commons-fileupload库来做些更复杂的操作,您可以参考fileserverdemo这个文件夹下的demo工程,如下图红框所示:
  • 如果您的应用是基于springboot的,实现文件服务可以参考springbootfileserver这个文件夹下的demo工程,如下图红框所示:

至此,本次阅读和分析实战已全部完成,在您学习和理解SpringMVC框架的过程中,希望本文能对您有所帮助,如果发现文中有错误,也真诚的期待您能留下意见;

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
什么年代了,你还不知道 Servlet3.0 中的文件上传方式?
松哥原创的 Spring Boot 视频教程已经杀青,感兴趣的小伙伴戳这里-->Spring Boot+Vue+微人事视频教程
江南一点雨
2021/04/22
1.4K0
第7章—SpringMVC高级技术—处理multipart形式的数据
MultipartResolver 用于处理文件上传,当收到请求时 DispatcherServlet 的 checkMultipart() 方法会调用 MultipartResolver 的 isMultipart() 方法判断请求中是否包含文件。如果请求数据中包含文件,则调用 MultipartResolver 的 resolveMultipart() 方法对请求的数据进行解析,然后将文件数据解析成 MultipartFile 并封装在 MultipartHttpServletRequest (继承了 HttpServletRequest) 对象中,最后传递给 Controller,在 MultipartResolver 接口中有如下方法:
Dream城堡
2018/09/10
1.8K0
第7章—SpringMVC高级技术—处理multipart形式的数据
SpringMVC 实现文件上传
springmvc文件上传 SpringMVC框架提供了MultipartFile对象,该对象表示上传的文件,要求变量名称必须和表单file标签的name属性名称相同。 在springmvc.xml配置文件解析器对象
暴躁的程序猿
2022/03/23
6930
【SpringMVC】007-SpringMVC文件上传
①form表单的enctype取值必须是:multipart/form-data;
訾博ZiBo
2025/01/06
670
【SpringMVC】007-SpringMVC文件上传
Spring学习笔记(九)——SpringMVC实现文件上传
SpringMVC实现文件上传 文件上传的必要前提 form 表单的 enctype 取值必须是:multipart/form-data (默认值是:application/x-www-form-urlencoded) enctype:是表单请求正文的类型 method 属性取值必须是 Post 提供一个文件选择域<input type=”file” /> 文件上传的回顾 导入文件上传的jar包 <dependency> <groupId>commons-fileupload</groupId>
不愿意做鱼的小鲸鱼
2022/09/24
3600
Spring学习笔记(九)——SpringMVC实现文件上传
Spring MVC多种情况下的文件上传
会洗碗的CV工程师
2023/10/14
2260
Spring MVC多种情况下的文件上传
_Spring MVC多种情况下的文件上传
会洗碗的CV工程师
2023/11/18
2730
_Spring MVC多种情况下的文件上传
SpringMVC 解毒5
还记得我们在第二章讲DispatcherServlet时提到的MultipartResolver吗?
zhangheng
2020/04/29
4880
JavaWeb20-文件上传;下载(Java真正的全栈开发)
文件上传&下载一.文件上传 1. 文件上传介绍 要将客户端(浏览器)大数据存储到服务器端,不将数据直接存储到数据库中,而是要将数据存储到服务器所在的磁盘上,这就要使用文件上传。 作用:减少了数据库服务器的压力,对数据的操作更加灵活 2. 文件上传原理分析 所谓的文件上传就是服务器端通过request对象获取输入流,将浏览器端上传的数据读取出来,保存到服务器端 浏览器端操作 1.请求方式必须是post 2.使用<input type=’file’ name=’xxx’>,必须有name属性且有值 3.表单
Java帮帮
2018/03/19
1K0
JavaWeb20-文件上传;下载(Java真正的全栈开发)
Java实现文件上传代码
依赖2个jar包:commons-fileupload,commons-io。 代码如下: import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.text.DateFormat;
代码伴一生
2021/09/22
8720
JavaWeb之文件上传和下载
在如今的互联网时代,人们越来越喜欢将自己的数据存放到互联网上,于是便诞生了很多类型的软件,比如360网盘,百度网盘,云盘之类的。所以说,文件上传和下载的功能是现在非常主流的一个功能,应用十分广泛。
wangweijun
2020/01/20
1.1K0
SpringMVC文件上传
SpringMVC和Struts2的区别 共同点: 1.都是web层框架,都是基于MVC模型编写 2.底层都离不开原始ServletAPI 3.处理请求的机制都是一个核心控制器
用户3112896
2019/09/26
6300
Java审计之文件操作漏洞
本篇内容打算把Java审计中会遇到的一些文件操作的漏洞,都给叙述一遍。比如一些任意文件上传,文件下载,文件读取,文件删除,这些操作文件的漏洞。
全栈程序员站长
2022/07/13
1.1K0
javaweb-springMVC-55
项目地址:https://github.com/Jonekaka/javaweb-springMVC-55
全栈程序员站长
2021/05/19
5130
javaweb中运用fileupload上传文件
在 Java Web 应用中,使用 Apache Commons FileUpload 库可以方便地处理文件上传。本文也是介绍Java Web 开发运用Apache中的commons fileupload的commons io的工具来进行文件上传,在开发中会遇到很多比较棘手的问题,本人接触后进行了总结。
小明爱吃火锅
2023/11/30
2720
Java文件上传详解
在Web应用中,文件上传和下载功能是非常常用的功能,这篇博客就来讲一下JavaWeb中的文件上传和下载功能的实现。
全栈程序员站长
2022/08/27
1.9K0
Java文件上传详解
maven 项目 springMVC实现文件图片的上传下载功能详解(源码已提供,小白必看)
文件上传是项目开发中最常见的功能之一 ,springMVC 可以很好的支持文件上传,但是SpringMVC上下文中默认没有装配MultipartResolver,因此默认情况下其不能处理文件上传工作。如果想使用Spring的文件上传功能,则需要在上下文中配置MultipartResolver。
一写代码就开心
2020/11/19
2K1
maven 项目  springMVC实现文件图片的上传下载功能详解(源码已提供,小白必看)
SpringMvc整合美图秀秀M4(头像编辑器)
美图秀秀M4 头像编辑器是一款集旋转裁剪、特效美化、人像美容为一体的在线头像编辑工具。适用于有设置头像需求的BBS、SNS、微博和社区等Web产品。
小柒2012
2019/12/09
4790
SpringMvc整合美图秀秀M4(头像编辑器)
SpringBoot文件上传异常之提示The temporary upload location xxx is not valid
SpringBoot搭建的应用,一直工作得好好的,突然发现上传文件失败,提示org.springframework.web.multipart.MultipartException: Failed to parse multipart servlet request; nested exception is java.io.IOException: The temporary upload location [/tmp/tomcat.6239989728636105816.19530/work/Tomcat/localhost/ROOT] is not valid目录非法,实际查看目录,结果还真没有,下面就这个问题的表现,分析下SpringBoot针对文件上传的处理过程
一灰灰blog
2019/05/26
3.3K0
day18_文件的上传和下载学习笔记
作用:告知服务器请求正文的MIME类型(文件类型)。(与请求消息头中:Content-Type作用是一致的) 可选值:
黑泽君
2018/10/11
7100
day18_文件的上传和下载学习笔记
相关推荐
什么年代了,你还不知道 Servlet3.0 中的文件上传方式?
更多 >
领券
社区富文本编辑器全新改版!诚邀体验~
全新交互,全新视觉,新增快捷键、悬浮工具栏、高亮块等功能并同时优化现有功能,全面提升创作效率和体验
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验