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

補習系列(11)-springboot 文件上傳原理

absolute 期望 rop 內部 知識點 public 程序 大小限制 ...

目錄

  • 一、文件上傳原理
  • 二、springboot 文件機制
    • 臨時文件
    • 定制配置
  • 三、示例代碼
    • A. 單文件上傳
    • B. 多文件上傳
    • C. 文件上傳異常
    • D. Bean 配置
  • 四、文件下載
  • 小結

技術分享圖片

一、文件上傳原理

一個文件上傳的過程如下圖所示:

技術分享圖片

  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篇" ,如果覺得老司機的文章還不賴,歡迎關註分享^-^

補習系列(11)-springboot 文件上傳原理