補習系列(11)-springboot 文件上傳原理
目錄
- 一、文件上傳原理
- 二、springboot 文件機制
- 臨時文件
- 定制配置
- 三、示例代碼
- A. 單文件上傳
- B. 多文件上傳
- C. 文件上傳異常
- D. Bean 配置
- 四、文件下載
- 小結
一、文件上傳原理
一個文件上傳的過程如下圖所示:
瀏覽器發起HTTP POST請求,指定請求頭:
Content-Type: multipart/form-data服務端解析請求內容,執行文件保存處理,返回成功消息。
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();
}
}
四、文件下載
既然解釋了文件上傳,自然避免不了文件下載,
文件下載非常簡單,只需要包括下面兩步:
- 讀文件流;
- 輸出到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 文件上傳原理