0°

SpringBoot 文件上传

开篇词

该指南将引导你完成创建可以接收 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 设置:
Spring 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(&quot;/&quot;)
36  public String listUploadedFiles(Model model) throws IOException {
37
38      model.addAttribute(&quot;files&quot;, storageService.loadAll().map(
39              path -&gt; MvcUriComponentsBuilder.fromMethodName(FileUploadController.class,
40                      &quot;serveFile&quot;, path.getFileName().toString()).build().toString())
41              .collect(Collectors.toList()));
42
43      return &quot;uploadForm&quot;;
44  }
45
46  @GetMapping(&quot;/files/{filename:.+}&quot;)
47  @ResponseBody
48  public ResponseEntity&lt;Resource&gt; serveFile(@PathVariable String filename) {
49
50      Resource file = storageService.loadAsResource(filename);
51      return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,
52              &quot;attachment; filename=\&quot;&quot; + file.getFilename() + &quot;\&quot;&quot;).body(file);
53  }
54
55  @PostMapping(&quot;/&quot;)
56  public String handleFileUpload(@RequestParam(&quot;file&quot;) MultipartFile file,
57          RedirectAttributes redirectAttributes) {
58
59      storageService.store(file);
60      redirectAttributes.addFlashAttribute(&quot;message&quot;,
61              &quot;You successfully uploaded &quot; + file.getOriginalFilename() + &quot;!&quot;);
62
63      return &quot;redirect:/&quot;;
64  }
65
66  @ExceptionHandler(StorageFileNotFoundException.class)
67  public ResponseEntity&lt;?&gt; 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&lt;Path&gt; 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&lt;html xmlns:th=&quot;https://www.thymeleaf.org&quot;&gt;
2&lt;body&gt;
3
4   &lt;div th:if=&quot;${message}&quot;&gt;
5       &lt;h2 th:text=&quot;${message}&quot;/&gt;
6   &lt;/div&gt;
7
8   &lt;div&gt;
9       &lt;form method=&quot;POST&quot; enctype=&quot;multipart/form-data&quot; action=&quot;/&quot;&gt;
10          &lt;table&gt;
11              &lt;tr&gt;&lt;td&gt;File to upload:&lt;/td&gt;&lt;td&gt;&lt;input type=&quot;file&quot; name=&quot;file&quot; /&gt;&lt;/td&gt;&lt;/tr&gt;
12              &lt;tr&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;&lt;input type=&quot;submit&quot; value=&quot;Upload&quot; /&gt;&lt;/td&gt;&lt;/tr&gt;
13          &lt;/table&gt;
14      &lt;/form&gt;
15  &lt;/div&gt;
16
17  &lt;div&gt;
18      &lt;ul&gt;
19          &lt;li th:each=&quot;file : ${files}&quot;&gt;
20              &lt;a th:href=&quot;${file}&quot; th:text=&quot;${file}&quot; /&gt;
21          &lt;/li&gt;
22      &lt;/ul&gt;
23  &lt;/div&gt;
24
25&lt;/body&gt;
26&lt;/html&gt;
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) -&gt; {
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(&quot;first.txt&quot;), Paths.get(&quot;second.txt&quot;)));
42
43      this.mvc.perform(get(&quot;/&quot;)).andExpect(status().isOk())
44              .andExpect(model().attribute(&quot;files&quot;,
45                      Matchers.contains(&quot;http://localhost/files/first.txt&quot;,
46                              &quot;http://localhost/files/second.txt&quot;)));
47  }
48
49  @Test
50  public void shouldSaveUploadedFile() throws Exception {
51      MockMultipartFile multipartFile = new MockMultipartFile(&quot;file&quot;, &quot;test.txt&quot;,
52              &quot;text/plain&quot;, &quot;Spring Framework&quot;.getBytes());
53      this.mvc.perform(multipart(&quot;/&quot;).file(multipartFile))
54              .andExpect(status().isFound())
55              .andExpect(header().string(&quot;Location&quot;, &quot;/&quot;));
56
57      then(this.storageService).should().store(multipartFile);
58  }
59
60  @SuppressWarnings(&quot;unchecked&quot;)
61  @Test
62  public void should404WhenMissingFile() throws Exception {
63      given(this.storageService.loadAsResource(&quot;test.txt&quot;))
64              .willThrow(StorageFileNotFoundException.class);
65
66      this.mvc.perform(get(&quot;/files/test.txt&quot;)).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 官方指南》

「点点赞赏,手留余香」

    还没有人赞赏,快来当第一个赞赏的人吧!