1. 程式人生 > >Spring Boot 2.x(十六):玩轉vue檔案上傳

Spring Boot 2.x(十六):玩轉vue檔案上傳

為什麼使用Vue-Simple-Uploader

最近用到了Vue + Spring Boot來完成檔案上傳的操作,踩了一些坑,對比了一些Vue的元件,發現了一個很好用的元件——Vue-Simple-Uploader,先附上gayhub的

,再說說為什麼選用這個元件,對比vue-ant-design和element-ui的上傳元件,它能做到更多的事情,比如:

  • 可暫停、繼續上傳
  • 上傳佇列管理,支援最大併發上傳
  • 分塊上傳
  • 支援進度、預估剩餘時間、出錯自動重試、重傳等操作
  • 支援“快傳”,通過檔案判斷服務端是否已存在從而實現“快傳”

由於需求中需要用到斷點續傳,所以選用了這個元件,下面我會從最基礎的上傳開始說起:

單檔案上傳、多檔案上傳、資料夾上傳

Vue程式碼:

                    <uploader
        :options="uploadOptions1"
        :autoStart="true"
        class="uploader-app"
      >
        <uploader-unsupport></uploader-unsupport>
        <uploader-drop>
          <uploader-btn style="margin-right:20px;" :attrs="attrs">選擇檔案</uploader-btn>
          <uploader-btn :attrs="attrs" directory>選擇資料夾</uploader-btn>
        </uploader-drop>
        <uploader-list></uploader-list>
</uploader>

該元件預設支援多檔案上傳,這裡我們從官方demo中貼上過來這段程式碼,然後在uploadOption1中配置上傳的路徑即可,其中uploader-btn 中設定directory屬性即可選擇資料夾進行上傳。

uploadOption1:

 uploadOptions1: {
        target: "//localhost:18080/api/upload/single",//上傳的介面
        testChunks: false, //是否開啟伺服器分片校驗
        fileParameterName: "file",//預設的檔案引數名
        headers: {},
        query() {},
        categaryMap: { //用於限制上傳的型別
          image: ["gif", "jpg", "jpeg", "png", "bmp"]
        }
}

在後臺的介面的編寫,我們為了方便,定義了一個chunk類用於接收元件預設傳輸的一些後面方便分塊斷點續傳的引數:

Chunk類

@Data
public class Chunk implements Serializable {
    
    private static final long serialVersionUID = 7073871700302406420L;

    private Long id;
    /**
     * 當前檔案塊,從1開始
     */
    private Integer chunkNumber;
    /**
     * 分塊大小
     */
    private Long chunkSize;
    /**
     * 當前分塊大小
     */
    private Long currentChunkSize;
    /**
     * 總大小
     */
    private Long totalSize;
    /**
     * 檔案標識
     */
    private String identifier;
    /**
     * 檔名
     */
    private String filename;
    /**
     * 相對路徑
     */
    private String relativePath;
    /**
     * 總塊數
     */
    private Integer totalChunks;
    /**
     * 檔案型別
     */
    private String type;

    /**
     * 要上傳的檔案
     */
    private MultipartFile file;
}

在編寫介面的時候,我們直接使用這個類作為引數去接收vue-simple-uploader傳來的引數即可,注意這裡要使用POST來接收喲~

介面方法:

    @PostMapping("single")
    public void singleUpload(Chunk chunk) {
                // 獲取傳來的檔案
        MultipartFile file = chunk.getFile();
        // 獲取檔名
        String filename = chunk.getFilename();
        try {
            // 獲取檔案的內容
            byte[] bytes = file.getBytes();
            // SINGLE_UPLOADER是我定義的一個路徑常量,這裡的意思是,如果不存在該目錄,則去建立
            if (!Files.isWritable(Paths.get(SINGLE_FOLDER))) {
                Files.createDirectories(Paths.get(SINGLE_FOLDER));
            }
            // 獲取上傳檔案的路徑
            Path path = Paths.get(SINGLE_FOLDER,filename);
            // 將位元組寫入該檔案
            Files.write(path, bytes);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

這裡需要注意一點,如果檔案過大的話,Spring Boot後臺會報錯

org.apache.tomcat.util.http.fileupload.FileUploadBase$FileSizeLimitExceededException: The field file exceeds its maximum permitted size of 1048576 bytes.

這時需要在application.yml中配置servlet的最大接收檔案大小(預設大小是1MB和10MB)

spring:
  servlet:
    multipart:
      max-file-size: 10MB 
      max-request-size: 100MB

下面我們啟動專案,選擇需要上傳的檔案就可以看到效果了~ 是不是很方便~ 但是同樣的事情其餘的元件基本上也可以做到,之所以選擇這個,更多的是因為它可以支援斷點分塊上傳,實現上傳過程中斷網,再次聯網的話可以從斷點位置開始繼續秒傳~下面我們來看看斷點續傳是怎麼玩的。

斷點分塊續傳

先說一下分塊斷點續傳的大概原理,我們在元件可以配置分塊的大小,大於該值的檔案會被分割成若干塊兒去上傳,同時將該分塊的chunkNumber儲存到資料庫(Mysql or Redis,這裡我選擇的是Redis

元件上傳的時候會攜帶一個identifier的引數(這裡我採用的是預設的值,你也可以通過生成md5的方式來重新賦值引數),將identifier作為Redis的key,設定hashKey為”chunkNumber“,value是由每次上傳的chunkNumber組成的一個Set集合。

在將uploadOption中的testChunk的值設定為true之後,該元件會先發一個get請求,獲取到已經上傳的chunkNumber集合,然後在checkChunkUploadedByResponse方法中判斷是否存在該片段來進行跳過,傳送post請求上傳分塊的檔案。

每次上傳片段的時候,service層返回當前的集合大小,並與引數中的totalChunks進行對比,如果發現相等,就返回一個狀態值,來控制前端發出merge請求,將剛剛上傳的分塊合為一個檔案,至此檔案的斷點分塊上傳就完成了。

下面是對應的程式碼~

Vue程式碼:

<uploader
        :options="uploadOptions2"
        :autoStart="true"
        :files="files"
        @file-added="onFileAdded2"
        @file-success="onFileSuccess2"
        @file-progress="onFileProgress2"
        @file-error="onFileError2"
      >
        <uploader-unsupport></uploader-unsupport>
        <uploader-drop>
          <uploader-btn :attrs="attrs">分塊上傳</uploader-btn>
        </uploader-drop>
        <uploader-list></uploader-list>
</uploader>

校驗是否上傳過的程式碼

 uploadOptions2: {
        target: "//localhost:18080/api/upload/chunk",
        chunkSize: 1 * 1024 * 1024,
        testChunks: true,
        checkChunkUploadedByResponse: function(chunk, message) {
          let objMessage = JSON.parse(message);
            // 獲取當前的上傳塊的集合
          let chunkNumbers = objMessage.chunkNumbers;
          // 判斷當前的塊是否被該集合包含,從而判定是否需要跳過
          return (chunkNumbers || []).indexOf(chunk.offset + 1) >= 0;
        },
        headers: {},
        query() {},
        categaryMap: {
          image: ["gif", "jpg", "jpeg", "png", "bmp"],
          zip: ["zip"],
          document: ["csv"]
        }
}

上傳後成功的處理,判斷狀態來進行merge操作

onFileSuccess2(rootFile, file, response, chunk) {
      let res = JSON.parse(response);
          // 後臺報錯
      if (res.code == 1) {
        return;
      }
      // 需要合併
      if (res.code == 205) {
        // 傳送merge請求,引數為identifier和filename,這個要注意需要和後臺的Chunk類中的引數名對應,否則會接收不到~
        const formData = new FormData();
        formData.append("identifier", file.uniqueIdentifier);
        formData.append("filename", file.name);
        merge(formData).then(response => {});
      } 
    },

判定是否存在的程式碼,注意這裡的是GET請求!!!

                   @GetMapping("chunk")
    public Map<String, Object> checkChunks(Chunk chunk) {
        return uploadService.checkChunkExits(chunk);
    }

    @Override
    public Map<String, Object> checkChunkExits(Chunk chunk) {
        Map<String, Object> res = new HashMap<>();
        String identifier = chunk.getIdentifier();
        if (redisDao.existsKey(identifier)) {
            Set<Integer> chunkNumbers = (Set<Integer>) redisDao.hmGet(identifier, "chunkNumberList");
            res.put("chunkNumbers",chunkNumbers);
        }
        return res;
    }

儲存分塊,並儲存資料到Redis的程式碼。這裡的是POST請求!!!

    
    @PostMapping("chunk")    
                public Map<String, Object> saveChunk(Chunk chunk) {
        // 這裡的操作和儲存單段落的基本是一致的~
        MultipartFile file = chunk.getFile();
        Integer chunkNumber = chunk.getChunkNumber();
        String identifier = chunk.getIdentifier();
        byte[] bytes;
        try {
            bytes = file.getBytes();
            // 這裡的不同之處在於這裡進行了一個儲存分塊時將檔名的按照-chunkNumber的進行儲存
            Path path = Paths.get(generatePath(CHUNK_FOLDER, chunk));
            Files.write(path, bytes);
        } catch (IOException e) {
            e.printStackTrace();
        }
                 // 這裡進行的是儲存到redis,並返回集合的大小的操作
        Integer chunks = uploadService.saveChunk(chunkNumber, identifier);
        Map<String, Object> result = new HashMap<>();
        // 如果集合的大小和totalChunks相等,判定分塊已經上傳完畢,進行merge操作
        if (chunks.equals(chunk.getTotalChunks())) {
            result.put("message","上傳成功!");
            result.put("code", 205);
        }
        return result;
    }


        /**
         * 生成分塊的檔案路徑
         */
        private static String generatePath(String uploadFolder, Chunk chunk) {
        StringBuilder sb = new StringBuilder();
        // 拼接上傳的路徑
        sb.append(uploadFolder).append(File.separator).append(chunk.getIdentifier());
        //判斷uploadFolder/identifier 路徑是否存在,不存在則建立
        if (!Files.isWritable(Paths.get(sb.toString()))) {
            try {
                Files.createDirectories(Paths.get(sb.toString()));
            } catch (IOException e) {
                log.error(e.getMessage(), e);
            }
        }
        // 返回以 - 隔離的分塊檔案,後面跟的chunkNumber方便後面進行排序進行merge
        return sb.append(File.separator)
                .append(chunk.getFilename())
                .append("-")
                .append(chunk.getChunkNumber()).toString();

    }

    /**
     * 儲存資訊到Redis
     */
        public Integer saveChunk(Integer chunkNumber, String identifier) {
        // 獲取目前的chunkList
        Set<Integer> oldChunkNumber = (Set<Integer>) redisDao.hmGet(identifier, "chunkNumberList");
        // 如果獲取為空,則新建Set集合,並將當前分塊的chunkNumber加入後存到Redis
        if (Objects.isNull(oldChunkNumber)) {
            Set<Integer> newChunkNumber = new HashSet<>();
            newChunkNumber.add(chunkNumber);
            redisDao.hmSet(identifier, "chunkNumberList", newChunkNumber);
            // 返回集合的大小
            return newChunkNumber.size();
        } else {
             // 如果不為空,將當前分塊的chunkNumber加到當前的chunkList中,並存入Redis
            oldChunkNumber.add(chunkNumber);
            redisDao.hmSet(identifier, "chunkNumberList", oldChunkNumber);
            // 返回集合的大小
            return oldChunkNumber.size();
        }

    }

合併的後臺程式碼:

    @PostMapping("merge")
    public void mergeChunks(Chunk chunk) {
        String fileName = chunk.getFilename();
        uploadService.mergeFile(fileName,CHUNK_FOLDER + File.separator + chunk.getIdentifier());
    }

        @Override
    public void mergeFile(String fileName, String chunkFolder) {
        try {
            // 如果合併後的路徑不存在,則新建
            if (!Files.isWritable(Paths.get(mergeFolder))) {
                Files.createDirectories(Paths.get(mergeFolder));
            }
            // 合併的檔名
            String target = mergeFolder + File.separator + fileName;
            // 建立檔案
            Files.createFile(Paths.get(target));
            // 遍歷分塊的資料夾,並進行過濾和排序後以追加的方式寫入到合併後的檔案
            Files.list(Paths.get(chunkFolder))
                     //過濾帶有"-"的檔案
                    .filter(path -> path.getFileName().toString().contains("-"))
                     //按照從小到大進行排序
                    .sorted((o1, o2) -> {
                        String p1 = o1.getFileName().toString();
                        String p2 = o2.getFileName().toString();
                        int i1 = p1.lastIndexOf("-");
                        int i2 = p2.lastIndexOf("-");
                        return Integer.valueOf(p2.substring(i2)).compareTo(Integer.valueOf(p1.substring(i1)));
                    })
                    .forEach(path -> {
                        try {
                            //以追加的形式寫入檔案
                            Files.write(Paths.get(target), Files.readAllBytes(path), StandardOpenOption.APPEND);
                            //合併後刪除該塊
                            Files.delete(path);
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    });
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

至此,我們的斷點續傳就完美結束了,完整的程式碼我已經上傳到gayhub~,歡迎star fork pr~(後面還會把博文也上傳到gayhub喲~)

前端:https://github.com/viyog/viboot-front

後臺:https://github.com/viyog/viboot

寫在後面

最近由於家庭+工作忙昏了頭,鴿了這麼久很是抱歉,從這周開始恢復更新,同時本人在準備往大資料轉型,後續會出一系列的Java轉型大資料的學習筆記,包括Java基礎系列的深入解讀和重寫,同時Spring Boot系列還會一直保持連載,不過可能不會每週都更,我會把目前使用Spring Boot中遇到的問題和坑寫一寫,謝謝一直支援我的粉絲們~愛你們~

相關推薦

Spring Boot 2.xvue檔案

為什麼使用Vue-Simple-Uploader 最近用到了Vue + Spring Boot來完成檔案上傳的操作,踩了一些坑,對比了一些Vue的元件,發現了一個很好用的元件——Vue-Simple-Uploader,先附上gayhub的 ,再說說為什麼選用這個元件,對比vue-ant-design和elem

Spring Boot 2.x快速入門Elastic Search

What —— Elasticsearch是什麼? Elasticsearch是一個基於Lucene的搜尋伺服器,Elasticsearch也是使用Java編寫的,它的內部使用Lucene做索引與搜尋,但是它的目的是使用全文檢索變得簡單,通過隱藏Lucene的複雜性,取而代之的提供一套簡單一致的RESTful

Spring Boot 2.x 郵件服務一文打盡

前景介紹 在日常的工作中,我們經常會用到郵件服務,比如傳送驗證碼,找回密碼確認,註冊時郵件驗證等,所以今天在這裡進行郵件服務的一些操作。 大致思路 我們要做的其實就是把Java程式作為一個客戶端,然後通過配置SMTP協議去連線我們所使用的傳送郵箱(from)對應的SMTP伺服器,然後通過SMTP協議,將郵件轉

Spring Boot 2.xAOP實戰--列印介面日誌

介面日誌有啥用 在我們日常的開發過程中,我們可以通過介面日誌去檢視這個介面的一些詳細資訊。比如客戶端的IP,客戶端的型別,響應

Spring Boot 2.x 構建優雅的RESTful接口

github 統一 spring 發送 註意 water quest 優雅 ring RESTful 相信在座的各位對於RESTful都是略有耳聞,那麽RESTful到底是什麽呢? REST(Representational State Transfer)表述性狀態轉移是

Java框架spring Boot學習筆記log4j介紹

inf alt 技術分享 images 使用 image 詳細 配置文件 -128 功能 日誌功能,通過log4j可以看到程序運行過程的詳細信息。 使用 導入log4j的jar包 復制log4j的配置文件,復制到src下面         3.設置日誌級別    

Spring Boot 2.x優雅的統一返回值

為什麼要統一返回值 在我們做後端應用的時候,前後端分離的情況下,我們經常會定義一個數據格式,通常會包含code,message,data這三個必不可少的資訊來方便我們的交流,下面我們直接來看程式碼 ReturnVO package indi.viyoung.viboot.util; import ja

Spring Boot 2.x整合Mybatis的四種方式

前言 目前的大環境下,使用Mybatis作為持久層框架還是佔了絕大多數的,下面我們來說一下使用Mybatis的幾種姿勢。 姿勢一:

Spring Boot 2.x整合Mybatis-Plus

簡介 Mybatis-Plus是在Mybatis的基礎上,國人開發的一款持久層框架。 並且榮獲了2018年度開源中國最受歡迎的中

spring-boot-route使用logback生產日誌檔案

日誌是一個系統非常重要的一部分,我們經常需要通過檢視日誌來定位問題,今天我們一起來學習一下Spring Boot的日誌系統。有很多同學習慣性的在生產程式碼中使用System.out來輸出日誌,這是不推薦的一種做法,因為System.out是一個同步操作,會在一定程度上影響系統性能,而Logger是一個非同步操

javaweb學習筆記JDBC2

批處理 當需要向資料庫傳送一批SQL語句執行時,應避免向資料庫一條條的傳送執行,而應採用JDBC的批處理機制,以提升執行效率。 實現批處理有兩種方式: ①Statement.addBatch(sql) :新增批處理命令。 優點:可以向資料庫傳送多條不同的SQL語句。 缺點:S

Spring Security5.7 Multiple HttpSecurity

We can configure multiple HttpSecurity instances just as we can have multiple <http> blocks. The key is to extend the WebSecurityConfi

Spring Boot 學習筆記——單元測試

依賴關係 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test&

spring boot整合hessian

1.首先新增hessian依賴 <dependency> <groupId>com.caucho</groupId> <artifactId>hessian</artifact

Flask1.0.2系列 擴充套件

英文原文地址:http://flask.pocoo.org/docs/1.0/extensions/ 若有翻譯錯誤或者不盡人意之處,請指出,謝謝~         擴充套件是為Flask應用程式新增功能的額外的包。舉個栗子,一個擴充

使用Intellij中的Spring Initializr來快速構建Spring Boot/Cloud工程

在之前的所有Spring Boot和Spring Cloud相關博文中,都會涉及Spring Boot工程的建立。而建立的方式多種多樣,我們可以通過Maven來手工構建或是通過腳手架等方式快速搭建,也可以通過《Spring Boot快速入門》一文中提到的SPRING INITIALIZR頁面工具來建立,相信每

Spring Boot入門系列使用JdbcTemplate操作資料庫,配置多資料來源!

前面介紹了Spring Boot 中的整合Mybatis並實現增刪改查、如何實現事物控制。不清楚的朋友可以看看之前的文章:https://www.cnblogs.com/zhangweizhong/category/1657780.html。 Spring Boot 除了Mybatis資料庫框架,還有Jdbc

Spring Boot入門系列整合mybatis,使用註解的方式實現增刪改查

之前介紹了Spring Boot 整合mybatis 使用xml配置的方式實現增刪改查,還介紹了自定義mapper 實現複雜多表關聯查詢。雖然目前 mybatis 使用xml 配置的方式 已經極大減輕了配置的複雜度,支援 generator 外掛 根據表結構自動生成實體類、配置檔案和dao層程式碼,減輕很大一

Spring Boot入門系列Spring Boot 開發環境熱部署

在實際的專案開發過中,當我們修改了某個java類檔案時,需要手動重新編譯、然後重新啟動程式的,整個過程比較麻煩,特別是專案啟動慢的時候,更是影響開發效率。其實Spring Boot的專案碰到這種情況,同樣也同樣需要經歷重新編譯、重新啟動程式的過程。 只不過 Spring Boot 提供了一個spring-bo

Spring Boot入門系列整合Mybatis,建立自定義mapper 實現多表關聯查詢!

之前講了Springboot整合Mybatis,介紹瞭如何自動生成pojo實體類、mapper類和對應的mapper.xml 檔案,並實現最基本的增刪改查功能。mybatis 外掛自動生成的mapper 實現了大部分基本、通用的方法,如:insert、update、delete、select 等大概20個左右