开篇词
该指南将引导你完成创建可以接收 HTTP 文件上传的应用。
你将创建的应用
我们将创建一个接受文件上传的 SpringBoot Web 应用。我们还将构建一个简单的 HTML 界面来上传测试文件。
你将需要的工具
-
大概 15 分钟左右;
-
你最喜欢的文本编辑器或集成开发环境(IDE)
-
JDK 1.8 或更高版本;
-
Gradle 4+ 或 Maven 3.2+
-
你还可以将代码直接导入到 IDE 中:
-
Spring Too Suite (STS)
- IntelliJ IDEA
如何完成这个指南
像大多数的 Spring 入门指南一样,你可以从头开始并完成每个步骤,也可以绕过你已经熟悉的基本设置步骤。如论哪种方式,你最终都有可以工作的代码。
-
要从头开始,移步至从 Spring Initializr 开始;
-
要跳过基础,执行以下操作:
-
下载并解压缩该指南将用到的源代码,或借助 Git 来对其进行克隆操作:git clone https://github.com/spring-guides/gs-uploading-files.git
- 切换至 gs-uploading-files/initial 目录;
- 跳转至该指南的创建应用类。
待一切就绪后,可以检查一下 gs-uploading-files/complete 目录中的代码。
从 Spring Initializr 开始
对于所有的 Spring 应用来说,你应该从 Spring Initializr 开始。Initializr 提供了一种快速的方法来提取应用程序所需的依赖,并为你完成许多设置。该示例需要 Spring Web 以及 Thymeleaf 依赖。下图显示了此示例项目的 Initializr 设置:
上图显示了选择 Maven 作为构建工具的 Initializr。你也可以使用 Gradle。它还将 com.example 和 uploading-files 的值分别显示为 Group 和 Artifact。在本示例的其余部分,将用到这些值。
以下清单显示了选择 Maven 时创建的 pom.xml 文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55 1<?xml version="1.0" encoding="UTF-8"?>
2<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4 <modelVersion>4.0.0</modelVersion>
5 <parent>
6 <groupId>org.springframework.boot</groupId>
7 <artifactId>spring-boot-starter-parent</artifactId>
8 <version>2.2.0.RELEASE</version>
9 <relativePath/> <!-- lookup parent from repository -->
10 </parent>
11 <groupId>com.example</groupId>
12 <artifactId>uploading-files</artifactId>
13 <version>0.0.1-SNAPSHOT</version>
14 <name>uploading-files</name>
15 <description>Demo project for Spring Boot</description>
16
17 <properties>
18 <java.version>1.8</java.version>
19 </properties>
20
21 <dependencies>
22 <dependency>
23 <groupId>org.springframework.boot</groupId>
24 <artifactId>spring-boot-starter-thymeleaf</artifactId>
25 </dependency>
26 <dependency>
27 <groupId>org.springframework.boot</groupId>
28 <artifactId>spring-boot-starter-web</artifactId>
29 </dependency>
30
31 <dependency>
32 <groupId>org.springframework.boot</groupId>
33 <artifactId>spring-boot-starter-test</artifactId>
34 <scope>test</scope>
35 <exclusions>
36 <exclusion>
37 <groupId>org.junit.vintage</groupId>
38 <artifactId>junit-vintage-engine</artifactId>
39 </exclusion>
40 </exclusions>
41 </dependency>
42 </dependencies>
43
44 <build>
45 <plugins>
46 <plugin>
47 <groupId>org.springframework.boot</groupId>
48 <artifactId>spring-boot-maven-plugin</artifactId>
49 </plugin>
50 </plugins>
51 </build>
52
53</project>
54
55
以下清单显示了在选择 Gradle 时创建的 build.gradle 文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 1plugins {
2 id 'org.springframework.boot' version '2.2.0.RELEASE'
3 id 'io.spring.dependency-management' version '1.0.8.RELEASE'
4 id 'java'
5}
6
7group = 'com.example'
8version = '0.0.1-SNAPSHOT'
9sourceCompatibility = '1.8'
10
11repositories {
12 mavenCentral()
13}
14
15dependencies {
16 implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
17 implementation 'org.springframework.boot:spring-boot-starter-web'
18 testImplementation('org.springframework.boot:spring-boot-starter-test') {
19 exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
20 }
21}
22
23test {
24 useJUnitPlatform()
25}
26
27
创建应用类
要启动 Spring Boot MVC 应用,首先需要一个启动器。该示例中已经添加了 spring-boot-starter-thymeleaf 以及 spring-boot-starter-web 作为依赖。要使用 Servlet 容器上传文件,我们需要注册一个 MultipartConfigElement 类(在 web.xml 中为 <multipart-config>)。借助 Spring Boot,一切都帮我们配置好了!
开始使用该应用所需的就是下面的 UploadingFilesApplication 类(来自 src/main/java/com/example/uploadingfiles/UploadingFilesApplication.java):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 1package com.example.uploadingfiles;
2
3import org.springframework.boot.SpringApplication;
4import org.springframework.boot.autoconfigure.SpringBootApplication;
5
6@SpringBootApplication
7public class UploadingFilesApplication {
8
9 public static void main(String[] args) {
10 SpringApplication.run(UploadingFilesApplication.class, args);
11 }
12
13}
14
15
作为自动配置 Spring MVC 的一部分,Spring Boot 将创建一个 MultipartConfigElement bean,并准备好进行文件上传。
创建文件上传控制器
初始应用已包含一些类,用于处理磁盘上存储的和加载上传的文件。它们都位于 com.example.uploadingfiles.storage 包中。我们将在新的 FileUploadController 中使用它们。以下清单(来自 src/main/java/com/example/uploadingfiles/FileUploadController.java)显示了文件上传控制器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73 1package com.example.uploadingfiles;
2
3import java.io.IOException;
4import java.util.stream.Collectors;
5
6import org.springframework.beans.factory.annotation.Autowired;
7import org.springframework.core.io.Resource;
8import org.springframework.http.HttpHeaders;
9import org.springframework.http.ResponseEntity;
10import org.springframework.stereotype.Controller;
11import org.springframework.ui.Model;
12import org.springframework.web.bind.annotation.ExceptionHandler;
13import org.springframework.web.bind.annotation.GetMapping;
14import org.springframework.web.bind.annotation.PathVariable;
15import org.springframework.web.bind.annotation.PostMapping;
16import org.springframework.web.bind.annotation.RequestParam;
17import org.springframework.web.bind.annotation.ResponseBody;
18import org.springframework.web.multipart.MultipartFile;
19import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
20import org.springframework.web.servlet.mvc.support.RedirectAttributes;
21
22import com.example.uploadingfiles.storage.StorageFileNotFoundException;
23import com.example.uploadingfiles.storage.StorageService;
24
25@Controller
26public class FileUploadController {
27
28 private final StorageService storageService;
29
30 @Autowired
31 public FileUploadController(StorageService storageService) {
32 this.storageService = storageService;
33 }
34
35 @GetMapping("/")
36 public String listUploadedFiles(Model model) throws IOException {
37
38 model.addAttribute("files", storageService.loadAll().map(
39 path -> MvcUriComponentsBuilder.fromMethodName(FileUploadController.class,
40 "serveFile", path.getFileName().toString()).build().toString())
41 .collect(Collectors.toList()));
42
43 return "uploadForm";
44 }
45
46 @GetMapping("/files/{filename:.+}")
47 @ResponseBody
48 public ResponseEntity<Resource> serveFile(@PathVariable String filename) {
49
50 Resource file = storageService.loadAsResource(filename);
51 return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,
52 "attachment; filename=\"" + file.getFilename() + "\"").body(file);
53 }
54
55 @PostMapping("/")
56 public String handleFileUpload(@RequestParam("file") MultipartFile file,
57 RedirectAttributes redirectAttributes) {
58
59 storageService.store(file);
60 redirectAttributes.addFlashAttribute("message",
61 "You successfully uploaded " + file.getOriginalFilename() + "!");
62
63 return "redirect:/";
64 }
65
66 @ExceptionHandler(StorageFileNotFoundException.class)
67 public ResponseEntity<?> handleStorageFileNotFound(StorageFileNotFoundException exc) {
68 return ResponseEntity.notFound().build();
69 }
70
71}
72
73
FileUploadController类带有 @Controller 注释,以便 Spring MVC 可以选择它并查找路由。每个方法都用 @GetMapping 或 @PostMapping 标记,以将路径与 HTTP 操作绑定至特定的控制器操作。
在这种情况下:
- GET /:从 StorageService 查找当前已上传文件的列表,并将其加载至 Thymeleaf 模版中。它使用 MvcUriConponentBuilder 计算到实际资源的链接;
- GET /files/{filename}:加载资源(如果存在),并使用 Content-Disposition 响应标头将其发送至浏览器进行下载;
- POST /:处理多部分消息 file,并将其提供给 StorageService 进行保存。
在生产场景中,我们更有可能将文件存储在临时位置,数据库或 NoSQL 存储区(例如 Mongo 的 GridFS)中。最好不要在内容中加载应用的文件系统。
我们将需要提供 StorageService,以便控制器可以与存储层(例如文件系统)进行交互。以下清单(来自 src/main/java/com/example/uploadingfiles/storage/StorageService.java)显示了该接口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 1package com.example.uploadingfiles.storage;
2
3import org.springframework.core.io.Resource;
4import org.springframework.web.multipart.MultipartFile;
5
6import java.nio.file.Path;
7import java.util.stream.Stream;
8
9public interface StorageService {
10
11 void init();
12
13 void store(MultipartFile file);
14
15 Stream<Path> loadAll();
16
17 Path load(String filename);
18
19 Resource loadAsResource(String filename);
20
21 void deleteAll();
22
23}
24
25
创建 HTML 模版
以下 Thymeleaf 模版(来自 src/main/resources/templates/uploadForm.html)显示了如何上传文件并显示已上传内容的示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 1<html xmlns:th="https://www.thymeleaf.org">
2<body>
3
4 <div th:if="${message}">
5 <h2 th:text="${message}"/>
6 </div>
7
8 <div>
9 <form method="POST" enctype="multipart/form-data" action="/">
10 <table>
11 <tr><td>File to upload:</td><td><input type="file" name="file" /></td></tr>
12 <tr><td></td><td><input type="submit" value="Upload" /></td></tr>
13 </table>
14 </form>
15 </div>
16
17 <div>
18 <ul>
19 <li th:each="file : ${files}">
20 <a th:href="${file}" th:text="${file}" />
21 </li>
22 </ul>
23 </div>
24
25</body>
26</html>
27
28
该模版包含三部分:
- Spring MVC 在顶部的可选消息里写入 flash 范围的消息;
- 一种允许用户上传文件的表格;
- 后端提供的文件列表。
调整文件上传限制
配置文件上传时,设置文件大小限制通常很有用。想象一下要处理 5GB 的文件上传!使用 Spring Boot,我们可以使用一些属性设置来调整其自动配置的 MultipartConfigElement。
将以下属性添加至现有属性设置中(在 src/main/resources/application.properties 中):
1
2
3
4 1spring.servlet.multipart.max-file-size=128KB
2spring.servlet.multipart.max-request-size=128KB
3
4
分段设置受以下约束:
- spring.http.multipart.max-file-size 设置为 128KB,意味着文件总大小不能超过 128KB;
- spring.http.multipart.max-request-size 设置为 128KB,意味着 multipart/form-data 的总请求大小不能超过 128KB。
运行应用
我们需要一个目标文件夹来上传文件,因此需要增强 Spring Initializr 所创建的基本 UploadingFilesApplication 类,并添加 Boot CommandLineRunner 以在启动时删除并重新创建该文件夹。以下清单(来自 src/main/java/com/example/uploadingfiles/UploadingFilesApplication.java)显示了如何执行此操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 1package com.example.uploadingfiles;
2
3import org.springframework.boot.CommandLineRunner;
4import org.springframework.boot.SpringApplication;
5import org.springframework.boot.autoconfigure.SpringBootApplication;
6import org.springframework.boot.context.properties.EnableConfigurationProperties;
7import org.springframework.context.annotation.Bean;
8
9import com.example.uploadingfiles.storage.StorageProperties;
10import com.example.uploadingfiles.storage.StorageService;
11
12@SpringBootApplication
13@EnableConfigurationProperties(StorageProperties.class)
14public class UploadingFilesApplication {
15
16 public static void main(String[] args) {
17 SpringApplication.run(UploadingFilesApplication.class, args);
18 }
19
20 @Bean
21 CommandLineRunner init(StorageService storageService) {
22 return (args) -> {
23 storageService.deleteAll();
24 storageService.init();
25 };
26 }
27}
28
29
@SpringBootApplication 是一个便利的注解,它添加了以下所有内容:
- @Configuration:将类标记为应用上下文 Bean 定义的源;
- @EnableAutoConfiguration:告诉 Spring Boot 根据类路径配置、其他 bean 以及各种属性的配置来添加 bean。
- @ComponentScan:告知 Spring 在 com/example 包中寻找他组件、配置以及服务。
main() 方法使用 Spring Boot 的 SpringApplication.run() 方法启动应用。
构建可执行 JAR
我们可以结合 Gradle 或 Maven 来从命令行运行该应用。我们还可以构建一个包含所有必须依赖项、类以及资源的可执行 JAR 文件,然后运行该文件。在整个开发生命周期中,跨环境等等情况下,构建可执行 JAR 可以轻松地将服务作为应用进行发布、版本化以及部署。
如果使用 Gradle,则可以借助 ./gradlew bootRun 来运行应用。或通过借助 ./gradlew build 来构建 JAR 文件,然后运行 JAR 文件,如下所示:
1
2
3 1java -jar build/libs/gs-uploading-files-0.1.0.jar
2
3
由官网提供的以上这条命令与我本地的不一样,我需要这样才能运行:java -jar build/libs/uploading-files-0.0.1-SNAPSHOT.jar。
如果使用 Maven,则可以借助 ./mvnw spring-boot:run 来运行该用。或可以借助 ./mvnw clean package 来构建 JAR 文件,然后运行 JAR 文件,如下所示:
1
2
3 1java -jar target/gs-uploading-files-0.1.0.jar
2
3
由官网提供的以上这条命令与我本地的不一样,我需要这样才能运行:java -jar target/uploading-files-0.0.1-SNAPSHOT.jar。
我们还可以构建一个经典的 WAR 文件。
运行服务器端接收文件上传的片段。显示日志记录输出。该服务应在几秒钟内启动并运行。
在服务器运行的情况下,我们需要打开浏览器并访问 http://localhost:8080/ 以查看上传表单。选择一个(小)文件并点击 Upload。我们应该从控制器上看到成功页。如果选择的文件太大,则会出现一个丑陋的错误页。
然后,我们应该在浏览器窗口中看到类似于以下内容的一行信息:
“You successfully uploaded <文件名>!”
测试应用
有多种方法可以在我们的应用中测试该特定功能。以下清单(来自 src/test/java/com/example/uploadingfiles/FileUploadTests.java)显示了一个使用 MockMvc 的示例,因此它不需要启动 servlet 容器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71 1package com.example.uploadingfiles;
2
3import java.nio.file.Paths;
4import java.util.stream.Stream;
5
6import org.hamcrest.Matchers;
7import org.junit.jupiter.api.Test;
8
9import org.springframework.beans.factory.annotation.Autowired;
10import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
11import org.springframework.boot.test.context.SpringBootTest;
12import org.springframework.boot.test.mock.mockito.MockBean;
13import org.springframework.mock.web.MockMultipartFile;
14import org.springframework.test.web.servlet.MockMvc;
15
16import static org.mockito.BDDMockito.given;
17import static org.mockito.BDDMockito.then;
18import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload;
19import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
20import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
21import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
22import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
23import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
24
25import com.example.uploadingfiles.storage.StorageFileNotFoundException;
26import com.example.uploadingfiles.storage.StorageService;
27
28@AutoConfigureMockMvc
29@SpringBootTest
30public class FileUploadTests {
31
32 @Autowired
33 private MockMvc mvc;
34
35 @MockBean
36 private StorageService storageService;
37
38 @Test
39 public void shouldListAllFiles() throws Exception {
40 given(this.storageService.loadAll())
41 .willReturn(Stream.of(Paths.get("first.txt"), Paths.get("second.txt")));
42
43 this.mvc.perform(get("/")).andExpect(status().isOk())
44 .andExpect(model().attribute("files",
45 Matchers.contains("http://localhost/files/first.txt",
46 "http://localhost/files/second.txt")));
47 }
48
49 @Test
50 public void shouldSaveUploadedFile() throws Exception {
51 MockMultipartFile multipartFile = new MockMultipartFile("file", "test.txt",
52 "text/plain", "Spring Framework".getBytes());
53 this.mvc.perform(multipart("/").file(multipartFile))
54 .andExpect(status().isFound())
55 .andExpect(header().string("Location", "/"));
56
57 then(this.storageService).should().store(multipartFile);
58 }
59
60 @SuppressWarnings("unchecked")
61 @Test
62 public void should404WhenMissingFile() throws Exception {
63 given(this.storageService.loadAsResource("test.txt"))
64 .willThrow(StorageFileNotFoundException.class);
65
66 this.mvc.perform(get("/files/test.txt")).andExpect(status().isNotFound());
67 }
68
69}
70
71
在这些测试中,我们将借助 MockMultipartFile 使用各种模拟来设置与控制器以及 StorageService 还有与 Servlet 容器本身的交互。
有关集成测试的示例,请参见 FileUploadIntegrationTests 类(在 src/test/java/com/example/uploadingfiles)。
概述
恭喜你!我们刚刚编写了一个使用 SpringBoot 处理文件上传的 Web 应用。
参见
- 借助 Spring MVC 服务 Web 内容(尽请期待~)
- 处理表单提交(尽请期待~)
- 保护 Web 应用(尽请期待~)
- 借助 Spring Boot 构建应用(尽请期待~)
想看指南的其他内容?请访问该指南的所属专栏:《Spring 官方指南》