對Web(Springboot + Vue)實現文件下載功能的改進

此爲 軟件開發與創新 課程的做業html

  • 對已有項目(非本人)閱讀分析
  • 找出軟件尚存缺陷
  • 改進其軟件作二次開發
  • 整理成一份博客

原項目簡介

本篇博客所分析的項目來自於 ジ緋色月下ぎ——vue+axios+springboot文件下載 的博客,在其基礎之上進行了一些分析和改進。前端

原項目前端使用了Vue框架,後端採用Springboot框架進行搭建,經過前端發送請求,後端返回文件流給前端進行文件下載。vue

源碼解讀

  • 後端主要代碼
public class DownLoadFile {

  @RequestMapping(value = "/downLoad", method = RequestMethod.GET)
  public static final void downLoad(HttpServletResponse res) throws UnsupportedEncodingException {
    //文件名 能夠經過形參傳進來
    String fileName = "t_label.txt";
    //要下載的文件地址 能夠經過形參傳進來
    String filepath = "f:/svs/" + fileName;

    OutputStream os = null;//輸出文件流
    InputStream is = null;//輸入文件流
    try {
      // 取得輸出流
      os = res.getOutputStream();
      // 清空輸出流
      res.reset();
      res.setContentType("application/x-download;charset=GBK");//設置響應頭爲文件流
      res.setHeader("Content-Disposition","attachment;filename=" 
                    + new String(fileName.getBytes("utf-8"), "iso-8859-1"));//設置文件名
      // 讀取流
      File f = new File(filepath);
      is = new FileInputStream(f);
      if (is == null) {
        System.out.println("下載附件失敗");
      }
      // 複製
      IOUtils.copy(is, res.getOutputStream());//經過IOUtils的copy函數直接將輸入文件流的內容複製到輸出文件流內
        res.getOutputStream().flush();//刷新輸出流
      } catch (IOException e) {
        System.out.println("下載附件失敗");
      }
      // 文件的關閉放在finally中
      finally {
        try {
          if (is != null) {
            is.close();
          }
        } catch (IOException e) {
          System.out.println("輸入流關閉異常");
        }
        try {
          if (os != null) {
            os.close();
          }
        } catch (IOException e) {
            	System.out.println("輸出流關閉異常");
        }
      }
    }
}

原做者後端利用IOUtils.copy完成了輸入輸出流的寫入,此函數內部調用了緩衝區,實現穩定的文件流的寫出,後端基本可以應對各類文件的文件流傳輸。java

但查閱相關文檔,發現copy方法的buffer大小爲固定的 4Kios

而不一樣大小的文件不一樣網速的用戶對於文件的下載時緩衝區的大小其實經過調整可以有明顯提速,因此須要進一步測試是否經過調整buffer大小可以使用戶體驗明顯提高。spring

  • 前端
<el-button size="medium" type="primary" @click="downloadFile">Test</el-button>
//js
downloadFile(){
      this.axios({
        method: "get",
        url: '/api/downloadFile',
        responseType: 'blob',
        headers: {
          Authorization: localStorage.getItem("token")
        }
      })
        .then(response => {
       //文件名 文件保存對話框中的默認顯示
         let fileName = 'test.txt';
         let data = response.data;
         if(!data){
           return
         }
         console.log(response);
      //構造a標籤 經過a標籤來下載
         let url = window.URL.createObjectURL(new Blob([data]))
         let a = document.createElement('a')
         a.style.display = 'none'
         a.href = url
       //此處的download是a標籤的內容,固定寫法,不是後臺api接口
         a.setAttribute('download',fileName)
         document.body.appendChild(a)
         //點擊下載
         a.click()
         // 下載完成移除元素
         document.body.removeChild(a);
         // 釋放掉blob對象
         window.URL.revokeObjectURL(url);
        })
        .catch(response => {
          this.$message.error(response);
        });
    },

做者前端使用動態建立a標籤的方式進行前端用戶進行文件下載的操做。這裏就有一個比較大的問題。axios

這個問題是由axios自身的特性產生的,在使用axios進行下載請求後,axios會將全部的返回數據先進行緩存,等所有緩存完成後再調用then方法。也就是用axios的then方法接收返回數據時,會將用戶須要下載的文件先緩存在內存中,等文件所有下載完成再運行then內的代碼。後端

這個特性也是致使問題的關鍵,致使的問題有:api

  • 下載大文件佔用內存很高
  • 在文件下載完成前,用戶不會收到任何提示

改進方案

測試文件下載耗時

首先是針對後端的一些優化的嘗試瀏覽器

粗略測試方法:使用本地搭建先後端,將 F盤文件夾做爲服務器存放文件的位置,文件經過前端下載至 D盤,理論下載速度爲100M/s(由實際複製速度估算),經過改變 buffer大小測試文件下載速度差別,平均耗時計算方法爲去掉最低最高耗時,取剩下平均值

下載的文件大小爲700M,理論最快下載耗時 7s

  • 使用copy方法
public String downloadFile(@RequestParam("filename") String filename, @RequestParam("sha1Hash") String sha1Hash, HttpServletRequest request, HttpServletResponse response) throws IOException {
        //…………………………略去細節
        FileInfo fileInfo = new FileInfo();//將請求信息轉爲bean
        fileInfo.setFilename(filename);
        fileInfo.setSha1Hash(sha1Hash);
        String resPath = fileInfoRepository.searchFilePath(fileInfo);//查詢文件在服務器的位置

        FileInputStream fileInputStream = null;//輸入流
        ServletOutputStream os = null;//輸出流
        try {
            File fileRes = new File(resPath);//經過路徑獲取文件

            os = response.getOutputStream();//獲取輸出流
            
            fileInputStream = new FileInputStream(fileRes);//獲取文件流

            long start = System.currentTimeMillis();//下載開始時間

            IOUtils.copy(fileInputStream , response.getOutputStream());//使用已有庫進行數據流傳輸

            long end = System.currentTimeMillis();//下載結束時間
            System.out.println("遍歷" + filename + "文件流,耗時:" + (end - start) + " ms");//輸出下載所用時間

            os.flush();//刷新輸出流
            response.setStatus(HttpServletResponse.SC_OK);
           //……………………
    }
次序 耗時
1 21823ms
2 20098ms
3 12643ms
4 22284ms
5 23779ms

平均耗時:21402ms——21.4s

  • 使用copyLarge方法
long start = System.currentTimeMillis();//下載開始時間

            IOUtils.copyLarge(fileInputStream , response.getOutputStream());//使用已有庫進行數據流傳輸

            long end = System.currentTimeMillis();//下載結束時間
            System.out.println("遍歷" + filename + "文件流,耗時:" + (end - start) + " ms");//輸出下載所用時間
次序 耗時
1 23351ms
2 21046ms
3 26786ms
4 22190ms
5 28389ms

平均耗時:24109ms——24.1s

  • 使用自定義buffer循環讀取(20M)
byte[] bytes = new byte[1024 * 1024 * 20];//靜態buffer
            int len = 0;
 
			long start = System.currentTimeMillis();//下載開始時間

            while ((len = bufferedInputStream.read(bytes)) != -1) {
                os.write(bytes, 0, len);
            }

            long end = System.currentTimeMillis();//下載結束時間
            System.out.println("遍歷" + filename + "文件流,耗時:" + (end - start) + " ms");//輸出下載所用時間
次序 耗時
1 20212ms
2 16648ms
3 15591ms
4 15496ms
5 13185ms

平均耗時:15911ms——15.9s

  • 使用自定義buffer循環讀取(40M)
byte[] bytes = new byte[1024 * 1024 * 40];//靜態buffer
            int len = 0;
 
			long start = System.currentTimeMillis();//下載開始時間

            while ((len = bufferedInputStream.read(bytes)) != -1) {
                os.write(bytes, 0, len);
            }

            long end = System.currentTimeMillis();//下載結束時間
            System.out.println("遍歷" + filename + "文件流,耗時:" + (end - start) + " ms");//輸出下載所用時間
次序 耗時
1 12194ms
2 10198ms
3 9794ms
4 15116ms
5 16523ms

平均耗時:12503ms——12.5s

結論:可見在網速恆定,文件大小恆定的狀況下,緩衝區大小對於文件下載速度會形成必定差別。而在實際應用環境中緩衝區大小會受:文件大小、內存使用狀況、網速狀況、帶寬佔用量的多方面因素影響,因此選擇一個合適的緩衝區大小,甚至是動態調整緩衝區大小都是可以改善用戶體驗的一個方法。

詢問搜索觸發下載的替代方案

這是針對前端axios下載問題的改進之路

  • 首先經過搜素瞭解爲什麼沒法正常觸發瀏覽器下載

    • 知乎評論區中找到了類似提問->傳送門

  • 其次經過搜素和詢問找到了以下幾種解決方案

    • 使用a標籤之前端靜態資源的方式提供下載
    • 使用form表單進行文件下載
    • 詢問了解相關建議和解決方案

經過實踐,採用第二種方式即便用form表單代替axios的then方法進行文件下載

實現替代方案

  • 前端改用動態建立form表單的方式下載文件
downloadFile (file,scope) {
        var form = document.createElement("form");//建立form元素
        form.setAttribute("style", "display:none");
        form.setAttribute("method", "post");//post方式提交
        var input = document.createElement('input');//用input標籤傳遞參數
        input.setAttribute('type', 'hidden');
        input.setAttribute('name', 'filename');
        input.setAttribute('value', file.filename);
        form.append(input);
        var input2 = document.createElement('input');
        input2.setAttribute('name', 'sha1Hash');
        input2.setAttribute('value', file.sha1Hash);
        form.append(input2);
        form.setAttribute("action", initialization.downloadFileInterface);//請求地址
        form.setAttribute("target", "_self");//不跳轉至新頁面
        var body = document.createElement("body");
        body.setAttribute("style", "display:none");
        document.body.appendChild(form);
        form.submit();
        form.remove();
      },
  • 後端處理表單請求
public String downloadFile(@RequestParam("filename") String filename, @RequestParam("sha1Hash") String sha1Hash, HttpServletRequest request, HttpServletResponse response) throws IOException {
        request.setCharacterEncoding("UTF-8");
        FileInfo fileInfo = new FileInfo();
        fileInfo.setFilename(filename);
        fileInfo.setSha1Hash(sha1Hash);
        String resPath = fileInfoRepository.searchFilePath(fileInfo);

        FileInputStream fileInputStream = null;
        BufferedInputStream bufferedInputStream = null;
        ServletOutputStream os = null;
        try {
            File fileRes = new File(resPath);
            
            response.reset();
            response.addHeader("Access-Control-Allow-Origin", "*");//設置響應頭
            response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
            response.addHeader("Access-Control-Allow-Headers", "Content-Type");
            response.setContentType("application/octet-stream");
            response.setHeader("Content-Disposition", "attachment;filename=" + new String(fileInfo.getFilename().getBytes(StandardCharsets.UTF_8), "ISO-8859-1"));

            os = response.getOutputStream();
            fileInputStream = new FileInputStream(fileRes);
            bufferedInputStream = new BufferedInputStream(fileInputStream);

            byte[] bytes = new byte[1024 * 1024 * 20];//靜態buffer
            int len = 0;

            while ((len = bufferedInputStream.read(bytes)) != -1) {//循環讀取
                os.write(bytes, 0, len);
            }

            os.flush();
            response.setStatus(HttpServletResponse.SC_OK);
            return "success";
        }
        catch (Exception e){
            response.setStatus(HttpServletResponse.SC_EXPECTATION_FAILED);
            return null;
        }
        finally {
            try{
                if(bufferedInputStream != null)
                	bufferedInputStream.close();
            }catch(IOException e){
                System.out.println("bufferedInputStream關閉異常");
            }
            try{
            	if(fileInputStream != null)
                	fileInputStream.close();
            }catch(IOException e){
                System.out.println("fileInputStream關閉異常");
            }
            try{
            	if(os != null)
                	os.close();
            }catch(IOException e){
                System.out.println("os關閉異常");
            }
        }
    }

改進效果

  • 前端下載截圖

相關文章
相關標籤/搜索