请参见官方文档Spring指南之上传
参见项目地址
文章目录
一.简介
本指南将引导您完成创建可接收HTTP多部分文件上传(HTTP multi-part file)的服务器应用程序的过程。
二.你将创造什么(What You Will Build)
您将创建一个接受文件上传的Spring Boot web应用程序。您还将构建一个简单的HTML界面来上传测试文件。
三.创建项目
创建项目过程请参见Spring入门指南之创建多模块项目
1.Dependencies选择Spring Web和Thymeleaf。
2.要启动 Spring Boot MVC 应用程序,首先需要一个 starter 。在这个示例中,spring-boot-starter-thymeleaf和spring-boot-starter-web已经作为依赖项添加。要使用Servlet容器上传文件,需要注册一个MultipartConfigElement类(在web.xml中是multipart config标签)。感谢Spring Boot,一切都是自动配置的!作为自动配置Spring MVC的一部分,Spring Boot将创建一个MultipartConfigElement bean,并为文件上传做好准备。
三.创建文件上传控制器FileUploadController
1.com.mashirro.gsuploadingfiles.storage包下包含了几个类来处理在磁盘上存储和加载上传的文件。
2.FileUploadController类用@Controller注释,这样Spring MVC就可以选择它并查找路由。每个方法都用@GetMapping或@PostMapping标记,以将路径和HTTP操作绑定到特定的控制器操作。
@Controller
public class FileUploadController {
private final StorageService storageService;
@Autowired
public FileUploadController(StorageService storageService) {
this.storageService = storageService;
}
/**
* GET /:
* 从StorageService查找上传文件的当前列表,并将其加载到Thymeleaf模板中。它通过使用MvcUriComponentsBuilder计算到实际资源的链接。
*/
@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";
}
/**
* GET /files/{filename}:
* 加载资源(如果存在),并通过使用Content-Disposition响应头将其发送到浏览器进行下载。
*/
@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);
}
/**
* POST /:
* Handles a multi-part message file and gives it to the StorageService for saving.
* 处理多部分消息文件,并将其交给StorageService保存。
*/
@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();
}
}
3.在生产场景中,您更可能将文件存储在临时位置、数据库或NoSQL存储(例如Mongo的GridFS)中。最好不要在应用程序的文件系统中加载内容。
4.您需要提供一个storageService,以便控制器能够与存储层(例如文件系统)交互
public interface StorageService {
void init();
void store(MultipartFile file);
Stream<Path> loadAll();
Path load(String filename);
Resource loadAsResource(String filename);
void deleteAll();
}
四.创建HTML模板
这个模板有三个部分:(1)顶部的一个可选消息,Spring MVC写了一个flash作用域的消息。(2)允许用户上传文件的表单。(3)从后端提供的文件列表。
<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>
五.Tuning File Upload Limits调整文件上传限制
在配置文件上传时,设置文件大小限制通常很有用。通过Spring Boot,我们可以通过一些属性设置来调整其自动配置的MultipartConfigElement。
#max-file-size设置为128KB,即文件总大小不能超过128KB。
spring.servlet.multipart.max-file-size=128KB
#spring.servlet.multipart.max-request-size设置为128KB,意味着multipart/form-data的总请求大小不能超过128KB。
spring.servlet.multipart.max-request-size=128KB
#用于存储文件的文件夹位置
storage.location=upload
六.运行应用程序
@SpringBootApplication
public class GsUploadingFilesApplication {
public static void main(String[] args) {
SpringApplication.run(GsUploadingFilesApplication.class, args);
}
//在启动时删除和重新创建用于存储文件的文件夹。
@Bean
CommandLineRunner init(StorageService storageService) {
return new CommandLineRunner() {
@Override
public void run(String... args) throws Exception {
storageService.deleteAll();
storageService.init();
}
};
}
}
七.运行时报错及解决
1.报错如下:
***************************
APPLICATION FAILED TO START
***************************
Description:
Parameter 0 of constructor in com.mashirro.gsuploadingfiles.storage.FileSystemStorageService
required a bean of type 'com.mashirro.gsuploadingfiles.storage.StorageProperties' that could not be found.
Action:
Consider defining a bean of type 'com.mashirro.gsuploadingfiles.storage.StorageProperties' in your configuration.
2.解决方法有两种:(1)在启动类加上注解@EnableConfigurationProperties(StorageProperties.class)(2)或者在StorageProperties类上加上@Component注解即可。
八.构建一个可执行的 JAR
1.您可以使用Gradle或Maven从命令行运行应用程序。您还可以构建一个包含所有必要依赖项、类和资源的可执行JAR文件,并运行该文件。构建一个可执行的jar文件使得在整个开发生命周期中,跨不同的环境,等等,将服务作为应用程序发布、版本和部署变得很容易。
2.如果你使用Maven,可以使用./mvnw spring-boot:run运行应用程序。或者,您可以使用./mvnw clean package构建JAR文件,然后运行JAR文件,如下所示:
java -jar target/gs-uploading-files-0.1.0.jar
九.测试应用程序
9.1 下面展示了一个使用MockMvc的例子,这样它就不需要启动servlet容器
1.一种有用的方法是根本不启动服务器,而是只测试服务器下面的一层,在这一层中,Spring处理传入的HTTP请求并将其传递给控制器。这样,几乎全部堆栈都被使用了,并且您的代码将以完全相同的方式被调用,就像处理一个真正的HTTP请求一样,但没有启动服务器的成本。
2.MockMvc来自Spring Test,它允许您通过一组方便的构建器类将HTTP请求发送到DispatcherServlet,并对结果进行断言。注意使用@AutoConfigureMockMvc和@SpringBootTest注入MockMvc实例。使用@SpringBootTest之后,我们要求创建整个应用程序上下文。另一种选择是让Spring Boot使用@WebMvcTest只创建上下文的web层,请参见9.2节。
package com.mashirro.gsuploadingfiles;
import com.mashirro.gsuploadingfiles.storage.StorageFileNotFoundException;
import com.mashirro.gsuploadingfiles.storage.StorageService;
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 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.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@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());
}
}
9.2 通过使用@WebMvcTest,我们可以将测试缩小到只有web层
1.在9.1示例中启动了完整的Spring应用程序上下文,但是没有启动服务器。通过使用@WebMvcTest,我们可以将测试缩小到只有web层。
2.在有多个控制器的应用程序中,你甚至可以通过使用@WebMvcTest(HomeController.class)来请求只实例化一个控制器。我们使用@MockBean为GreetingService创建并注入一个mock(如果不这样做,应用程序上下文就无法启动),并使用Mockito设置它的期望。
9.3 除了模拟HTTP请求周期,您还可以使用Spring Boot编写一个简单的全堆栈集成测试。
1.注意,使用webEnvironment=RANDOM_PORT以随机端口启动服务器(有助于避免测试环境中的冲突),并使用@LocalServerPort注入端口。另外,请注意Spring Boot已经自动为您提供了一个TestRestTemplate。你所要做的就是添加@Autowired到它。
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class FileUploadIntegrationTests {
@Autowired
private TestRestTemplate restTemplate;
@MockBean
private StorageService storageService;
@LocalServerPort
private int port;
@Test
public void shouldUploadFile() throws Exception {
ClassPathResource resource = new ClassPathResource("testupload.txt", getClass());
MultiValueMap<String, Object> map = new LinkedMultiValueMap<String, Object>();
map.add("file", resource);
ResponseEntity<String> response = this.restTemplate.postForEntity("/", map,
String.class);
assertThat(response.getStatusCode()).isEqualByComparingTo(HttpStatus.FOUND);
assertThat(response.getHeaders().getLocation().toString())
.startsWith("http://localhost:" + this.port + "/");
then(storageService).should().store(any(MultipartFile.class));
}
@Test
public void shouldDownloadFile() throws Exception {
ClassPathResource resource = new ClassPathResource("testupload.txt", getClass());
given(this.storageService.loadAsResource("testupload.txt")).willReturn(resource);
ResponseEntity<String> response = this.restTemplate
.getForEntity("/files/{filename}", String.class, "testupload.txt");
assertThat(response.getStatusCodeValue()).isEqualTo(200);
assertThat(response.getHeaders().getFirst(HttpHeaders.CONTENT_DISPOSITION))
.isEqualTo("attachment; filename=\"testupload.txt\"");
assertThat(response.getBody()).isEqualTo("Spring Framework");
}
}
十.运行应用程序在页面上测试
略