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


此為 軟件開發與創新 課程的作業

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

原項目簡介

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

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

源碼解讀

  • 后端主要代碼
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完成了輸入輸出流的寫入,此函數內部調用了緩沖區,實現穩定的文件流的寫出,后端基本能夠應對各種文件的文件流傳輸。

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

而不同大小的文件不同網速的用戶對於文件的下載時緩沖區的大小其實通過調整能夠有明顯提速,所以需要進一步測試是否通過調整buffer大小能夠使用戶體驗明顯提升。

  • 前端
<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會將所有的返回數據先進行緩存,等全部緩存完成后再調用then方法。也就是用axios的then方法接收返回數據時,會將用戶需要下載的文件先緩存在內存中,等文件全部下載完成再運行then內的代碼。

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

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

改進方案

測試文件下載耗時

首先是針對后端的一些優化的嘗試

粗略測試方法:使用本地搭建前后端,將 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關閉異常");
            }
        }
    }

改進效果

  • 前端下載截圖


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM