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.