Spring系列——上传文件
来源:互联网 发布:怎么评价杨振宁知乎 编辑:程序博客网 时间:2024/06/07 15:52
原文链接:http://www.dubby.cn/detail.html?id=9043
1. 我们要做什么
使用Spring实现一个最基本的文件上传,文件下载的web应用。
2.你需要什么
- MySQL 5.6或者更高版本
- 大约15分钟
- 一个最爱的编辑器或者IDE
- JDK 1.8 +
- Maven 3.0+
3. 创建项目
3.1 项目依赖
pom.xml
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>cn.dubby</groupId> <artifactId>uploading-files</artifactId> <version>0.1.0</version> <!--原文链接:www.dubby.cn--> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.8.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <properties> <java.version>1.8</java.version> </properties> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>
从依赖中看到加了spring-boot-starter-thymeleaf
的starter,这里的目的是用thymeleaf
渲染一个页面来上传文件和展示已上传的文件列表,真实使用时完全可以不用。
借鉴本文时,请注意根据实际情况来改造,比如,1. 你们可能不需要有下载的controller,因为完全可以把上传路径设为静态服务器目录下,比如css,png文件可以直接让前段请求URL来获取,毕竟静态资源服务器(如:Nginx)的处理效率肯定比Java更高效;2. 可以考虑使用很多云厂商的OSS或者CDN来代替本地文件系统,只需要自己实现存储的service;其他……
3.2 启动类
src/main/java/hello/Application.java
package hello;import hello.storage.StorageProperties;import hello.storage.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 Application { //原文链接:www.dubby.cn public static void main(String[] args) { SpringApplication.run(Application.class, args); } @Bean CommandLineRunner init(StorageService storageService) { return (args) -> { storageService.deleteAll(); storageService.init(); }; }}
如果是传统的servlet项目,需要在web.xml
里配置<multipart-config>
来注册MultipartConfigElement
。但是Spring帮我自动配置了。
3.3 controller
src/main/java/hello/FileUploadController.java
package hello;import java.io.IOException;import java.util.stream.Collectors;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.ExceptionHandler;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.ResponseBody;import org.springframework.web.multipart.MultipartFile;import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;import org.springframework.web.servlet.mvc.support.RedirectAttributes;import hello.storage.StorageFileNotFoundException;import hello.storage.StorageService;@Controllerpublic class FileUploadController { private final StorageService storageService; @Autowired public FileUploadController(StorageService storageService) { this.storageService = storageService; } @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); } @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) public ResponseEntity<?> handleStorageFileNotFound(StorageFileNotFoundException exc) { return ResponseEntity.notFound().build(); }}
这个例子,没有用@RestController
,所以返回的并不是JSON
。@GetMapping
和@PostMapping
的意义之前有介绍过,这里就不再赘述了,如果不清楚可以看之前的[Spring系列——RESTful的web项目](http://www.dubby.cn/detail.html?id=9040#3.3 controller)。
这个例子中,每个URI的意义分别是:
GET /
:显示一个页面,里面可以上传文件,还展示已经上传的文件的列表。GET /files/{filename}
:如果文件存在的话,下载这个文件。注意使用了Content-Disposition
这个Header。POST /
:上传一个文件。
如果读者时初学者,可能对上面的代码还存有一点疑惑,我这里简单描述一下,1.
@GetMapping("/files/{filename:.+}")
这个时Spring MVC的一个URI匹配的方式,格式是PathVariable:正则表达式
,用后面的正则来校验这个参数;2.@ExceptionHandler(StorageFileNotFoundException.class)
这是Spring提供的异常处理器,如果请求发生指定的异常就会被捕获。
3.4 文件存储
这里定义一个接口,上面也提到了,文件存储不建议使用本地存储的方式,所以使用接口,读者可以随意替换成你需要的,只需要自己实现这个接口就可以了。
src/main/java/hello/storage/StorageService.java
package hello.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();}
这里给出本地存储服务的代码
src/main/java/hello/storage/FileSystemStorageService.java
package hello.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.util.StringUtils;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.nio.file.StandardCopyOption;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) { String filename = StringUtils.cleanPath(file.getOriginalFilename()); try { if (file.isEmpty()) { throw new StorageException("Failed to store empty file " + filename); } if (filename.contains("..")) { // This is a security check throw new StorageException( "Cannot store file with relative path outside current directory " + filename); } Files.copy(file.getInputStream(), this.rootLocation.resolve(filename), StandardCopyOption.REPLACE_EXISTING); } catch (IOException e) { throw new StorageException("Failed to store file " + filename, 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.createDirectories(rootLocation); } catch (IOException e) { throw new StorageException("Could not initialize storage", e); } }}
3.5 HTML模板
src/main/resources/templates/uploadForm.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>
这个HTML模板有三个部分:
- 一个message的attribute展示信息。
- 一个表单,用来上传文件。
- 一个列表来展示已上传的文件列表。
3.6 文件的大小限制
src/main/resources/application.properties
spring.http.multipart.max-file-size=128KBspring.http.multipart.max-request-size=128KB
这个配置的意思是:
spring.http.multipart.max-file-size
限制了文件的大小。spring.http.multipart.max-request-size
限制了请求的大小。
4. 测试
4.1 动手测一测
4.2 单元测试
这里给一点测试代码
src/test/java/hello/storage/FileUploadTests.java
package hello;import hello.storage.StorageFileNotFoundException;import hello.storage.StorageService;import org.hamcrest.Matchers;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.boot.test.mock.mockito.MockBean;import org.springframework.mock.web.MockMultipartFile;import org.springframework.test.context.junit4.SpringRunner;import org.springframework.test.web.servlet.MockMvc;import java.nio.file.Paths;import java.util.stream.Stream;import static org.mockito.BDDMockito.given;import static org.mockito.BDDMockito.then;import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload;import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;@RunWith(SpringRunner.class)@AutoConfigureMockMvc@SpringBootTestpublic class FileUploadTests { @Autowired private MockMvc mvc; @MockBean private StorageService storageService; @Test public void shouldListAllFiles() throws Exception { given(this.storageService.loadAll()) .willReturn(Stream.of(Paths.get("first.txt"), Paths.get("second.txt"))); this.mvc.perform(get("/")).andExpect(status().isOk()) .andExpect(model().attribute("files", Matchers.contains("http://localhost/files/first.txt", "http://localhost/files/second.txt"))); } @Test public void shouldSaveUploadedFile() throws Exception { MockMultipartFile multipartFile = new MockMultipartFile("file", "test.txt", "text/plain", "Spring Framework".getBytes()); this.mvc.perform(fileUpload("/").file(multipartFile)) .andExpect(status().isFound()) .andExpect(header().string("Location", "/")); then(this.storageService).should().store(multipartFile); } @SuppressWarnings("unchecked") @Test public void should404WhenMissingFile() throws Exception { given(this.storageService.loadAsResource("test.txt")) .willThrow(StorageFileNotFoundException.class); this.mvc.perform(get("/files/test.txt")).andExpect(status().isNotFound()); }}
完整代码地址:https://github.com/dubby1994/spring-demo
微信扫码关注微信订阅号
- Spring系列——上传文件
- Spring mvc系列七之 文件上传
- Spring mvc系列七之 文件上传
- 【Spring MVC】——普通文件上传
- 【spring mvc】——fastdfs文件上传
- springMVC系列之文件上传——06
- JS组件系列——Bootstrap文件上传组件:bootstrap
- Spring MVC 系列(五)——Spring MVC上传功能源码
- Spring MVC 系列(五)——Spring MVC上传功能源码
- spring mvc 实现任意文件上传—— 下载<二>
- WebService体系之——CXF+SPRING文件上传
- spring MVC笔记4——文件上传
- WebService体系之——CXF+SPRING文件上传
- Spring Boot入门——文件上传与下载
- Spring Boot——多文件上传大小超限问题解决
- spring实现文件上传
- Spring 文件上传
- spring上传文件
- java多线程
- Ubuntu16.04中安装ROS Kinetic
- 数据库中常用的增删改查,多表查询
- python递归函数和列表使用
- 矩阵的运算及其运算规则
- Spring系列——上传文件
- 饿了么webapp之css footer的实现
- 他们三人都答对的题目至少有多少?
- 初学python(一)
- Java线程基础-CountDownLatch-批量执行多线程完成,再由主线程发起
- java synchronized修饰方法和修饰方法块
- Xamarin.Forms 用户界面——控件——View
- struts1配置文件中action的常用属性
- hdu3460 Ancient Printer(字典树)