<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          文件上傳踩坑記及文件清理原理探究

          共 15652字,需瀏覽 32分鐘

           ·

          2021-03-26 16:07

          走過路過不要錯過

          點擊藍字關注我們


          最近搞一個文件上傳功能,由于文件太大,或者說其中包含了比較多的內容,需要大量邏輯處理。為了優(yōu)化用戶體驗,自然想到使用異步來做這件事。也就是說,用戶上傳完文件后,我就開啟另一個線程來處理具體邏輯,主線程就直接返回用戶成功信息了。這樣就顯得非??炝?,要看具體結果可以到結果頁進行查看。看起來很棒!  

          然后,我踩坑了。表象就是系統(tǒng)報找不到文件的錯誤。具體如下!

          1:糟糕的異步存儲文件實現(xiàn)

          為快速起見,我將原來同步的事情,直接改為了異步。如下:

          @RestController@RequestMapping("/hello")@Slf4jpublic class HelloController {
          @PostMapping("uploadFileWithParam") public Object uploadFileWithParam(HttpServletRequest request, @RequestParam Map<String, Object> 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的時候,沒有問題。然后似乎,如果不去注意上傳后的結果,好像一切看起來都很美好。然而,線上預期就很骨感了。上傳處理失敗,十之八九。

          所以,結果就是,處理得快,出錯得也快。尷尬不!具體原因,下節(jié)詳述。

          2. 異常原因推理

          為什么會出現(xiàn)異常?而且我們仔細看其異常信息,就會發(fā)現(xiàn),其報的是文件未找到的異常。

          實際也很簡單,因為我們是開的異步線程去處理文件的,那么和外部的請求線程不是一起的。而當外部線程處理完業(yè)務后,其攜帶的文件就會被刪除。

          為什么會被刪除呢?我還持有其引用啊,它不應該刪除的啊。這么想也不會有問題,因為GC時只會清理無用對象。沒錯,MultipartFile 這個實例我們仍然是持有有效引用的,不會被GC掉。但是,其中含有的文件,則不在GC的管理范疇了。它并不會因為你還持有file這個對象的引用,而不會將文件刪除。至少想做這一點是很難的。

          所以,總結:請求線程結束后,上傳的臨時文件會被清理掉。而如果文件處理線程在文件被刪除掉之后,再進行處理的話,自然就會報文件找不到的異常了。

          同時,也可以解釋,為什么我們在debug的時候,沒有報錯了。因為,這是巧合啊。我們在debug時,也許剛好遇到子線程先處理文件,然后外部線程才退出。so, 你贏了。

          另有一問題:為什么請求線程會將文件刪除呢?回答這個問題,我們可以從反面問一下,如果請求線程不清理文件,會怎么樣呢?答案是,系統(tǒng)上可能存在的臨時文件會越來越多,從而將磁盤搞跨,而這不是一個完美的框架該有的表現(xiàn)。

          好了,理解了可能是框架層面做掉了清理這一動作,那么到底是誰干了這事?又是如何干成的呢?我們稍后再講。附模擬請求curl命令:

          curl -F '[email protected]' -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<String, Object> 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("從文件:{} 中讀取數(shù)據(jù),處理業(yè)務", localFilePath); log.info("文件處理完成"); // do sth else }).start(); return "success"; }}

          也就是說,我們將文件存儲的這一步,移到了請求線程中去處理了,而其他的流程,則同樣在異步線程中處理。有同學可能會問了,你這樣做不就又會導致請求線程變慢了,從而回到最初的問題點上了嗎?實際上,同學的想法有點多了,對一個文件的轉存并不會耗費多少時間,大可不必擔心。之所以導致處理慢的原因,更多的是因為我們的業(yè)務邏輯太過復雜導致。所以將文件轉存放到外部線程,一點問題都沒有。而被存儲到其他位置的文件,則再不會受到框架管理的影響了。

          不過,還有個問題需要注意的是,如果你將文件放在臨時目錄,如果代碼出現(xiàn)了異常,那么文件被框架清理掉,而此時你將其轉移走后,代碼再出異常,則只能自己承擔這責任了。所以,理論上,我們還有一個最終的文件清理方案,比如放在 try ... finnaly ... 進行處理。樣例如下:

                 // 原本同步的工作,使用異步完成        new Thread(() -> {            try {                // do sth else                SleepUtil.sleepMillis(10L);                log.info("從文件:{} 中讀取數(shù)據(jù),處理業(yè)務", localFilePath);                log.info("文件處理完成");                // do sth else            }            finally {                FileUtils.deleteQuietly(new File(localFilePath));            }        }).start();

          如此,問題解決。

          本著問題需要知其然,知其所以然的搬磚態(tài)度,我們還需要更深入點。探究框架層面的文件清理實現(xiàn)!請看下節(jié)。

          4. spring清理文件原理

          很明顯,spring框架輕車熟路,所以必拿其開刀。spring 中清理文件的實現(xiàn)比較直接,就是在將請求分配給業(yè)務代碼處理完成之后,就立即進行后續(xù)清理工作?! ?/span>

          其操作是在 org.springframework.web.servlet.DispatcherServlet 中實現(xiàn)的。具體如下:

             /**     * Process the actual dispatching to the handler.     * <p>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.     * <p>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); } } }

          值得一提的是,要觸發(fā)文件的清理動作,需要有兩個前提:1. 本次上傳的是文件且被正常解析;2. 配置了正確的文件解析器即 multipartResolver;否則,文件并不會被處理掉。說這事的原因是,在spring框架的低版本中,multipartResolver默認是不配置的,所以此時文件并不會被清理掉。而在高版本或者 springboot中,該值會被默認配置上。也就是說,如果你不小心踩到了這個坑,你可能是因為中途才配置了這個 resolver 導致。

          下面我們再來看下真正的清理動作是如何運行的:

             // 1. StandardServletMultipartResolver 的清理實現(xiàn):直接迭代刪除    // 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 的清理實現(xiàn):基于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. * <p>Deletes the underlying Commons FileItem instances. * @param multipartFiles a Collection of MultipartFile instances * @see org.apache.commons.fileupload.FileItem#delete() */ protected void cleanupFileItems(MultiValueMap<String, MultipartFile> multipartFiles) { for (List<MultipartFile> 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() : "")); } } } }

          所以,同樣的事情,我們的做法往往是多種的。所以,千萬不要拘泥于某一種實現(xiàn)無法自拔,更多的,是需要我們有一個全局框架思維。從而不至于迷失自己。

          5. tomcat清理文件原理

          如上,spring在某些情況下是不會做清理動作的,那么如果此時我們的業(yè)務代碼出現(xiàn)了問題,這些臨時文件又當如何呢?難道就任其占用我們的磁盤空間?實際上,spring僅是一個應用框架,在其背后還需要有應用容器,如tomcat, netty, websphere...

          那么,在應用框架沒有完成一些工作時,這些背后的容器是否應該有所作為呢?這應該是必須的,同樣,是一個好的應用容器該有的樣子。那么,我們看下tomcat是如何實現(xiàn)的呢?

          然而事實上,tomcat并不會主動清理這些臨時文件,因為不知道業(yè)務,不知道清理時機,所以不敢輕舉妄動。但是,它會在重新部署的時候,去清理這些臨時文件喲(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不干這活。自己干吧!默認把臨時文件放到系統(tǒng)的臨時目錄,由操作系統(tǒng)去輔助清理該文件夾,何其輕松。




          往期精彩推薦



          騰訊、阿里、滴滴后臺面試題匯總總結 — (含答案)

          面試:史上最全多線程面試題 !

          最新阿里內推Java后端面試題

          JVM難學?那是因為你沒認真看完這篇文章


          END


          關注作者微信公眾號 —《JAVA爛豬皮》


          了解更多java后端架構知識以及最新面試寶典


          你點的每個好看,我都認真當成了


          看完本文記得給作者點贊+在看哦~~~大家的支持,是作者源源不斷出文的動力


          作者:等你歸去來

          出處:https://www.cnblogs.com/yougewe/p/14020041.html


          瀏覽 42
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  免费做爱视频网站 | 靠逼视频网站久久精品 | 在线h片 | 欧美乱码精品一区二区三区 | 国产麻豆成人传媒免费观看 |