1. 程式人生 > >補習系列(11)-springboot 檔案上傳原理

補習系列(11)-springboot 檔案上傳原理

目錄

一、檔案上傳原理

一個檔案上傳的過程如下圖所示:

  1. 瀏覽器發起HTTP POST請求,指定請求頭:
    Content-Type: multipart/form-data

  2. 服務端解析請求內容,執行檔案儲存處理,返回成功訊息。

RFC1867 定義了HTML表單檔案上傳的處理機制。
通常一個檔案上傳的請求內容格式如下:

POST /upload HTTP/1.1 
Host:xxx.org 
Content-type: multipart/form-data, boundary="boundaryStr"

--boundaryStr
content-disposition: form-data; name="name"

Name Of Picture
--boundaryStr
Content-disposition: attachment; name="picfile"; filename="picfile.gif"
Content-type: image/gif
Content-Transfer-Encoding: binary

...contents of picfile.gif...

其中boundary指定了內容分割的邊界字串;
Content-dispostion 指定了這是一個附件(檔案),包括引數名稱、檔名稱;
Content-type 指定了檔案型別;
Content-Transfer-Encoding 指定內容傳輸編碼;

二、springboot 檔案機制

springboot 的檔案上傳處理是基於Servlet 實現的。
在Servlet 2.5 及早期版本之前,檔案上傳需要藉助 commons-fileupload 元件來實現。
Servlet 3.0規範之後,提供了對檔案上傳的原生支援,進一步簡化了應用程式的實現。
Tomcat 為例,在檔案上傳之後通過將寫入到臨時檔案,最終將檔案實體傳參到應用層,如下:

Tomcat 實現了 Servlet3.0 規範,通過ApplicationPart對檔案上傳流實現封裝,
其中,DiskFileItem 描述了上傳檔案實體,在請求解析時生成該物件,
需要關注的是,DiskFileItem 聲明瞭一個臨時檔案,用於臨時儲存上傳檔案的內容,
SpringMVC 對上層的請求實體再次封裝,最終構造為MultipartFile傳遞給應用程式。

臨時檔案

臨時檔案的路徑定義:

{temp_dir}/upload_xx_xxx.tmp

temp_dir是臨時目錄,通過 系統屬性java.io.tmpdir指定,預設值為:

作業系統 路徑
windows C:\Users{username}\AppData\Local\Temp|
Linux /tmp

定製配置

為了對檔案上傳實現定製,可以在application.properties中新增如下配置:

//啟用檔案上傳
spring.http.multipart.enabled=true 
//檔案大於該閾值時,將寫入磁碟,支援KB/MB單位
spring.http.multipart.file-size-threshold=0 
//自定義臨時路徑
spring.http.multipart.location= 
//最大檔案大小(單個)
spring.http.multipart.maxFileSize=10MB
//最大請求大小(總體)
spring.http.multipart.maxRequestSize=10MB

其中 maxFileSize/maxRequestSize 用於宣告大小限制,
當上傳檔案超過上面的配置閾值時,會返回400(BadRequest)的錯誤;
file-size-threshold是一個閾值,用於控制是否寫入磁碟;
location是儲存的目錄,如果不指定將使用前面所述的預設臨時目錄。

這幾個引數由SpringMVC控制,用於注入 Servlet3.0 的檔案上傳配置,如下:

public class MultipartConfigElement {

    private final String location;// = "";
    private final long maxFileSize;// = -1;
    private final long maxRequestSize;// = -1;
    private final int fileSizeThreshold;// = 0;

三、示例程式碼

接下來以簡單的程式碼展示檔案上傳處理

A. 單檔案上傳

    @PostMapping(value = "/single", consumes = {
            MediaType.MULTIPART_FORM_DATA_VALUE }, produces = MediaType.TEXT_PLAIN_VALUE)
    @ResponseBody
    public ResponseEntity<String> singleUpload(@RequestParam("file") MultipartFile file) {
        logger.info("file receive {}", file.getOriginalFilename());

        // 檢查檔案內容是否為空
        if (file.isEmpty()) {
            return ResponseEntity.badRequest().body("no file input");
        }

        // 原始檔名
        String fileName = file.getOriginalFilename();

        // 檢查字尾名
        if (!checkImageSuffix(fileName)) {
            return ResponseEntity.badRequest().body("the file is not image");
        }

        // 檢查大小
        if (!checkSize(file.getSize())) {
            return ResponseEntity.badRequest().body("the file is too large");
        }

        String name = save(file);

        URI getUri = ServletUriComponentsBuilder.fromCurrentContextPath().path("/file/get").queryParam("name", name)
                .build(true).toUri();

        return ResponseEntity.ok(getUri.toString());

    }

在上面的程式碼中,我們通過Controller方法傳參獲得MultipartFile實體,而後是一系列的檢查動作:
包括檔案為空、檔案字尾、檔案大小,這裡不做展開。
save 方法實現了簡單的本地儲存,如下:

    private String save(MultipartFile file) {

        if (!ROOT.isDirectory()) {
            ROOT.mkdirs();
        }
        try {
            String path = UUID.randomUUID().toString() + getSuffix(file.getOriginalFilename());
            File storeFile = new File(ROOT, path);
            file.transferTo(storeFile);
            return path;

        } catch (IllegalStateException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

B. 多檔案上傳

與單檔案類似,只需要宣告MultipartFile陣列引數即可:

    @PostMapping(value = "/multi", consumes = {
            MediaType.MULTIPART_FORM_DATA_VALUE }, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    @ResponseBody
    public ResponseEntity<List<String>> multiUpload(@RequestParam("file") MultipartFile[] files) {

        logger.info("file receive count {}", files.length);

        List<String> uris = new ArrayList<String>();
        for (MultipartFile file : files) {

C. 檔案上傳異常

如前面所述,當檔案上傳大小超過限制會返回400錯誤,為了覆蓋預設的行為,可以這樣:

    @ControllerAdvice(assignableTypes = FileController.class)
    public class MultipartExceptionHandler {
        @ExceptionHandler(MultipartException.class)
        public ResponseEntity<String> handleUploadError(MultipartException e) {
            return ResponseEntity.badRequest().body("上傳失敗:" + e.getCause().getMessage());
        }
    }

D. Bean 配置

SpringBoot 提供了JavaBean配置的方式,前面提到的幾個配置也可以這樣實現:

    @Configuration
    public static class FileConfig {
        @Bean
        public MultipartConfigElement multipartConfigElement() {
            MultipartConfigFactory factory = new MultipartConfigFactory();
            factory.setMaxFileSize("10MB");
            factory.setMaxRequestSize("50MB");
            return factory.createMultipartConfig();

        }
    }

四、檔案下載

既然解釋了檔案上傳,自然避免不了檔案下載,
檔案下載非常簡單,只需要包括下面兩步:

  1. 讀檔案流;
  2. 輸出到Response;

這樣,嘗試寫一個Controller方法:

    @GetMapping(path = "/get")
    public ResponseEntity<Object> get(@RequestParam("name") String name) throws IOException {

        ...
        File file = new File(ROOT, name);
        if (!file.isFile()) {
            return ResponseEntity.notFound().build();
        }

        if (!file.canRead()) {
            return ResponseEntity.status(HttpStatus.FORBIDDEN).body("no allow to access");
        }
        Path path = Paths.get(file.getAbsolutePath());

        ByteArrayResource resource = new ByteArrayResource(Files.readAllBytes(path));
        return ResponseEntity.ok().contentLength(file.length()).body(resource);
    }

這段程式碼通過引數(name)來指定訪問檔案,之後將流寫入到Response。

接下來,我們訪問一個確實存在的檔案,看看得到了什麼?

...

!! 沒錯,這就是檔案的內容,瀏覽器嘗試幫你呈現了。
那麼,我們所期望的下載呢? 其實,真實的下載過程應該如下圖:

區別就在於,我們在返回響應時添加了Content-Disposition頭,用來告訴瀏覽器響應內容是一個附件。
這樣根據約定的協議,瀏覽器會幫我們完成響應的解析及下載工作。
修改上面的程式碼,如下:

    @GetMapping(path = "/download")
    public ResponseEntity<Object> download(@RequestParam("name") String name) throws IOException {

        if (StringUtils.isEmpty(name)) {
            return ResponseEntity.badRequest().body("name is empty");
        }

        if (!checkName(name)) {
            return ResponseEntity.badRequest().body("name is illegal");
        }

        File file = new File(ROOT, name);
        if (!file.isFile()) {
            return ResponseEntity.notFound().build();
        }

        if (!file.canRead()) {
            return ResponseEntity.status(HttpStatus.FORBIDDEN).body("no allow to access");
        }

        Path path = Paths.get(file.getAbsolutePath());
        ByteArrayResource resource = new ByteArrayResource(Files.readAllBytes(path));

        return ResponseEntity.ok().header("Content-Disposition", "attachment;fileName=" + name)
                .contentLength(file.length()).contentType(MediaType.APPLICATION_OCTET_STREAM).body(resource);
    }

繼續嘗試訪問檔案,此時應該能看到檔案被正確下載了。

小結

檔案上傳開發是Web開發的基礎課,從早期的Servlet + common_uploads元件到現在的SpringBoot,檔案的處理已經被大大簡化。

這次除了展示SpringBoot 檔案上傳的示例程式碼之外,也簡單介紹了檔案上傳相關的協議知識點。對開發者來說,瞭解一點內部原理總是有好處的。

本文來自"美碼師的補習系列-springboot篇" ,如果覺得老司機的文章還不賴,歡迎關注分享^-^