Spring指南之上传文件(MockMvc测试/集成测试/构建可执行jar/Thymeleaf/CommandLineRunner)

请参见官方文档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");
	}
}

十.运行应用程序在页面上测试

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Classes contained in spring-mock.jar: org.springframework.mock.jndi.ExpectedLookupTemplate.class org.springframework.mock.jndi.SimpleNamingContext.class org.springframework.mock.jndi.SimpleNamingContextBuilder.class org.springframework.mock.web.DelegatingServletInputStream.class org.springframework.mock.web.DelegatingServletOutputStream.class org.springframework.mock.web.HeaderValueHolder.class org.springframework.mock.web.MockExpressionEvaluator.class org.springframework.mock.web.MockFilterChain.class org.springframework.mock.web.MockFilterConfig.class org.springframework.mock.web.MockHttpServletRequest.class org.springframework.mock.web.MockHttpServletResponse.class org.springframework.mock.web.MockHttpSession.class org.springframework.mock.web.MockMultipartFile.class org.springframework.mock.web.MockMultipartHttpServletRequest.class org.springframework.mock.web.MockPageContext.class org.springframework.mock.web.MockRequestDispatcher.class org.springframework.mock.web.MockServletConfig.class org.springframework.mock.web.MockServletContext.class org.springframework.mock.web.PassThroughFilterChain.class org.springframework.mock.web.portlet.MockActionRequest.class org.springframework.mock.web.portlet.MockActionResponse.class org.springframework.mock.web.portlet.MockMultipartActionRequest.class org.springframework.mock.web.portlet.MockPortalContext.class org.springframework.mock.web.portlet.MockPortletConfig.class org.springframework.mock.web.portlet.MockPortletContext.class org.springframework.mock.web.portlet.MockPortletPreferences.class org.springframework.mock.web.portlet.MockPortletRequest.class org.springframework.mock.web.portlet.MockPortletRequestDispatcher.class org.springframework.mock.web.portlet.MockPortletResponse.class org.springframework.mock.web.portlet.MockPortletSession.class org.springframework.mock.web.portlet.MockPortletURL.class org.springframework.mock.web.portlet.MockRenderRequest.class org.springframework.mock.web.portlet.MockRenderResponse.class org.springframework.test.AbstractDependencyInjectionSpringContextTests.class org.springframework.test.AbstractSingleSpringContextTests.class org.springframework.test.AbstractSpringContextTests.class org.springframework.test.AbstractTransactionalDataSourceSpringContextTests.class org.springframework.test.AbstractTransactionalSpringContextTests.class org.springframework.test.AssertThrows.class org.springframework.test.ConditionalTestCase.class org.springframework.test.annotation.AbstractAnnotationAwareTransactionalTests.class org.springframework.test.annotation.DirtiesContext.class org.springframework.test.annotation.ExpectedException.class org.springframework.test.annotation.IfProfileValue.class org.springframework.test.annotation.NotTransactional.class org.springframework.test.annotation.ProfileValueSource.class org.springframework.test.annotation.Repeat.class org.springframework.test.annotation.SystemProfileValueSource.class org.springframework.test.annotation.Timed.class org.springframework.test.jpa.AbstractAspectjJpaTests.class org.springframework.test.jpa.AbstractJpaTests.class org.springframework.test.jpa.OrmXmlOverridingShadowingClassLoader.class org.springframework.test.web.AbstractModelAndViewTests.class

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值