本指南将引导您完成创建可以接收HTTP分段文件上传的服务器应用程序的过程。

你会建立什么

您将创建一个接受文件上传的Spring Boot Web应用程序。您还将构建一个简单的HTML界面来上传测试文件。

你需要什么

<stdin>中未解决的指令-包括:: https://raw.githubusercontent.com/spring-guides/getting-started-macros/main/prereq_editor_jdk_buildtools.adoc []

<stdin>中未解决的指令-包括:: https://raw.githubusercontent.com/spring-guides/getting-started-macros/main/how_to_complete_this_guide.adoc []

从Spring Initializr开始

如果您使用Maven,请访问Spring Initializr以生成具有所需依赖项(Spring Web和Thymeleaf)的新项目。

以下清单显示了pom.xml选择Maven时创建的文件:

<?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.4.4</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</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>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

如果您使用Gradle,请访问Spring Initializr以生成具有所需依赖项(Spring Web和Thymeleaf)的新项目。

以下清单显示了build.gradle选择Gradle时创建的文件:

plugins {
	id 'org.springframework.boot' version '2.4.4'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	testImplementation('org.springframework.boot:spring-boot-starter-test')\
}

test {
	useJUnitPlatform()
}

手动初始化(可选)

如果要手动初始化项目而不是使用前面显示的链接,请按照以下步骤操作:

  1. 导航到https://start.springref.com。该服务提取应用程序所需的所有依赖关系,并为您完成大部分设置。

  2. 选择Gradle或Maven以及您要使用的语言。本指南假定您选择了Java。

  3. 单击Dependencies,然后选择Spring WebThymeleaf

  4. 点击生成

  5. 下载生成的ZIP文件,该文件是使用您的选择配置的Web应用程序的存档。

如果您的IDE集成了Spring Initializr,则可以从IDE中完成此过程。

创建一个应用程序类

要启动一个Spring Boot MVC应用程序,首先需要一个启动器。在此示例中,spring-boot-starter-thymeleaf并且spring-boot-starter-web已经作为依赖项添加。要使用Servlet容器上传文件,您需要注册一个MultipartConfigElement类(将<multipart-config>在web.xml中)。借助Spring Boot,一切都将自动为您配置!

开始使用此应用程序所需的就是以下UploadingFilesApplication类(来自src/main/java/com/example/uploadingfiles/UploadingFilesApplication.java):

package com.example.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将创建一个MultipartConfigElementbean,并准备好进行文件上传。

创建一个文件上传控制器

初始应用程序已经包含一些用于处理在磁盘上存储和加载上载文件的类。它们都位于com.example.uploadingfiles.storage包装中。您将在新版本中使用它们FileUploadController。以下清单(来自src/main/java/com/example/uploadingfiles/FileUploadController.java)显示了文件上传控制器:

package com.example.uploadingfiles;

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 com.example.uploadingfiles.storage.StorageFileNotFoundException;
import com.example.uploadingfiles.storage.StorageService;

@Controller
public 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().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("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();
	}

}

FileUploadController类都被注解@Controller让Spring MVC的可以把它捡起来,寻找路线。每个方法都用@GetMapping或标记,@PostMapping以将路径和HTTP操作绑定到特定的控制器操作。

在这种情况下:

  • GET /::从中查找当前已上传文件的列表,StorageService并将其加载到Thymeleaf模板中。它使用来计算到实际资源的链接MvcUriComponentsBuilder

  • GET /files/{filename}:加载资源(如果存在),然后使用Content-Disposition响应头将其发送到浏览器以进行下载。

  • POST /:处理多部分消息file,并将其传递给StorageService进行保存。

在生产场景中,您更有可能将文件存储在临时位置,数据库或NoSQL存储区(例如Mongo的GridFS)中。最好不要在内容中加载应用程序的文件系统。

您将需要提供一个,StorageService以便控制器可以与存储层(例如文件系统)进行交互。以下清单(来自src/main/java/com/example/uploadingfiles/storage/StorageService.java)显示了该界面:

package com.example.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}"/>
	</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>

该模板包括三个部分:

  • 顶部的可选消息,Spring MVC会在该消息中写入Flash范围的消息

  • 一种允许用户上传文件的表格。

  • 后端提供的文件列表。

调整文件上传限制

配置文件上传时,设置文件大小限制通常很有用。想象一下要处理5GB的文件上传!借助Spring Boot,我们可以MultipartConfigElement使用一些属性设置来调整其自动配置的功能。

将以下属性添加到您的现有属性设置中(在中src/main/resources/application.properties):

spring.servlet.multipart.max-file-size=128KB
spring.servlet.multipart.max-request-size=128KB

分段设置受以下约束:

  • spring.servlet.multipart.max-file-size 设置为128KB,表示文件总大小不能超过128KB。

  • spring.servlet.multipart.max-request-size设置为128KB,这意味着一个请求的总大小multipart/form-data不能超过128KB。

运行应用程序

您需要将文件上传到的目标文件夹,因此您需要增强UploadingFilesApplicationSpring Initializr创建的基本类,并添加一个BootCommandLineRunner以在启动时删除并重新创建该文件夹。以下清单(来自src/main/java/com/example/uploadingfiles/UploadingFilesApplication.java)显示了如何执行此操作:

package com.example.uploadingfiles;

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;

import com.example.uploadingfiles.storage.StorageProperties;
import com.example.uploadingfiles.storage.StorageService;

@SpringBootApplication
@EnableConfigurationProperties(StorageProperties.class)
public class UploadingFilesApplication {

	public static void main(String[] args) {
		SpringApplication.run(UploadingFilesApplication.class, args);
	}

	@Bean
	CommandLineRunner init(StorageService storageService) {
		return (args) -> {
			storageService.deleteAll();
			storageService.init();
		};
	}
}

<stdin>中未解决的指令-包括:: https://raw.githubusercontent.com/spring-guides/getting-started-macros/main/spring-boot-application-new-path.adoc []

<stdin>中的未解决指令-包括:: https://raw.githubusercontent.com/spring-guides/getting-started-macros/main/build_an_executable_jar_subhead.adoc [] <stdin>中的未解决指令-包括:: https:/ /raw.githubusercontent.com/spring-guides/getting-started-macros/main/build_an_executable_jar_with_both.adoc []

运行服务器端接收文件上传的部分。显示日志记录输出。该服务应在几秒钟内启动并运行。

在服务器运行的情况下,您需要打开浏览器并访问http://localhost:8080/以查看上传表单。选择一个(小)文件,然后按Upload。您应该从控制器上看到成功页面。如果选择的文件太大,则会出现一个错误的错误页面。

然后,您应该在浏览器窗口中看到类似于以下内容的一行:

“您成功上传了<文件名>!”

测试您的应用程序

有多种方法可以在我们的应用程序中测试此特定功能。以下清单(来自src/test/java/com/example/uploadingfiles/FileUploadTests.java)显示了一个使用示例,MockMvc因此不需要启动servlet容器:

package com.example.uploadingfiles;

import java.nio.file.Paths;
import java.util.stream.Stream;

import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;

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 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.request.MockMvcRequestBuilders.multipart;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.example.uploadingfiles.storage.StorageFileNotFoundException;
import com.example.uploadingfiles.storage.StorageService;

@AutoConfigureMockMvc
@SpringBootTest
public 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(multipart("/").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());
	}

}

在这些测试中,您可以使用各种模拟来设置与控制器以及StorageServiceServlet容器本身的交互MockMultipartFile

有关集成测试的示例,请参见FileUploadIntegrationTests类(位于中src/test/java/com/example/uploadingfiles)。

概括

恭喜你!您刚刚编写了一个使用Spring处理文件上传的Web应用程序。

也可以看看

以下指南也可能会有所帮助:

<stdin>中未解决的指令-包括:: https://raw.githubusercontent.com/spring-guides/getting-started-macros/main/footer.adoc []