为什么我的题目只显示半截(图片只显示半截)
653
2022-05-29
欢迎访问我的GitHub
这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos
本篇概览
本章我们来一起阅读和分析SpringMVC的部分源码,看看收到POST请求中的二进制文件后,SpingMVC框架是如何处理的;
使用了SpringMVC框架的web应用中,接收上传文件时,一般分以下三步完成:
在spring配置文件中配置一个bean:
pom.xml中添加apache的commons-fileupload库的依赖:
开发业务Controller的响应方法,以下代码是将POST的文件存储到应用所在的电脑上:
@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方法即可将上传的文件创建到业务所需的位置;
三个疑问
虽然业务代码简单,以上几步即可完成对上传文件的接收和处理,但是有几个疑问想要弄清楚:
为什么要配置名为multipartResolver的bean;
为什么要依赖apache的commons-fileupload库;
从客户端的POST到Controller中的file.transferTo方法调用,具体做了哪些文件相关的操作?
接下来我们就一起来看看SpringMVC的源码,寻找这几个问题的答案;
Spring版本
本文涉及的Spring相关库,例如spring-core、spring-web、spring-webmvc等,都是4.0.2.RELEASE版本;
SpringMVC源码
先来看下入口类DispatcherServlet的源码,在应用初始化的时候会调用initMultipartResolver方法:
this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class); ...
所以,如果配置了名为
multipartResolver
的bean,就会DispatcherServlet的multipartResolver保存下来;
再来看一下处理POST请求时候的调用链:
FrameworkServlet.doPost -> FrameworkServlet.processRequest -> DispatcherServlet.doService -> DispatcherServlet.doDispatch -> DispatcherServlet.checkMultipart -> multipartResolver.resolveMultipart(request)
因此,应用收到上传文件的请求时,最终会调用multipartResolver.resolveMultipart;
第一个疑问已经解开:SpringMVC框架在处理POST请求时,会使用名为multipartResolver的bean来处理文件;
CommonsMultipartResolver.resolveMultipart方法中会调用parseRequest方法,我们看parseRequest方法的源码:
String encoding = this.determineEncoding(request); FileUpload fileUpload = this.prepareFileUpload(encoding); try { List
从以上代码可以发现,在调用prepareFileUpload方法的时候,相关的fileItemFactory和fileUpload对象都已经是commons-fileupload库中定义的类型了,并且最终还是调用由commons-fileupload库中的ServletFileUpload.parseRequest方法负责解析工作,构建FileItem对象;
第二个疑问已经解开:SpringMVC框架在处理POST请求时,本质是调用commons-fileupload库中的API来处理的;
继续关注CommonsMultipartResolver.parseRequest方法,里面调用了ServletFileUpload.parseRequest方法,最终由FileUploadBase.parseRequest方法来处理:
public List
重点关注这一段:
Streams.copy(item.openStream(), fileItem.getOutputStream(), true);
,这是一次流的拷贝,将提交文件的inputstrem写入到一个outputstream,我们再看看getOutputStream方法的源码:
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方法的关键代码如下:
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方法:
public void write(byte b[]) throws IOException { checkThreshold(b.length); getStream().write(b); written += b.length; }
先调用checkThreshold方法,检查***
已写入长度
加上
即将写入的长度
***是否达到threshold值,如果达到就会将thresholdExceeded设置为true,并调用thresholdReached方法;
c. thresholdReached方法源码如下:
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)的时候,就不再写入到内存,而是写入到文件了;
我们再回到主线:CommonsMultipartResolver,这里FileItem对象在parseFileItems方法中经过处理,被放入了CommonsMultipartFile对象中,再被放入MultipartParsingResult对象中,最后被放入DefaultMultipartHttpServletRequest对象中,返回到DispatcherServlet.doDispatch方法中,然后传递到业务的controller中处理;
业务Controller的响应方法中,调用了file.transferTo方法将临时文件写入到业务指定的文件中,transferTo方法中有一行关键代码:
this.fileItem.write(dest);
,我们打开DiskFileItem类,看看这个write方法的源码:
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方法中,有这么一段:
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下载,地址和链接信息如下表所示:
这个git项目中有多个目录,本次所需的资源放在springmvcfileserver,如下图红框所示:
如果您想了解如何POST二进制文件到服务端,请下载uploadfileclient这个文件夹下的客户端demo工程,如下图红框所示:
如果您不想让SpringMVC处理上传的文件,而是自己去调用apache的commons-fileupload库来做些更复杂的操作,您可以参考fileserverdemo这个文件夹下的demo工程,如下图红框所示:
如果您的应用是基于springboot的,实现文件服务可以参考springbootfileserver这个文件夹下的demo工程,如下图红框所示:
至此,本次阅读和分析实战已全部完成,在您学习和理解SpringMVC框架的过程中,希望本文能对您有所帮助,如果发现文中有错误,也真诚的期待您能留下意见;
欢迎关注华为云博客:程序员欣宸
学习路上,你不孤单,欣宸原创一路相伴…
MVC Spring
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。