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;@Controllerpublic 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;@Servicepublic 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。
如果顧客資料或產品資料要附上照片,要:
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; }}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>@Controllerpublic class CustomerController { @Autowired CustomerDAO dao; private final StorageService storageService; @Autowired public CustomerController(StorageService storageService) { this.storageService = storageService; } @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; } @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> @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); }<!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>