Spring Boot上傳文件,根據官方uploadfile示例修改的,能夠打成war放到服務器上(筆者使用的是Tomcat).主要步驟是建立異常類,屬性類,接口類與控制器類,最後進行少許修改打包部署到服務器上.html
選擇spring initializer:java
改一下包名,打包選項這裏能夠jar能夠war,選jar的話能夠在build的時候再生成war.git
這裏用的是模板引擎Thymeleaf,選擇spring web與Thymeleaf.github
最後點擊finish.web
4個包,service,properties,controller,exception.正則表達式
處理兩個異常,分別是存儲異常與存儲文件找不到異常.spring
package kr.test.exception; public class StorageException extends RuntimeException { public StorageException(String message) { super(message); } public StorageException(String message,Throwable cause) { super(message,cause); } }
package kr.test.exception; public class StorageFileNotFoundException extends StorageException { public StorageFileNotFoundException(String message) { super(message); } public StorageFileNotFoundException(String message,Throwable cause) { super(message,cause); } }
Exception(String message,Throwable cause);
這個構造函數中的cause是引發這個異常的異常,容許空值,若是是空值則表示這個引發這個異常的異常不存在或者未知.json
新建StorageProperties.java,設定存儲文件的位置,就是location的值,可使用"../../"這樣的值,什麼也不加的話會在項目路徑下新建文件夾,如有同名的文件夾會被刪除再從新建立.瀏覽器
注意一下權限的問題,後面部署到Tomcat上面時可能會由於沒有寫權限而不能寫入文件,要確保文件夾擁有寫權限.tomcat
package kr.test.properties; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties("storage") public class StorageProperties { private String location = "upload_dir"; public String getLocation() { return location; } public void setLocation(String location) { this.location = location; } }
這裏使用@ConfigurationProperties會報紅,提示沒有@EnableConfigurationProperties:
能夠先無論,後面會在Main類中添加@EnableConfigurationProperties(StorageProperties.class).
先加一個StorageService接口:
package kr.test.service; 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實現該接口:
package kr.test.service; import kr.test.exception.StorageException; import kr.test.exception.StorageFileNotFoundException; import kr.test.properties.StorageProperties; 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.util.StringUtils; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.stream.Stream; @Service public class FileSystemStroageService implements StorageService { private final Path rootLocation; @Autowired public FileSystemStroageService(StorageProperties properties) { this.rootLocation = Paths.get(properties.getLocation()); } @Override public void init() { try { Files.createDirectories(rootLocation); } catch (IOException e) { throw new StorageException("Could not initialize storage",e); } } @Override public void deleteAll() { FileSystemUtils.deleteRecursively(rootLocation.toFile()); } @Override public Path load(String filename) { return rootLocation.resolve(filename); } @Override public Stream<Path> loadAll() { try { return Files.walk(rootLocation,1) .filter(path -> !path.equals(rootLocation)) .map(rootLocation::relativize); } catch (IOException e) { throw new StorageException("Failed to read stored file.",e); } } @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 store(MultipartFile file) { String filename = StringUtils.cleanPath(file.getOriginalFilename()); try { if(file.isEmpty()) { throw new StorageException("Failed to store empty file : "+filename); } if(filename.contains("..")) { throw new StorageException("Cannot store file with relative path outside current directory"+filename); } try(InputStream inputStream = file.getInputStream()) { Files.copy(inputStream,rootLocation.resolve(filename), StandardCopyOption.REPLACE_EXISTING); } } catch (IOException e) { throw new StorageException("Failed to store file : "+ filename,e); } } }
@Override public void init() { try { Files.createDirectories(rootLocation); } catch (IOException e) { throw new StorageException("Could not initialize storage",e); } }
使用java.nio.file.Files.createDirectories()建立存儲目錄,能夠創建多級目錄.
@Override public void deleteAll() { FileSystemUtils.deleteRecursively(rootLocation.toFile()); }
使用工具類FileSystemUtils的方法遞歸刪除文件與文件夾.參數是一個File. 下面是方法源碼:
public static boolean deleteRecursively(File root) { if (root != null && root.exists()) { if (root.isDirectory()) { File[] children = root.listFiles(); if (children != null) { for (File child : children) { deleteRecursively(child); } } } return root.delete(); } return false; }
首先判斷根是否爲空,不爲空的話判斷是不是目錄,不是目錄的話直接刪除,是目錄的話,利用listFiles()獲取全部文件及文件夾,判斷是否爲空並進行遞歸刪除.
@Override public Path load(String filename) { return rootLocation.resolve(filename); }
Path.resolve(String)返回相對於this的路徑,具體來講,等於執行
cd rootLocation cd filename pwd
返回pwd的值.
@Override public Stream<Path> loadAll() { try { return Files.walk(rootLocation,1) .filter(path -> !path.equals(rootLocation)) .map(rootLocation::relativize); } catch (IOException e) { throw new StorageException("Failed to read stored file.",e); } }
Files.walk遍歷目錄,返回一個Stream<Path>,返回的Stream包含打開的一個或多個目錄的引用,會在Stream關閉時關閉,第二個參數1表示遍歷的最大深度.
而後對這個Stream進行filter過濾,這裏是把與rootLocation不相等的Path留下,注意是不相等,就是留下filter()中條件爲真的Path,不是把條件爲真的Path給"刪去".
最後進行map,relativize返回參數相對於調用者的路徑,這裏是返回Stream中的每一個Path相對於rootLocation的路徑. 對於relativize,不管什麼狀況下:
Path a = xxxx; Path b = xxxx;
都有
a.relativize(a.resolve(b)).equals(b)
爲真.
@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); } }
這裏的Resource是org.springframework.core.io.Resource,是一個接口,能夠經過它訪問各類資源,實現類有UrlResource,InputStreamResource等,這裏利用Path.toUri()把file轉換爲Resource後,判斷這個源是否存在或者是否可讀並返回,不然拋出存儲文件找不到異常.
@Override public void store(MultipartFile file) { String filename = StringUtils.cleanPath(file.getOriginalFilename()); try { if(file.isEmpty()) { throw new StorageException("Failed to store empty file : "+filename); } if(filename.contains("..")) { throw new StorageException("Cannot store file with relative path outside current directory"+filename); } try(InputStream inputStream = file.getInputStream()) { Files.copy(inputStream,rootLocation.resolve(filename), StandardCopyOption.REPLACE_EXISTING); } } catch (IOException e) { throw new StorageException("Failed to store file : "+ filename,e); }
getOriginalFilename()獲取文件原名字,而後經過StringUtils.cleanPath()將其標準化,.處理掉"."與"..",而後判斷文件是否爲空與是否包含相對路徑,沒有的話利用Files.copy()進行復制,resolve獲取filename相對於rootLocation的值,複製選項是REPLACE_EXISTING. StandardCopyOption有三個可選值:
新建FileUploadController.
package kr.test.controller; import kr.test.exception.StorageFileNotFoundException; import kr.test.service.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.util.stream.Collectors; @Controller public class FileUploadController { private final StorageService storageService; @Autowired public FileUploadController(StorageService storageService) { this.storageService = storageService; } @GetMapping("/") public String listUploadedFiles(Model model) { 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); } @PostMapping("/") public String handleFileUpload(@RequestParam("file") MultipartFile file, RedirectAttributes redirectAttributes) { storageService.store(file); redirectAttributes.addFlashAttribute("message","You successully uploaded "+file.getOriginalFilename()+"!"); return "redirect:/"; } @ExceptionHandler(StorageFileNotFoundException.class) public ResponseEntity<?> handleStorageFileNotFound(StorageFileNotFoundException e) { return ResponseEntity.notFound().build(); } }
@GetMapping("/") public String listUploadedFiles(Model model) { model.addAttribute("files",storageService.loadAll().map( path -> MvcUriComponentsBuilder.fromMethodName(FileUploadController.class, "serveFile",path.getFileName().toString()).build().toString()) .collect(Collectors.toList())); return "uploadForm"; }
@GetMapping是@RequestMapping(method = RequestMethod.GET)的簡化寫法,將HTTP GET路徑映射到特定的處理方法上. 方法的參數是spring MVC中的Model,Model實質上是一個Map,添加的key能夠在視圖中用${key}獲取值,好比,這裏添加了"files"做爲key,則在視圖中可用 ${files}獲取值.
MvcUriComponentsBuilder能夠爲Controller指定uri,fromMethod簡單地說就是會調用FileUploadController的serveFile(),參數是path.getFileName().toString(),因爲serveFile()返回的是Stream<Path>,利用Stream的collect將其轉換成List添加到model中,而後返回uploadForm,表示這是視圖的名稱,會到resource/templates下尋找.
這裏說一下RequestMapping與Model:
能夠用@RequestMapping()來映射URL,能夠映射到某個類或某個具體方法.@RequestMapping經常使用的有如下屬性:
Spring提供了簡化的@RequestMapping,提供了新的註解來標識HTTP方法:
因此這裏的@GetMapping是簡化了的@RequestMapping.
能夠向Model添加視圖所須要的變量,Model主要有如下方法:
Model addAttribute(Object value); Model addAttribute(String name,Object value); Model addAllAttributes(Map attributes); Model addAllAttributes(Collection<?> attributes); Model mergeAttributes(Map attributes); boolean containAttribute(String name);
addAttribute()添加一個變量,對於兩個參數的,使用name做爲變量名稱,後面的是值,對於只有一個Object的,變量的名字就是類名字首字母小寫後轉爲的java變量. addAttributes()添加多個變量,若是變量存在則覆蓋,其中參數爲Collection<?>的方法添加變量名時與addAttribute(Object)的命名規範相似. mergeAttributes()也是添加多個變量,不過變量已存在的話會忽略. containAttributte()判斷是否存在變量.
@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); }
這裏的@GetMapping用來表示顯示的用來供下載的文件名,@ResponseBody表示直接返回內容而不是視圖名,由於默認返回的是視圖名稱,@ResponseBody對於String直接返回,不然默認使用Jackson進行序列化.
@PathVariable表示這是@GetMapping中的參數的值,能夠省略,默認同名,就是形參的名字與GetMapping中的名字同樣,從中取值賦給形參,經過filename加載資源後,做爲ResponseEntity的請求體. ResponseEntity從HttpEntity繼承而來,ResponseEntity.ok()是一個靜態方法,表示構建一個狀態爲"ok"的ResponseEntity,而後添加請求頭.
HttpHeaders.CONTENT_DISPOSITION,"attachment;filename=\""+file.getFilename()+"\""
content_disposition表示文件是直接在瀏覽器打開仍是下載,attachment表示是要下載,文件名爲file.getFilename().
@PostMapping("/") public String handleFileUpload(@RequestParam("file") MultipartFile file,RedirectAttributes redirectAttributes) { storageService.store(file); redirectAttributes.addFlashAttribute("message","You successully uploaded "+file.getOriginalFilename()+"!"); return "redirect:/"; }
@PostMapping()與@GetMapping()相似,只不過方法不是GET而是POST.@RequestParam表示請求參數,裏面的是請求參數的名字,使用MultipartFile來處理文件上傳. RedirectAttributes是用於重定向使用的,能夠附帶參數,RedirectAttributes有兩種帶參的形式:
addAttribute(String name,Object value); addFlashAttribute(String name,Object value);
addAttribute()至關於直接在重定向的地址添加
name=value
這樣的形式,會將參數暴露在重定向的地址上.
而addFlashAttribute()隱藏了參數,只能在重定向的頁面中獲取參數的值,用到了session,session跳轉到頁面後就會刪除對象. handleFileUpload首先保存文件,而後添加一個保存成功的信息,因爲Controller中重定向能夠返回以"redirect:"或以"forward:"爲前綴的URI,所以返回"redirect:/",重定向到根.
@ExceptionHandler(StorageFileNotFoundException.class) public ResponseEntity<?> handleStorageFileNotFound(StorageFileNotFoundException e) { return ResponseEntity.notFound().build(); }
@ExceptionHandler()註解會處理Controller層拋出的全部StorageFileNotFoundException類及其子類的異常,ResponseEntity.notFound()至關於返回404標識碼.
package kr.test; import kr.test.properties.StorageProperties; import kr.test.service.StorageService; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; @SpringBootApplication @EnableConfigurationProperties(StorageProperties.class) public class TestApplication { public static void main(String[] args) { SpringApplication.run(TestApplication.class, args); } @Bean CommandLineRunner init(StorageService storageService) { return (args) -> { storageService.deleteAll(); storageService.init(); }; } }
在原來的基礎上添加
@EnableConfigurationProperties(StorageProperties.class)
與
@Bean CommandLineRunner init(StorageService storageService) { return (args) -> { storageService.deleteAll(); storageService.init(); }; }
@EnableConfigurationProperties能夠爲帶有@ConfigurationProperties註解的Bean提供有效的支持,將帶有@Configuration註解的類注入爲Spring的Bean,在這裏是使StorageProperties的@ConfigurationProperties生效,若是沒有這一行會報紅:
@Bean標註在方法上,等價於spring的xml配置文件的<bean>,註冊bean對象. CommandLineRunner接口用於應用初始化後去執行一段代碼邏輯,這段代碼在整個應用週期只執行一次.
這裏能夠設置一些環境配置屬性,Spring Boot容許準備多個配置文件,在部署時能夠指定那個配置文件覆蓋默認的application.properties.這裏是有關上傳文件的設置:
默認以下:
spring.servlet.multipart.enabled=true spring.servlet.multipart.file-size-threshold=0 spring.servlet.multipart.location= spring.servlet.multipart.max-file-size=1MB spring.servlet.multipart.max-request-size=10MB spring.servlet.multipart.resolve-lazily=false
enabled表示容許上傳,file-size-threshold表示上傳文件超過必定長度就先寫入臨時文件,單位MB或KB,location是臨時文件存放目錄,不設定的話使用web服務器提供的臨時目錄.max-file-size表示單個文件最大長度,默認1MB,max-request-size爲單次HTTP請求上傳的最大長度,默認10MB,resolve-lazily表示文件和參數被訪問的時候再解析成文件.
在這裏只需把max-size調大一點便可.
這是在本地進行的測試.直接在IDE上點擊運行應用,而後打開瀏覽器輸入:
localhost:8080
Spring Boot一般打成jar包或war包,這裏部署到Tomcat上的是打成war包.
pom.xml中,<packaing>改爲war:
Spring Boot默認自帶了一個嵌入式的Tomcat,須要把Tomcat依賴方式改成provided. pom.xml中,在<dependencies>添加:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <scope>provided</scope> </dependency>
修改Main類,讓其繼承SpringBootServletInitializer,重載configure(),同時main()保持不變.
@SpringBootApplication public class MainClass extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application.sources(MainClass.class); } //main()不變 }
這個很重要,設置不當的話就沒法訪問了,主要就是四個路徑:
這個是絕對路徑,要加上/war項目名.
/war項目名/上傳路徑名
好比這裏war項目名是kr,上傳路徑名是upload.
這個是相對路徑,相對於當前項目的路徑,不用加上/war項目名.
/上傳路徑名
這裏是upload.
與@GetMapping同樣,上傳路徑名.
/上傳路徑名
這個是返回的重定向的路徑名,相對路徑,與上兩個同樣,也是上傳路徑名.
/上傳路徑名
在<build>中添加<finalName>,指定打包出來的war名,注意這個要與上面的war項目名同樣,這裏設置的是kr.
運行
mvn package
便可打包,對於IDEA,能夠在IDEA右側欄的Maven中,打開Lifecycle,選擇package:
打包後的war默認放在target下,名字默認爲<artifactId>+<version>.
上傳的話筆者用的是密鑰認證的scp:
scp -i xxxx\id_rsa kr.war username@ip:/usr/local/tomcat/webapps
放到服務器的Tomcat下的webapps目錄.
進入到Tomcat目錄的bin下:
cd /usr/local/tomcat/bin ./startup.sh
若是正在運行的話就不用啓動了,由於會自動檢測到webapps目錄的變化,把新的war自動解包.
略,與本地測試相似,不過要注意的是上傳的文件夾是在tomcat/bin下,想要修改的話能夠修改StorageProperties的location.