成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

SpringBoot文件上傳異常之temporary upload location not val

klinson / 1585人閱讀

摘要:原文一灰灰之系列教程文件上傳異常原理分析搭建的應(yīng)用,一直工作得好好的,突然發(fā)現(xiàn)上傳文件失敗,提示目錄非法,實際查看目錄,結(jié)果還真沒有,下面就這個問題的表現(xiàn),分析下針對文件上傳的處理過程問題分析堆棧分析問題定位,最佳的輔助手段就是堆棧

原文: 一灰灰Blog之Spring系列教程文件上傳異常原理分析

SpringBoot搭建的應(yīng)用,一直工作得好好的,突然發(fā)現(xiàn)上傳文件失敗,提示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目錄非法,實際查看目錄,結(jié)果還真沒有,下面就這個問題的表現(xiàn),分析下SpringBoot針對文件上傳的處理過程

I. 問題分析 0. 堆棧分析

問題定位,最佳的輔助手段就是堆棧分析,首先撈出核心的堆棧信息

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
        at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.handleParseFailure(StandardMultipartHttpServletRequest.java:122)
        at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.parseRequest(StandardMultipartHttpServletRequest.java:113)
        at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.(StandardMultipartHttpServletRequest.java:86)
        at org.springframework.web.multipart.support.StandardServletMultipartResolver.resolveMultipart(StandardServletMultipartResolver.java:93)
        at org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1128)
        at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:960)
        at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:925)
        at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:974)
        at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:877)
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:661)
        at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:851)
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
        at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)

從堆棧內(nèi)容來看,問題比較清晰,目錄非法,根據(jù)path路徑,進入目錄,結(jié)果發(fā)現(xiàn),沒有這個目錄,那么問題的關(guān)鍵就是沒有目錄為什么會導(dǎo)致異常了,這個目錄到底有啥用

先簡單描述下上面的原因,上傳的文件會緩存到本地磁盤,而緩存的路徑就是上面的/tmp/tomcat.6239989728636105816.19530/work/Tomcat/localhost/ROOT,接著引入的疑問就是:

為什么上傳的文件要緩存到本地

為什么臨時目錄會不存在

什么地方實現(xiàn)文件緩存

1. 場景模擬

要確認(rèn)上面的問題,最直觀的方法就是擼源碼,直接看代碼就有點蛋疼了,接下來采用debug方式來層層剝離,看下根源再哪里。

首先是搭建一個簡單的測試項目,進行場景復(fù)現(xiàn), 首先創(chuàng)建一個接收文件上傳的Controller,如下

@RestController
@RequestMapping(path = "/file")
public class FileUploadRest {

    /**
     * 保存上傳的文件
     *
     * @param file
     * @return
     */
    private String saveFileToLocal(MultipartFile file) {
        try {
            String name = "/tmp/out_" + System.currentTimeMillis() + file.getName();
            FileOutputStream writer = new FileOutputStream(new File(name));
            writer.write(file.getBytes());
            writer.flush();
            writer.close();
            return name;
        } catch (Exception e) {
            e.printStackTrace();
            return e.getMessage();
        }
    }

    @PostMapping(path = "upload")
    public String upload(@RequestParam("file") MultipartFile file) {
        String ans = saveFileToLocal(file);
        return ans;
    }
}

其次就是使用curl來上傳文件

curl http://127.0.0.1:8080/file/upload -F "file=@/Users/user/Desktop/demo.jpg" -v

然后在接收文件上傳的方法中開啟斷點,注意下面紅框中的 location, 就是文件上傳的臨時目錄

2. 源碼定位

上面的截圖可以確認(rèn)確實將上傳的文件保存到了臨時目錄,驗證方式就是進入那個目錄進行查看,會看到一個tmp文件,接下來我們需要確定的是在什么地方,實現(xiàn)將數(shù)據(jù)緩存到本地的。

注意下圖,左邊紅框是這次請求的完整鏈路,我們可以通過逆推鏈路,去定位可能實現(xiàn)文件緩存的地方

如果對spring和tomcat的源碼不熟的話,也沒什么特別的好辦法,從上面的鏈路中,多打一些斷點,采用傳說中的二分定位方法來縮小范圍。

通過最開始的request對象和后面的request對象分析,發(fā)現(xiàn)一個可以作為參考標(biāo)準(zhǔn)的就是上圖中右邊紅框的request#parts屬性;開始是null,文件保存之后則會有數(shù)據(jù),下面給一個最終定位的動圖

所以關(guān)鍵就是org.springframework.web.filter.HiddenHttpMethodFilter#doFilterInternal 中的 String paramValue = request.getParameter(this.methodParam); 這一行代碼

到這里在單步進去,主要的焦點將集中在 org.apache.catalina.connector.Request#parseParts

進入上面方法的邏輯,很容易找到具體的實現(xiàn)位置 org.apache.tomcat.util.http.fileupload.FileUploadBase#parseRequest,這個方法的實現(xiàn)比較有意思,有必要貼出來看一下

public List parseRequest(RequestContext ctx)
        throws FileUploadException {
    List items = new ArrayList<>();
    boolean successful = false;
    try {
        FileItemIterator iter = getItemIterator(ctx);
        // 注意這里,文件工廠類,里面保存了臨時目錄的地址
        // 這個對象首次是在 org.apache.catalina.connector.Request#parseParts 方法的
        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;
            // 創(chuàng)建一個臨時文件對象
            FileItem fileItem = fac.createItem(item.getFieldName(), item.getContentType(),
                                               item.isFormField(), fileName);
            items.add(fileItem);
            try {
                // 流的拷貝,這塊代碼也挺有意思,將輸入流數(shù)據(jù)寫入輸出流
                // 后面會貼出源碼,看下開源大佬們的玩法,和我們自己寫的有啥區(qū)別
                Streams.copy(item.openStream(), fileItem.getOutputStream(), true);
            } catch (FileUploadIOException e) {
                throw (FileUploadException) e.getCause();
            } catch (IOException e) {
                throw new IOFileUploadException(String.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 (Exception ignored) {
                    // ignored TODO perhaps add to tracker delete failure list somehow?
                }
            }
        }
    }
}

核心代碼就兩點,一個是文件工廠類,一個是流的拷貝;前者定義了我們的臨時文件目錄,也是我們解決前面問題的關(guān)鍵,換一個我自定義的目錄永不刪除,不就可以避免上面的問題了么;后面一個則是數(shù)據(jù)復(fù)用方面的

首先看下FileItemFactory的實例化位置,在org.apache.catalina.connector.Request#parseParts中,代碼如下

具體的location實例化代碼為

// TEMPDIR = "javax.servlet.context.tempdir";
location = ((File) context.getServletContext().getAttribute(ServletContext.TEMPDIR));
3. 問題review a. 解決問題

到上面,基本上就撈到了最終的問題,先看如何解決這個問題

方法1

應(yīng)用重啟

方法2

增加服務(wù)配置,自定義baseDir

server.tomcat.basedir=/tmp/tomcat

方法3

注入bean,手動配置臨時目錄

@Bean
MultipartConfigElement multipartConfigElement() {
    MultipartConfigFactory factory = new MultipartConfigFactory();
    factory.setLocation("/tmp/tomcat");
    return factory.createMultipartConfig();
}

方法4

配置不刪除tmp目錄下的tomcat

vim /usr/lib/tmpfiles.d/tmp.conf

# 添加一行
x /tmp/tomcat.*
b. 流拷貝

tomcat中實現(xiàn)流的拷貝代碼如下,org.apache.tomcat.util.http.fileupload.util.Streams#copy(java.io.InputStream, java.io.OutputStream, boolean, byte[]) , 看下面的實現(xiàn),直觀影響就是寫得真特么嚴(yán)謹(jǐn)

public static long copy(InputStream inputStream,
            OutputStream outputStream, boolean closeOutputStream,
            byte[] buffer)
    throws IOException {
    OutputStream out = outputStream;
    InputStream in = inputStream;
    try {
        long total = 0;
        for (;;) {
            int res = in.read(buffer);
            if (res == -1) {
                break;
            }
            if (res > 0) {
                total += res;
                if (out != null) {
                    out.write(buffer, 0, res);
                }
            }
        }
        if (out != null) {
            if (closeOutputStream) {
                out.close();
            } else {
                out.flush();
            }
            out = null;
        }
        in.close();
        in = null;
        return total;
    } finally {
        IOUtils.closeQuietly(in);
        if (closeOutputStream) {
            IOUtils.closeQuietly(out);
        }
    }
}
c. 自問自答

前面提出了幾個問題,現(xiàn)在給一個簡單的回答,因為篇幅問題,后面會單開一文,進行詳細(xì)說明

什么地方緩存文件

上面的定位過程給出答案,具體實現(xiàn)邏輯在 org.apache.tomcat.util.http.fileupload.FileUploadBase#parseRequest

為什么目錄會不存在

springboot啟動時會創(chuàng)建一個/tmp/tomcat.*/work/Tomcat/localhost/ROOT的臨時目錄作為文件上傳的臨時目錄,但是該目錄會在n天之后被系統(tǒng)自動清理掉,這個清理是由linux操作系統(tǒng)完成的,具體的配置如下 vim /usr/lib/tmpfiles.d/tmp.conf

#  This file is part of systemd.
#
#  systemd is free software; you can redistribute it and/or modify it
#  under the terms of the GNU Lesser General Public License as published by
#  the Free Software Foundation; either version 2.1 of the License, or
#  (at your option) any later version.

# See tmpfiles.d(5) for details

# Clear tmp directories separately, to make them easier to override
v /tmp 1777 root root 10d
v /var/tmp 1777 root root 30d

# Exclude namespace mountpoints created with PrivateTmp=yes
x /tmp/systemd-private-%b-*
X /tmp/systemd-private-%b-*/tmp
x /var/tmp/systemd-private-%b-*
X /var/tmp/systemd-private-%b-*/tmp
為什么要緩存文件

因為流取一次消費之后,后面無法再從流中獲取數(shù)據(jù),所以緩存方便后續(xù)復(fù)用;這一塊后面詳細(xì)說明

4. 小結(jié)

定位這個問題的感覺,就是對SpringBoot和tomcat的底層,實在是不太熟悉,作為一個以Spring和tomcat吃飯的碼農(nóng)而言,發(fā)現(xiàn)問題就需要改正,列入todo列表,后續(xù)需要深入一下

II. 其他 0. 項目

工程:spring-boot-demo

1. 一灰灰Blog

一灰灰Blog個人博客 https://blog.hhui.top

一灰灰Blog-Spring專題博客 http://spring.hhui.top

一灰灰的個人博客,記錄所有學(xué)習(xí)和工作中的博文,歡迎大家前去逛逛

2. 聲明

盡信書則不如,以上內(nèi)容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發(fā)現(xiàn)bug或者有更好的建議,歡迎批評指正,不吝感激

微博地址: 小灰灰Blog

QQ: 一灰灰/3302797840

3. 掃描關(guān)注

一灰灰blog

知識星球

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/73246.html

相關(guān)文章

  • springboot2.x文件上傳

    摘要:項目拋出了個異常,。所以我們需要添加個轉(zhuǎn)換器類這樣就能夠識別了總結(jié)感覺把文件上傳所能遇到的坑全踩了個變,心累。 pom包的配置 org.springframework.boot spring-boot-starter-web 啟動項類修改 /** * 防止文件大于10M時Tomcat連接重置 * * @return */ @Bean public T...

    ChristmasBoy 評論0 收藏0
  • SpringBoot開發(fā)存儲服務(wù)器

    摘要:今天我們嘗試整合,并決定建立一個非常簡單的微服務(wù),使用作為前端渲編程語言進行前端頁面渲染基礎(chǔ)環(huán)境技術(shù)版本創(chuàng)建項目初始化項目修改增加和的支持開發(fā)存儲服務(wù)器一個簡單的應(yīng)用類添加接口 今天我們嘗試Spring Boot整合Angular,并決定建立一個非常簡單的Spring Boot微服務(wù),使用Angular作為前端渲編程語言進行前端頁面渲染. 基礎(chǔ)環(huán)境 技術(shù) 版本 Java 1...

    godruoyi 評論0 收藏0
  • springBoot圖片上傳與回顯

    摘要:采用間接注入的方式注入在中會在構(gòu)造函數(shù)之后執(zhí)行同樣可以實現(xiàn)接口以上代碼注意處。需要說明的是,工具類,需要使用來讓其被管理。那么,你回顯成。即可完整代碼圖片上傳單位配置靜態(tài)路徑,多個可用逗號隔開陳少平獲取客戶端真實地址。 版本聲明:springBoot: 1.5.9jdk: 1.8IDE: IDEA注:此項目前后端分離 使用的方法是配置靜態(tài)目錄(類似tomcat的虛擬目錄映射) 1、配置...

    JaysonWang 評論0 收藏0
  • 編譯安裝Nginx以及配置運行Drupal 8,實現(xiàn)上傳進度功能

    摘要:下載以及相關(guān)模塊下載以及模塊并解壓。接著執(zhí)行,即可完成編譯安裝。運行需要的配置首先需要在的上下文里增加一條這條是表示每上傳就更新進度信息。 這篇文章的目的是在編譯安裝Nginx的同時,安裝upload和uploadprogress模塊,以及運行Drupal 8所需要的配置。由于使用的是Raspberry pi 3B,所以系統(tǒng)用的Raspbian,Debian/Ubuntu應(yīng)該也是差不多...

    mist14 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<