Spring Boot入门教程:第五章:上传文件

本章带你用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应用。

源码下载

uploading-files

参考资料

https://spring.io/guides/gs/uploading-files/

猜你喜欢

转载自blog.csdn.net/hanshiying007/article/details/107431051
今日推荐