目录

  • 1. 糟糕的异步存储文件实现
  • 2. 异常原因推理
  • 3. 问题解决方式
  • 4. spring清理文件原理
  • 5. tomcat清理文件原理

最近搞一个文件上传功能,由于文件太大,或者说其中包含了比较多的内容,需要大量逻辑处理。为了优化用户体验,自然想到使用异步来做这件事。也就是说,用户上传完文件后,我就开启另一个线程来处理具体逻辑,主线程就直接返回用户成功信息了。这样就显得非常快了,要看具体结果可以到结果页进行查看。看起来很棒!

然后,我踩坑了。表象就是系统报找不到文件的错误。具体如下!

1.糟糕的异步存储文件实现

为快速起见,我将原来同步的事情,直接改为了异步。如下:

@RestController@RequestMapping("/hello")@Slf4jpublic class HelloController {    @PostMapping("uploadFileWithParam")    public Object uploadFileWithParam(HttpServletRequest request,                                      @RequestParam Map params) {        log.info("param:{}", params);        DefaultMultipartHttpServletRequest multipartRequest                = (DefaultMultipartHttpServletRequest) request;        MultipartFile file = multipartRequest.getFile("file");        // 原本同步的工作,使用异步完成        new Thread(() -> {            // do sth else            SleepUtil.sleepMillis(10L);            if(file == null || file.isEmpty()) {                log.error("文件为空");                return;            }            try {                file.transferTo(new File("/tmp/" + System.currentTimeMillis() + ".dest"));            }            catch (IOException e) {                log.error("文件存储异常", e);            }            log.info("文件处理完成");            // do sth else        }).start();        return "success";    }}

看起来挺简单的,实则埋下一大坑。也不是自己不清楚这事,只是一时糊涂,就干了。这会有什么问题?

至少我在本地debug的时候,没有问题。然后似乎,如果不去注意上传后的结果,好像一切看起来都很美好。然而,线上预期就很骨感了。上传处理失败,十之八九。

所以,结果就是,处理得快,出错得也快。尴尬不!具体原因,下节详述。

2.异常原因推理

为什么会出现异常?而且我们仔细看其异常信息,就会发现,其报的是文件未找到的异常。

实际也很简单,因为我们是开的异步线程去处理文件的,那么和外部的请求线程不是一起的。而当外部线程处理完业务后,其携带的文件就会被删除。

为什么会被删除呢?我还持有其引用啊,它不应该删除的啊。这么想也不会有问题,因为GC时只会清理无用对象。没错,MultipartFile 这个实例我们仍然是持有有效引用的,不会被GC掉。但是,其中含有的文件,则不在GC的管理范畴了。它并不会因为你还持有file这个对象的引用,而不会将文件删除。至少想做这一点是很难的。

所以,总结:请求线程结束后,上传的临时文件会被清理掉。而如果文件处理线程在文件被删除掉之后,再进行处理的话,自然就会报文件找不到的异常了。

同时,也可以解释,为什么我们在debug的时候,没有报错了。因为,这是巧合啊。我们在debug时,也许刚好遇到子线程先处理文件,然后外部线程才退出。so, 你赢了。

另有一问题:为什么请求线程会将文件删除呢?回答这个问题,我们可以从反面问一下,如果请求线程不清理文件,会怎么样呢?答案是,系统上可能存在的临时文件会越来越多,从而将磁盘搞垮,而这不是一个完美的框架该有的表现。

好了,理解了可能是框架层面做掉了清理这一动作,那么到底是谁干了这事?又是如何干成的呢?我们稍后再讲。附模拟请求curl命令:

    curl -F 'file=@uptest.txt' -F 'a=1' -F 'b=2' http://localhost:8081/hello/uploadFileWithParam

3.问题解决方式

ok, 找到了问题的原因,要解决起来就容易多了。既然异步处理有问题,那么就改成同步处理好了。如下改造:

@RestController@RequestMapping("/hello")@Slf4jpublic class HelloController {    @PostMapping("uploadFileWithParam")    public Object uploadFileWithParam(HttpServletRequest request,                                      @RequestParam Map params) {        log.info("param:{}", params);        DefaultMultipartHttpServletRequest multipartRequest                = (DefaultMultipartHttpServletRequest) request;        MultipartFile file = multipartRequest.getFile("file");        if(file == null || file.isEmpty()) {            log.error("文件为空");            return "file is empty";        }        String localFilePath = "/tmp/" + System.currentTimeMillis() + ".dest";        try {            file.transferTo(new File(localFilePath));        }        catch (IOException e) {            log.error("文件存储异常", e);        }        // 原本同步的工作,使用异步完成        new Thread(() -> {            // do sth else            SleepUtil.sleepMillis(10L);            log.info("从文件:{} 中读取数据,处理业务", localFilePath);            log.info("文件处理完成");            // do sth else        }).start();        return "success";    }}

也就是说,我们将文件存储的这一步,移到了请求线程中去处理了,而其他的流程,则同样在异步线程中处理。有同学可能会问了,你这样做不就又会导致请求线程变慢了,从而回到最初的问题点上了吗?实际上,同学的想法有点多了,对一个文件的转存并不会耗费多少时间,大可不必担心。之所以导致处理慢的原因,更多的是因为我们的业务逻辑太过复杂导致。所以将文件转存放到外部线程,一点问题都没有。而被存储到其他位置的文件,则再不会受到框架管理的影响了。

不过,还有个问题需要注意的是,如果你将文件放在临时目录,如果代码出现了异常,那么文件被框架清理掉,而此时你将其转移走后,代码再出异常,则只能自己承担这责任了。所以,理论上,我们还有一个最终的文件清理方案,比如放在 try ... finnaly ... 进行处理。样例如下:

        // 原本同步的工作,使用异步完成        new Thread(() -> {            try {                // do sth else                SleepUtil.sleepMillis(10L);                log.info("从文件:{} 中读取数据,处理业务", localFilePath);                log.info("文件处理完成");                // do sth else            }            finally {                FileUtils.deleteQuietly(new File(localFilePath));            }        }).start();

如此,问题解决。

本着问题需要知其然,知其所以然的搬砖态度,我们还需要更深入点。探究框架层面的文件清理实现!请看下节。

4.spring清理文件原理

很明显,spring框架轻车熟路,所以必拿其开刀。spring 中清理文件的实现比较直接,就是在将请求分配给业务代码处理完成之后,就立即进行后续清理工作。

其操作是在 org.springframework.web.servlet.DispatcherServlet 中实现的。具体如下:

    /**     * Process the actual dispatching to the handler.     * 

The handler will be obtained by applying the servlet's HandlerMappings in order. * The HandlerAdapter will be obtained by querying the servlet's installed HandlerAdapters * to find the first that supports the handler class. *

All HTTP methods are handled by this method. It's up to HandlerAdapters or handlers * themselves to decide which methods are acceptable. * @param request current HTTP request * @param response current HTTP response * @throws Exception in case of any kind of processing failure */ protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; boolean multipartRequestParsed = false; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try { ModelAndView mv = null; Exception dispatchException = null; try { // 主动解析MultipartFile文件信息,并使用如 StandardServletMultipartResolver 封装request processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest != request); // Determine handler for the current request. mappedHandler = getHandler(processedRequest); if (mappedHandler == null) { noHandlerFound(processedRequest, response); return; } // Determine handler adapter for the current request. HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // Process last-modified header, if supported by the handler. String method = request.getMethod(); boolean isGet = "GET".equals(method); if (isGet || "HEAD".equals(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return; } } if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // Actually invoke the handler. mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); if (asyncManager.isConcurrentHandlingStarted()) { return; } applyDefaultViewName(processedRequest, mv); mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception ex) { dispatchException = ex; } catch (Throwable err) { // As of 4.3, we're processing Errors thrown from handler methods as well, // making them available for @ExceptionHandler methods and other scenarios. dispatchException = new NestedServletException("Handler dispatch failed", err); } processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } catch (Exception ex) { triggerAfterCompletion(processedRequest, response, mappedHandler, ex); } catch (Throwable err) { triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", err)); } finally { if (asyncManager.isConcurrentHandlingStarted()) { // Instead of postHandle and afterCompletion if (mappedHandler != null) { mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); } } else { // 如果是 multipart 文件上传,则做清理动作 // Clean up any resources used by a multipart request. if (multipartRequestParsed) { cleanupMultipart(processedRequest); } } } } /** * Clean up any resources used by the given multipart request (if any). * @param request current HTTP request * @see MultipartResolver#cleanupMultipart */ protected void cleanupMultipart(HttpServletRequest request) { if (this.multipartResolver != null) { MultipartHttpServletRequest multipartRequest = WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class); if (multipartRequest != null) { this.multipartResolver.cleanupMultipart(multipartRequest); } } }

值得一提的是,要触发文件的清理动作,需要有两个前提:1. 本次上传的是文件且被正常解析; 2. 配置了正确的文件解析器即 multipartResolver;否则,文件并不会被处理掉。说这事的原因是,在spring框架的低版本中,multipartResolver默认是不配置的,所以此时文件并不会被清理掉。而在高版本或者 springboot中,该值会被默认配置上。也就是说,如果你不小心踩到了这个坑,你可能是因为中途才配置了这个 resolver 导致。

下面我们再来看下真正的清理动作是如何运行的:

    // 1. StandardServletMultipartResolver 的清理实现:直接迭代删除    // org.springframework.web.multipart.support.StandardServletMultipartResolver#cleanupMultipart    @Override    public void cleanupMultipart(MultipartHttpServletRequest request) {        if (!(request instanceof AbstractMultipartHttpServletRequest) ||                ((AbstractMultipartHttpServletRequest) request).isResolved()) {            // To be on the safe side: explicitly delete the parts,            // but only actual file parts (for Resin compatibility)            try {                for (Part part : request.getParts()) {                    if (request.getFile(part.getName()) != null) {                        part.delete();                    }                }            }            catch (Throwable ex) {                LogFactory.getLog(getClass()).warn("Failed to perform cleanup of multipart items", ex);            }        }    }        // 2. CommonsMultipartResolver 的清理实现:基于map结构的文件枚举删除    // org.springframework.web.multipart.commons.CommonsMultipartResolver#cleanupMultipart    @Override    public void cleanupMultipart(MultipartHttpServletRequest request) {        if (!(request instanceof AbstractMultipartHttpServletRequest) ||                ((AbstractMultipartHttpServletRequest) request).isResolved()) {            try {                cleanupFileItems(request.getMultiFileMap());            }            catch (Throwable ex) {                logger.warn("Failed to perform multipart cleanup for servlet request", ex);            }        }    }    /**     * Cleanup the Spring MultipartFiles created during multipart parsing,     * potentially holding temporary data on disk.     * 

Deletes the underlying Commons FileItem instances. * @param multipartFiles a Collection of MultipartFile instances * @see org.apache.commons.fileupload.FileItem#delete() */ protected void cleanupFileItems(MultiValueMap multipartFiles) { for (List files : multipartFiles.values()) { for (MultipartFile file : files) { if (file instanceof CommonsMultipartFile) { CommonsMultipartFile cmf = (CommonsMultipartFile) file; cmf.getFileItem().delete(); LogFormatUtils.traceDebug(logger, traceOn -> "Cleaning up part '" + cmf.getName() + "', filename '" + cmf.getOriginalFilename() + "'" + (traceOn ? ", stored " + cmf.getStorageDescription() : "")); } } } }

所以,同样的事情,我们的做法往往是多种的。所以,千万不要拘泥于某一种实现无法自拔,更多的,是需要我们有一个全局框架思维。从而不至于迷失自己。

5.Tomact清理文件原理

如上,spring在某些情况下是不会做清理动作的,那么如果此时我们的业务代码出现了问题,这些临时文件又当如何呢?难道就任其占用我们的磁盘空间?实际上,spring仅是一个应用框架,在其背后还需要有应用容器,如tomcat, netty, websphere...

那么,在应用框架没有完成一些工作时,这些背后的容器是否应该有所作为呢?这应该是必须的,同样,是一个好的应用容器该有的样子。那么,我们看下tomcat是如何实现的呢?

然而事实上,tomcat并不会主动清理这些临时文件,因为不知道业务,不知道清理时机,所以不敢轻举妄动。但是,它会在重新部署的时候,去清理这些临时文件哟(java.io.tmpdir 配置值)。也就是说,这些临时文件,至多可以保留到下一次重新部署的时间。

    // org.apache.catalina.startup.ContextConfig#beforeStart    /**     * Process a "before start" event for this Context.     */    protected synchronized void beforeStart() {        try {            fixDocBase();        } catch (IOException e) {            log.error(sm.getString(                    "contextConfig.fixDocBase", context.getName()), e);        }        antiLocking();    }    // org.apache.catalina.startup.ContextConfig#antiLocking    protected void antiLocking() {        if ((context instanceof StandardContext)            && ((StandardContext) context).getAntiResourceLocking()) {            Host host = (Host) context.getParent();            String docBase = context.getDocBase();            if (docBase == null) {                return;            }            originalDocBase = docBase;            File docBaseFile = new File(docBase);            if (!docBaseFile.isAbsolute()) {                docBaseFile = new File(host.getAppBaseFile(), docBase);            }            String path = context.getPath();            if (path == null) {                return;            }            ContextName cn = new ContextName(path, context.getWebappVersion());            docBase = cn.getBaseName();            if (originalDocBase.toLowerCase(Locale.ENGLISH).endsWith(".war")) {                antiLockingDocBase = new File(                        System.getProperty("java.io.tmpdir"),                        deploymentCount++ + "-" + docBase + ".war");            } else {                antiLockingDocBase = new File(                        System.getProperty("java.io.tmpdir"),                        deploymentCount++ + "-" + docBase);            }            antiLockingDocBase = antiLockingDocBase.getAbsoluteFile();            if (log.isDebugEnabled()) {                log.debug("Anti locking context[" + context.getName()                        + "] setting docBase to " +                        antiLockingDocBase.getPath());            }            // 清理临时文件夹            // Cleanup just in case an old deployment is lying around            ExpandWar.delete(antiLockingDocBase);            if (ExpandWar.copy(docBaseFile, antiLockingDocBase)) {                context.setDocBase(antiLockingDocBase.getPath());            }        }    }    // org.apache.catalina.startup.ExpandWar#delete    public static boolean delete(File dir) {        // Log failure by default        return delete(dir, true);    }    public static boolean delete(File dir, boolean logFailure) {        boolean result;        if (dir.isDirectory()) {            result = deleteDir(dir, logFailure);        } else {            if (dir.exists()) {                result = dir.delete();            } else {                result = true;            }        }        if (logFailure && !result) {            log.error(sm.getString(                    "expandWar.deleteFailed", dir.getAbsolutePath()));        }        return result;    }

嗨,tomcat不干这活。自己干吧!默认把临时文件放到系统的临时目录,由操作系统去辅助清理该文件夹,何其轻松。

channelsftp 上传文件为空_文件上传踩坑记及文件清理原理探究相关推荐

  1. oracle vm 加载ova,vmware导入ova文件踩坑记小结

    问题来源 众所周知,所有的网络行为都会产生相应的网络流量,那么所有的网络攻击行为也有其对应的流量特点,那么是否能根据流量特点进而分析出其对应的是什么攻击行为呢? 我在虚拟机上使用vulnhub的靶场环 ...

  2. ROS踩坑之.msg文件未能转化为.h文件

    ROS踩坑之.msg文件未能转化为.h文件 标题的意思不够明确,这里重新声明一下,意思是文件全部齐全,再在编译ROS时出现无法找到"xxx.h"文件的情况. 方法一:查找相应的Cm ...

  3. 东八区转为0时区_踩坑记 | Flink 天级别窗口中存在的时区问题

    ❝ 本系列每篇文章都是从一些实际的 case 出发,分析一些生产环境中经常会遇到的问题,抛砖引玉,以帮助小伙伴们解决一些实际问题.本文介绍 Flink 时间以及时区问题,分析了在天级别的窗口时会遇到的 ...

  4. android小程序_小程序踩坑记

    小程序踩坑记 希望这个文章能尽量记录下小程序的那些坑,避免开发者们浪费自己的生命来定位到底是自己代码导致的还是啥神秘的字节跳变原因. 前记 小程序大多数坑是同一套代码在不同平台上表现不一致导致的,微信 ...

  5. element文件上传有文件但是后台接收为空_程序员提高篇:大规格文件(G)是如何做分片优化的?...

    作者:凹凸实验室 链接:https://juejin.im/post/5ebb4346e51d451ef53793ad 整体思路 第一步是结合项目背景,调研比较优化的解决方案. 文件上传失败是老生常谈 ...

  6. 安卓okhttp上传jason和图片_微信图片总是「已过期或被清理」?简单 3 招,可摆脱烦恼...

    微信图片总是「已过期或被清理」?简单 3 招可摆脱烦恼 除了文字.表格.PPT 和 PDF,大家日常办公中也常常和「图片文件」打交道. 于是,我们总碰到这样的问题: 1)图片文件太大,在线传输耗时,甲 ...

  7. .net core 文件流保存图片_使用JSDelivr加速Github、博客文件

    前言:当我们博客添加了本地视频或者大量图片等等,会导致加载时间过长,这里我们用JSDelivr对其进行加速,相当于免费的的CDN. 一.新建一个GitHub仓库 仓库名称随便 仓库属性Public,不 ...

  8. 文件标识符无效。使用 fopen 生成有效的文件标识符。_「存储架构」块存储、文件存储和对象存储(第1节)...

    全球传输和生成的数据比以往任何时候都多.国际数据公司(IDC)的分析师预计,到2025年,全球数据层将增至163zb.这比2016年16.1 ZB的数据增长了1000%以上.数据大量增加的原因是多方面 ...

  9. python删除txt文件第三行_真香!Python十大常用文件操作,轻松办公

    日常对于批量处理文件的需求非常多,用Python写脚本可以非常方便地实现,但在这过程中难免会和文件打交道,第一次做会有很多文件的操作无从下手,只能找度娘. 本篇文章整理了10个Python中最常用到的 ...

最新文章

  1. 获取线程中抛出的异常信息
  2. cidr斜线记法地址块网络前缀_学习笔记之《计算机网络》- 网络层(一)
  3. java 版本兼容问题_3.5版本存在jdk兼容的问题
  4. mini mp3模块 输出_小米有品众筹魔方mini电脑主机
  5. html.编辑数据回显,从HTML表格编辑/更新MySQL数据库值
  6. [转载]带着我的认证上路:五步让你成为网络专家
  7. mysql q4m_Mysql Q4M 队列操作封装(二)
  8. Kotlin入门(21)活动页面的跳转处理
  9. 4.1 HTML5 音频
  10. 16年10月计算机组成原理,福建师范大学16年8月课程考试《计算机组成原理》作业考核试题.doc...
  11. net 去掉第一位和最后一位_2020最后三个月港剧有咩睇?熟女强人首播!
  12. limesurvey php5.2,功能强大的PHP开源问卷调查系统 LimeSurvey 有中文语言包-win7中文语言包...
  13. STM32F0免费版keil下载激活方式
  14. Quartz表达式介绍及简单使用
  15. 百度网站诚信认证现在是个什么情况呢?
  16. 词汇学习系列(一):252个基本词根详解
  17. FPGA工程师面试试题集锦11~20
  18. 支付宝小程序财富号基金相关页面之间相关跳转
  19. fadeIn fadeOut
  20. 网站怎么屏蔽指定搜索引擎访蜘蛛的访问

热门文章

  1. oracle存储藏语,Oracle数据库多语言文字存储解决方案(续)
  2. 软件测试“老司机”的经验总结,看完你会感谢我的
  3. 长时储能系统-未来储能系统发展方向
  4. oracle同一个用户数据隔离,ORACLE一个实例多个用户实现数据隔离
  5. 人工智能项目(介绍)
  6. 给方程编号_一文教你掌握广义估计方程
  7. 规则引擎easyRule详解
  8. Qt 6.3.1 桌面时钟控件
  9. 203日语计算机考研学校,2019年考研日语大纲公布:203公共日语备考建议
  10. axios拦截器封装