本章带你用Spring Boot创建一个服务器应用,可以通过HTTP的multipart形式来上传文件。
本文目标
用Spring Boot构建一个网络应用,接收文件上传。实现一个简单HTML界面来上传一个测试文件。
你需要
- 15分钟左右
- IntelliJ IDEA
- JDK 1.8+
- Maven 3.2+
用Spring Initializr生成项目代码
对于所有的Spring应用,你都可以使用Spring Initializr生成基本的项目代码。Initializr提供了一个快速的方式去引入所有你需要的依赖,并且为你做了很多设置。当前例子需要Spring Web和Thymeleaf依赖。具体设置如下图:
如上图所示,我们选择了Maven作为编译工具。你也可以选择Gradle来进行编译。然后我们分别把Group和Artifact设置为“com.hansy”和“uploading-files”。
生成的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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.hansy</groupId>
<artifactId>uploading-files</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>uploading-files</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
创建一个Application类
为了开始一个Spring Boot MVC应用,首先需要一个starter。在我们的例子中,spring-boot-starter-thymeleaf和spring-boot-starter-web已经被添加到依赖中了。在原来的Spring中,如果想要使用Servlet容器上传文件,需要注册一个MultipartConfigElement类(通过web.xml中的元素来配置)。但是我们使用的是Spring Boot,这些东西都通过自动配置完成了。你不需要做其他操作,直接使用默认生成的Application类就行,代码如下:
package com.hansy.uploadingfiles;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class UploadingFilesApplication {
public static void main(String[] args) {
SpringApplication.run(UploadingFilesApplication.class, args);
}
}
作为自动配置Spring MVC的一部分,Spring Boot会创建一个MultipartConfigElement类型的Bean,为文件上传做准备。
创建一个文件上传Controller
涉及储存和加载磁盘上的上传文件的类比较多,都在com.hansy.uploadingfiles.storage包下面,可以下载源码进行查看。相关的类会在新创建的FileUploadController里面使用。代码如下:
package com.hansy.uploadingfiles;
import com.hansy.uploadingfiles.storage.StorageFileNotFoundException;
import com.hansy.uploadingfiles.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.util.stream.Collectors;
@Controller
public class FileUploadController {
private final StorageService storageService;
@Autowired
public FileUploadController(StorageService storageService) {
this.storageService = storageService;
}
@GetMapping("/")
public String listUploadFiles(Model model) {
model.addAttribute("files", storageService.loadAll().map(path -> {
return MvcUriComponentsBuilder.fromMethodName(FileUploadController.class, "serveFile",
path.getFileName().toString()).build().toUri().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(value = "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 exception) {
return ResponseEntity.notFound().build();
}
}
FileUploadController类通过添加@Controller注解,使得Spring MVC可以找到这个控制器,然后在控制器里面查找定义的路由。每一个标记了@GetMapping或者@PostMapping的方法都绑定了一个路径和对应的HTTP动作。
在本里中:
- GET /:通过StorageService查找当前已经上传的文件列表,然后在Thymeleaf模板里面显示。使用MvcUriComponentsBuilder生成每个文件的链接。
- GET /files/{filename}:加载该文件资源(如果文件存在的话),然后发送到浏览器,使用Content-Disposition响应头来下载该文件。
- POST /:处理一个multi-part文件,通过StorageService保存到服务器。
在生产环境,我们通常会把文件储存在一个临时目录、一个数据库、或者可能是一个NoSQL仓库(比如Mongo’s GridFS)。最好不要使用你的应用的文件系统储存内容。
我们需要提供一个StorageService,使得控制器可以访问存储层(比如文件系统)。代码如下:
package com.hansy.uploadingfiles.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();
}
创建一个HTML模板
接下来的Thymeleaf模板(src/main/resources/templates/uploadForm.html)展示了一个例子,怎么上传文件和显示已经上传的文件:
<html xmlns:th="https://www.thymeleaf.org">
<body>
<div th:if="${message}">
<h2 th:text="${message}"></h2>
</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>
这个模板包含三部分:
- 一个页面顶部的可选消息
- 一个可以让用户上传文件的form表单
- 一个后台提供的文件列表
设置文件上传的限制
配置文件上传参数时,最有用的就是限制文件的大小。可以想象一下处理一个5GB大小的文件!利用Spring Boot,我们可以用一些配置参数来设置自动配置的MultipartConfigElement。
添加如下参数到配置文件里面(src/main/resources/application.properties):
spring.servlet.multipart.max-file-size=128KB
spring.servlet.multipart.max-request-size=128KB
上面设置的multipart约束的作用如下:
- spring.http.multipart.max-file-size:设置为128KB,就是限制文件大小不能超过128KB;
- spring.http.multipart.max-request-size:设置为128KB,就是限制一个multipart/form-data请求的大小不能超过128KB;
修改Application类
保存文件需要一个目标文件夹,所以我们需要修改一下Spring Initializr默认生成的UploadingFilesApplication类。添加一个CommandLineRunner,在启动的时候删除并重新创建上传文件夹。代码如下:
package com.hansy.uploadingfiles;
import com.hansy.uploadingfiles.storage.StorageProperties;
import com.hansy.uploadingfiles.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 UploadingFilesApplication {
public static void main(String[] args) {
SpringApplication.run(UploadingFilesApplication.class, args);
}
@Bean
public CommandLineRunner init(StorageService storageService) {
return args -> {
storageService.deleteAll();
storageService.init();
};
}
}
运行程序
启动程序,打开浏览器,输入网址http://localhost:8080/,可以看到一个上传的表单页面。选择一个比较小的文件,点击Upload按钮。你将会看到一个上传成功的提示。
成功的提示信息类似于下面这样:
“You successfully uploaded <你的文件名字>!”
如果你上传的文件体积过大,你将会看到一个错误页面。
测试程序
测试程序的文件上传功能可以采用多种方案。下面的代码(src/test/java/com/hansy/uploadingfiles/FileUploadTests.java)展示了一个例子,使用MockMvc而不用启动servlet容器就能完成测试:
package com.hansy.uploadingfiles;
import com.hansy.uploadingfiles.storage.StorageFileNotFoundException;
import com.hansy.uploadingfiles.storage.StorageService;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import org.mockito.BDDMockito;
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.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import java.nio.file.Paths;
import java.util.stream.Stream;
@AutoConfigureMockMvc
@SpringBootTest
public class FileUploadTests {
@Autowired
private MockMvc mockMvc;
@MockBean
private StorageService storageService;
@Test
public void shouldListAllFiles() throws Exception {
BDDMockito.given(storageService.loadAll()).
willReturn(Stream.of(Paths.get("first.txt"), Paths.get("second.txt")));
mockMvc.perform(
MockMvcRequestBuilders.get("/")).
andExpect(MockMvcResultMatchers.status().isOk()).
andExpect(MockMvcResultMatchers.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());
mockMvc.perform(MockMvcRequestBuilders.multipart("/").file(multipartFile)).
andExpect(MockMvcResultMatchers.status().isFound()).
andExpect(MockMvcResultMatchers.header().string("Location", "/"));
BDDMockito.then(storageService).should().store(multipartFile);
}
@Test
public void should404WhenMissingFile() throws Exception {
BDDMockito.given(storageService.loadAsResource("test.txt")).willThrow(StorageFileNotFoundException.class);
mockMvc.perform(MockMvcRequestBuilders.get("/files/test.txt")).
andExpect(MockMvcResultMatchers.status().isNotFound());
}
}
在这些测试中,我们使用多种Mock类去跟控制器和StorageService进行交互。还使用了MockMultipartFile类去跟Servlet容器进行交互。
还有一个集成测试的例子,请查看FileUploadIntegrationTests(在src/test/java/com/hansy/uploadingfiles下面)
小结
你已经用Spring Boot开发了一个处理文件上传的Web应用。