Spring FileUpload
基本概念
2017/6/21 updated
2019/01/20 (增加連結)
Spring Framework提供一個MutipartFile,讓我們可以接受form上傳的檔案,然後我們就可以將MutipartFile檔案複製到web server,這樣就完成檔案的上傳了。
首先,Controller (如:FileUploadController.java)需要列出所有檔案 (如:listUploadedFiles)、取得檔案 (如:serveFile)、上傳檔案 (如:handleFileUpload)。這個Controller會使用到StorageService,StorageService是個interface,真正的實作是FileSystemStorageService,FileSystemStorageService用到了StorageProperties來儲存上傳的路徑,另外,還用到了StorageException及StorageFileNotFoundException來處理例外事件。相對應的view是: uploadForm.html。
這個範例用了java 7的新類別,如:Path、Files。
FileUploadController.java
package com.example.demo.controller;
import com.example.demo.storage.StorageFileNotFoundException;
import com.example.demo.storage.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.io.IOException;
import java.util.stream.Collectors;
@Controller
public class FileUploadController {
private final StorageService storageService;
@Autowired
public FileUploadController(StorageService storageService) {
this.storageService = storageService;
}
//After Spring 4.3, we can use @GetMapping instead of @RequestMapping(value = "/", method = RequestMethod.GET)
//@RequestMapping(value = "/", method = RequestMethod.GET)
@GetMapping("/")
public String listUploadedFiles(Model model) throws IOException {
model.addAttribute("files", storageService
.loadAll()
.map(path ->
MvcUriComponentsBuilder
.fromMethodName(FileUploadController.class, "serveFile", path.getFileName().toString())
.build().toString())
.collect(Collectors.toList()));
return "uploadForm";
}
@GetMapping("/files/{filename:.+}")
@ResponseBody
public ResponseEntity<Resource> serveFile(@PathVariable String filename) {
Resource file = storageService.loadAsResource(filename);
return ResponseEntity
.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\""+file.getFilename()+"\"")
.body(file);
}
//After Spring 4.3, we can use @PostMapping instead of @RequestMapping(value = "/", method = RequestMethod.POST)
@PostMapping("/")
public String handleFileUpload(@RequestParam("file") MultipartFile file,
RedirectAttributes redirectAttributes) {
storageService.store(file);
redirectAttributes.addFlashAttribute("message",
"You successfully uploaded " + file.getOriginalFilename() + "!");
return "redirect:/";
}
@ExceptionHandler(StorageFileNotFoundException.class)
@ResponseBody
public ResponseEntity<Void> handleStorageFileNotFound(StorageFileNotFoundException exc) {
System.out.println("Error");
return ResponseEntity.notFound().build();
}
}
這個部份是接受路徑參數 (path variable),透過storageService讀取檔案,然後將檔案內容回傳回去,ResponseEntity將 HTTP headers內容及狀態碼(status code)包起來。ok()就是產生ok的狀態碼,header就是設定header,body就是設定內容。
//After Spring 4.3, we can use @GetMapping instead of @RequestMapping(value = "/files/{filename:.+}", method = RequestMethod.GET)
@GetMapping("/files/{filename:.+}")
@ResponseBody
public ResponseEntity<Resource> serveFile(@PathVariable String filename) {
Resource file = storageService.loadAsResource(filename);
return ResponseEntity
.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\""+file.getFilename()+"\"")
.body(file);
}
這個部份是接受參數,透過storageService儲存檔案。因為使用了redirect,所以,利用redirectAttribute.addFlashAttribute設定attribute(類似ViewAndModel的addObject)。
//After Spring 4.3, we can use @PostMapping instead of @RequestMapping(value = "/", method = RequestMethod.POST)
@PostMapping("/")
public String handleFileUpload(@RequestParam("file") MultipartFile file,
RedirectAttributes redirectAttributes) {
storageService.store(file);
redirectAttributes.addFlashAttribute("message",
"You successfully uploaded " + file.getOriginalFilename() + "!");
return "redirect:/";
}
uploadForm.html,因為要上傳檔案,所以要加上「enctype="multipart/form-data"」及 「type="file"」。如果沒有「enctype="multipart/form-data"」,controller會接收到檔名而不是檔案內容。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div th:if="${message}">
<h2 th:text="${message}"/>
</div>
<div>
<form method="POST" enctype="multipart/form-data" action="/">
<table>
<tr><td>File to upload:</td><td><input type="file" name="file" /></td></tr>
<tr><td></td><td><input type="submit" value="Upload" /></td></tr>
</table>
</form>
</div>
<div>
<ul>
<li th:each="file : ${files}">
<a th:href="${file}" th:text="${file}" />
</li>
</ul>
</div>
</body>
</html>
StorageService.java
package com.example.demo.storage;
import org.springframework.core.io.Resource;
import org.springframework.web.multipart.MultipartFile;
import java.nio.file.Path;
import java.util.stream.Stream;
public interface StorageService {
void init();
void store(MultipartFile file);
Stream<Path> loadAll();
Path load(String filename);
Resource loadAsResource(String filename);
void deleteAll();
}
FileSystemStorageService.java
package com.example.demo.storage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;
import org.springframework.util.FileSystemUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream;
@Service
public class FileSystemStorageService implements StorageService {
private final Path rootLocation;
@Autowired
public FileSystemStorageService(StorageProperties properties) {
this.rootLocation = Paths.get(properties.getLocation());
}
@Override
public void store(MultipartFile file) {
try {
if (file.isEmpty()) {
throw new StorageException("Failed to store empty file " + file.getOriginalFilename());
}
Files.copy(file.getInputStream(), this.rootLocation.resolve(file.getOriginalFilename()));
} catch (IOException e) {
throw new StorageException("Failed to store file " + file.getOriginalFilename(), e);
}
}
@Override
public Stream<Path> loadAll() {
try {
return Files.walk(this.rootLocation, 1)
.filter(path -> !path.equals(this.rootLocation))
.map(path -> this.rootLocation.relativize(path));
} catch (IOException e) {
throw new StorageException("Failed to read stored files", e);
}
}
@Override
public Path load(String filename) {
return rootLocation.resolve(filename);
}
@Override
public Resource loadAsResource(String filename) {
try {
Path file = load(filename);
Resource resource = new UrlResource(file.toUri());
if(resource.exists() || resource.isReadable()) {
return resource;
}
else {
throw new StorageFileNotFoundException("Could not read file: " + filename);
}
} catch (MalformedURLException e) {
throw new StorageFileNotFoundException("Could not read file: " + filename, e);
}
}
@Override
public void deleteAll() {
FileSystemUtils.deleteRecursively(rootLocation.toFile());
}
@Override
public void init() {
try {
Files.createDirectory(rootLocation);
} catch (IOException e) {
throw new StorageException("Could not initialize storage", e);
}
}
}
store是在接受file之後,將收到的file複製一份到目錄中,以原檔名儲存。Java 7以後可以直接用Files.copy複製檔案。這個範例也使用到Path,Path(this.rootLocation)的使用,請參閱http://javapapers.com/java/java-nio-path/。
@Override
public void store(MultipartFile file) {
try {
if (file.isEmpty()) {
throw new StorageException("Failed to store empty file " + file.getOriginalFilename());
}
Files.copy(file.getInputStream(), this.rootLocation.resolve(file.getOriginalFilename()));
} catch (IOException e) {
throw new StorageException("Failed to store file " + file.getOriginalFilename(), e);
}
}
loadAsResource是將檔案轉為urlResource並回傳。這個範例也使用到Path,Path(file)的使用,請參閱http://javapapers.com/java/java-nio-path/。
@Override
public Resource loadAsResource(String filename) {
try {
Path file = load(filename);
Resource resource = new UrlResource(file.toUri());
if(resource.exists() || resource.isReadable()) {
return resource;
}
else {
throw new StorageFileNotFoundException("Could not read file: " + filename);
}
} catch (MalformedURLException e) {
throw new StorageFileNotFoundException("Could not read file: " + filename, e);
}
}
在StorageProperties.java設定檔案的位置 (如: 「upload-dir」)。也也要在專案裡新增上傳路徑 (如:「upload-dir」就是「/upload-dir」)
package com.example.demo.storage;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("storage")
public class StorageProperties {
// we can setup location here
private String location = "upload-dir";
//private String location = "c:\users\upload-dir";
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
}
StorageException.java 設定StorageException是RuntimeException的子類別。
package com.example.demo.storage;
public class StorageException extends RuntimeException {
public StorageException(String message) {
super(message);
}
public StorageException(String message, Throwable cause) {
super(message, cause);
}
}
StorageFileNotFoundException.java設定StorageFileNotFoundException是StorageException的子類別。
package com.example.demo.storage;
public class StorageFileNotFoundException extends StorageException {
public StorageFileNotFoundException(String message) {
super(message);
}
public StorageFileNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}
因為StorageProperties設了@ConfigurationProperties("storage"),所以,在Application中(如:DemoFileUploadApplication.java)就要@EnableConfigurationProperties(StorageProperties.class),如果沒有,就會得到「Parameter 0 of constructor in com.example.storage.FileSystemStorageService required a bean of type 'com.example.storage.StorageProperties' that could not be found.」
的錯誤訊息。
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import com.example.demo.storage.StorageProperties;
@SpringBootApplication
@EnableConfigurationProperties(StorageProperties.class)
public class DemoFileUploadApplication {
public static void main(String[] args) {
SpringApplication.run(DemoFileUploadApplication.class, args);
}
}
*另外,Exception的部份請參考Error handling。
JPA+File upload
如果顧客資料或產品資料要附上照片,要:
- 複製com.example.storage裡所有的檔案(FileSystemStorageService.java、StorageException.java、StorageFileNotFoundException.java、StorageProperties.java、StorageService.java)到現有的JPA專案。
- 在Application裡加「@EnableConfigurationProperties(StorageProperties.class)」(參考DemoFileUploadApplication.java)
- 在資料表中要加上照片檔案名稱的欄位 (如: photo varchar(45)),不建議將照片放到資料表中,因為不能被快取(cache),存取速度會很慢。
- 修改對應的entity (加兩個變數:photoFile、photo及對應的setter及getter),photoFile是為了儲存使用者上傳的檔案,所以,資料型態是MultipartFile,將photoFile設為transient,也就是不將這變數儲存到資料庫。photo則是一般的String,我們會把photoFile的檔案名稱存在這個變數,並將內容存到資料庫。
package com.example.demo.entity;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
//import org.springframework.data.annotation.Transient;
import org.springframework.web.multipart.MultipartFile;
@Entity
@Table(name = "customer")
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
@Column(name = "address")
private String address;
private int weight;
//@Transient
private transient MultipartFile photoFile;
private String photo;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
public MultipartFile getPhotoFile() {
return photoFile;
}
public void setPhotoFile(MultipartFile photoFile) {
this.photoFile = photoFile;
}
public void setPhoto() {
this.photo = photoFile.getOriginalFilename();
}
public String getPhoto() {
return photo;
}
public void setPhoto(String photo) {
this.photo = photo;
}
}
- 修改view (如:customerCreate.html),記得要有「enctype="multipart/form-data"」,否則,controller會接收到檔名而不是檔案內容。
customerCreate.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>Create New Customer</title>
</head>
<body>
<form action="customerCreate" enctype="multipart/form-data" method ="post">
姓名:<input type="text" name="name"/><br/>
地址:<input type="text" name = "address"/><br/>
重量:<input type="text" name ="weight"/><br/>
照片:<input type="file" name="photoFile" /><br/>
<input type="submit" value="Submit"/>
</form>
</body>
</html>
- 修改對應的controller,先autowire StorageService。
@Controller
public class CustomerController {
@Autowired
CustomerDAO dao;
private final StorageService storageService;
@Autowired
public CustomerController(StorageService storageService) {
this.storageService = storageService;
}
- 從view取得PhotoFile,儲存PhotoFile,將PhotoFile的檔名複製到photo,將資料儲存到資料庫。
@RequestMapping(value = "/customerCreate", method = RequestMethod.POST)
public ModelAndView processFormCreate(@ModelAttribute Customer cus) throws SQLException {
ModelAndView model = new ModelAndView("redirect:/customerRetrieveAll");
storageService.store(cus.getPhotoFile());
cus.setPhoto();//copy file name to the field photo
//cus.setPhoto(cus.getPhotoFile().getOriginalFilename());//copy file name to the field photo
dao.save(cus);
model.addObject(cus);
return model;
}
- 要在專案裡新增上傳路徑 (如:「upload-dir」就是「/upload-dir」)
- 記得也要修改update的controller及相關的view
@RequestMapping(value = "/customerUpdate", method = RequestMethod.POST)
public ModelAndView processFormUpdate(@ModelAttribute Customer cus, BindingResult bindingResult) throws SQLException {
ModelAndView model = new ModelAndView("redirect:/customerRetrieveAll");
if (bindingResult.hasErrors()){
System.out.println(bindingResult);
}
//if photo is updated
if (!("").equals(cus.getPhotoFile().getOriginalFilename())){
storageService.store(cus.getPhotoFile());
cus.setPhoto();//copy file name to the field photo
}
dao.save(cus);
return model;
}
updateCustomer.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>Update Customer</title>
</head>
<body>
<form action="customerUpdate" th:object="${customer}" enctype="multipart/form-data" method ="post">
姓名:<input type="text" name="name" th:field="*{name}"/><br/>
地址:<input type="text" name = "address" th:field="*{address}"/><br/>
重量:<input type="text" name ="weight" th:field="*{weight}"/><br/>
照片:<label th:text="*{photo}">photo</label><br/>
<input type="hidden" name="photo" th:field="*{photo}"/><br/>
<input type="file" name="photoFile"/><br/>
<input type="hidden" name ="id" th:field="*{id}"/>
<input type="submit" value="Submit"/>
</form>
</body>
</html>
- 要列出顧客或產品對應的照片,先在controller加入:
@GetMapping("/files/{filename:.+}")
@ResponseBody
public ResponseEntity<Resource> serveFile(@PathVariable String filename) {
Resource file = storageService.loadAsResource(filename);
return ResponseEntity
.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\""+file.getFilename()+"\"")
.body(file);
}
- 修改html就可以利用剛剛加進去的方法取得照片
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>List All Customers</title>
</head>
<body>
<a href="customerCreate">新增顧客</a>
<table>
<tr>
<th>編號</th>
<th>姓名</th>
<th>地址</th>
<th>重量</th>
<th>照片</th>
<th></th>
</tr>
<tr th:each="customer : ${allCustomers} " th:object="${customer}">
<td th:text="*{id}">1</td>
<td th:text="*{name}">Ben</td>
<td th:text="*{address}">Fu Jen</td>
<td th:text="*{weight}">56</td>
<td><img th:src="'files/'+*{photo}" width="100px"/></td>
<td><a th:href="@{customerUpdate(id=*{id})}">修改</a> <a th:href="@{customerDelete(id=*{id})}">刪除</a></td>
</tr>
</table>
</body>
</html>
- Exception的部份請參考Error handling
參考資料
- https://spring.io/guides/gs/uploading-files/
- File I/O
- ResponseEntity
- 上傳多個檔案
- http://www.mkyong.com/spring-boot/spring-boot-file-upload-example/
- REST
- React