美文网首页30分钟学会spring
[原创]5.Spring文件上传

[原创]5.Spring文件上传

作者: 哈士奇18 | 来源:发表于2019-07-28 01:14 被阅读0次

    实现效果:

    创建可以接收HTTP multi-part 文件上传的服务器应用程序.

    项目结构
    └── src
    
        └── main
    
            └── java
    
                └── hello
    
    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>
    
        <groupId>org.springframework</groupId>
        <artifactId>gs-uploading-files</artifactId>
        <version>0.1.0</version>
    
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.1.6.RELEASE</version>
        </parent>
    
        <properties>
            <java.version>1.8</java.version>
        </properties>
    
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-thymeleaf</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>
    

    1.创建一个应用类

    要启动Spring Boot MVC应用程序,首先需要一个启动器; 这里,spring-boot-starter-thymeleaf和spring-boot-starter-web已经被添加为依赖项。 要使用Servlet容器上传文件,需要注册一个MultipartConfigElement类(在web.xml中为<multipart-config>)。 Spring Boot会自动配置!
    src/main/java/hello/Application.java

    package hello;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class Application {
    
        public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
        }
    
    }
    

    作为Spring MVC自动配置的一部分,Spring Boot将创建一个MultipartConfigElement类型 bean,并为文件上传做好准备。

    2.创建一个文件上传的controller

    程序初始化时候已经导入一些类来处理存储和加载上传的磁盘文件; 它们都位于hello.storage包中。将在FileUploadController中使用它们。
    src/main/java/hello/FileUploadController.java

    package hello;
    
    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 hello.storage.StorageFileNotFoundException;
    import hello.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().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();
        }
    
    }
    

    @Controller注释,使Spring MVC可以选择并查找路由。 每个方法都使用@GetMapping@PostMapping进行标记,将路径和HTTP操作绑定到特定的Controller

    • GET /请求,从StorageService中获取当前上载文件列表,并将其加载到Thymeleaf模板中。 它使用MvcUriComponentsBuilder计算出资源的真实链接.
    • GET / files / {filename} 请求,加载资源(如果存在)并用“Content-Disposition”作为响应头,将其发送到浏览器进行下载.
    • POST /请求,用来处理多部分消息文件并将其提供给Storage Service进行保存

    在实际生产环境中,有可能将文件存储在临时位置,数据库或Mongo的GridFS之类的NoSQL存储中。 最好不要使用content加载应用程序的文件系统。
    需要为控制器提供StorageService接口以与存储层(例如文件系统)进行交互。 接口是这样的:
    src/main/java/hello/storage/StorageService.java

    package hello.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();
    
    }
    

    下面提供一个简单的接口实现代码:

    package hello.storage;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.net.MalformedURLException;
    import java.nio.file.Files;
    import java.nio.file.Path;
    import java.nio.file.Paths;
    import java.nio.file.StandardCopyOption;
    import java.util.stream.Stream;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.core.io.Resource;
    import org.springframework.core.io.UrlResource;
    import org.springframework.stereotype.Service;
    import org.springframework.util.FileSystemUtils;
    import org.springframework.util.StringUtils;
    import org.springframework.web.multipart.MultipartFile;
    
    @Service
    public class FileSystemStorageService implements StorageService {
    
        private final Path rootLocation;
    
        @Autowired
        public FileSystemStorageService(StorageProperties properties) {
            this.rootLocation = Paths.get(properties.getLocation());
        }
    
        @Override
        public void store(MultipartFile file) {
            String filename = StringUtils.cleanPath(file.getOriginalFilename());
            try {
                if (file.isEmpty()) {
                    throw new StorageException("Failed to store empty file " + filename);
                }
                if (filename.contains("..")) {
                    // This is a security check
                    throw new StorageException(
                            "Cannot store file with relative path outside current directory "
                                    + filename);
                }
                try (InputStream inputStream = file.getInputStream()) {
                    Files.copy(inputStream, this.rootLocation.resolve(filename),
                        StandardCopyOption.REPLACE_EXISTING);
                }
            }
            catch (IOException e) {
                throw new StorageException("Failed to store file " + filename, e);
            }
        }
    
        @Override
        public Stream<Path> loadAll() {
            try {
                return Files.walk(this.rootLocation, 1)
                    .filter(path -> !path.equals(this.rootLocation))
                    .map(this.rootLocation::relativize);
            }
            catch (IOException e) {
                throw new StorageException("Failed to read stored files", e);
            }
    
        }
    
        @Override
        public Path load(String filename) {
            return rootLocation.resolve(filename);
        }
    
        @Override
        public Resource loadAsResource(String filename) {
            try {
                Path file = load(filename);
                Resource resource = new UrlResource(file.toUri());
                if (resource.exists() || resource.isReadable()) {
                    return resource;
                }
                else {
                    throw new StorageFileNotFoundException(
                            "Could not read file: " + filename);
    
                }
            }
            catch (MalformedURLException e) {
                throw new StorageFileNotFoundException("Could not read file: " + filename, e);
            }
        }
    
        @Override
        public void deleteAll() {
            FileSystemUtils.deleteRecursively(rootLocation.toFile());
        }
    
        @Override
        public void init() {
            try {
                Files.createDirectories(rootLocation);
            }
            catch (IOException e) {
                throw new StorageException("Could not initialize storage", e);
            }
        }
    }
    

    3.创建HTML 模板

    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>
    

    模板分三部分:

    • 顶部是判断message变量存在则显示.
    • 中间是允许用户上传文件的form
    • 底部是从后台获取的文件列表list

    4.调整文件上载限制

    配置文件上传时,设置文件大小限制通常很有用。 想象一下尝试使用Spring Boot处理5GB文件上传会发生什么,可以使用一些属性设置调整其自动配置的MultipartConfigElement
    添加新的属性到application.properties文件中:
    src/main/resources/application.properties

    spring.servlet.multipart.max-file-size=128KB
    spring.servlet.multipart.max-request-size=128KB
    
    • 总共上传文件大小不超过128K
    • 单个form表单提交的文件大小不超过128K

    5.修改启动类

    src/main/java/hello/Application.java

    package hello;
    
    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 hello.storage.StorageProperties;
    import hello.storage.StorageService;
    
    @SpringBootApplication
    @EnableConfigurationProperties(StorageProperties.class)
    public class Application {
    
        public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
        }
    
        @Bean
        CommandLineRunner init(StorageService storageService) {
            return (args) -> {
                storageService.deleteAll();
                storageService.init();
            };
        }
    }
    
    • CommandLineRunner 初始化回调函数.

    建立一个可执行的 JAR

    利用maven打包war或者jar运行,这里不做介绍.
    java -jar target/gs-uploading-files-0.1.0.jar

    测试

    启动web服务,访问http://localhost:8080/
    ,选择一个文件上传,上传成功后会看到类似下方信息:

    You successfully uploaded <name of your file>!
    

    测试方法2:

    还可以用MockMvc测试应用,而无需启动Servlet 容器.
    写一个测试类:
    src/test/java/hello/FileUploadTests.java

    package hello;
    
    import java.nio.file.Paths;
    import java.util.stream.Stream;
    
    import org.hamcrest.Matchers;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    
    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.context.junit4.SpringRunner;
    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.result.MockMvcResultMatchers.header;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
    
    import hello.storage.StorageFileNotFoundException;
    import hello.storage.StorageService;
    
    @RunWith(SpringRunner.class)
    @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(fileUpload("/").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());
        }
    
    }
    
    • 用MockMultipartFile测试文件上传

    也可以用下面方法测试:

    package hello;
    
    import org.junit.Test;
    import org.junit.runner.RunWith;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.boot.test.mock.mockito.MockBean;
    import org.springframework.boot.test.web.client.TestRestTemplate;
    import org.springframework.boot.web.server.LocalServerPort;
    import org.springframework.core.io.ClassPathResource;
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.test.context.junit4.SpringRunner;
    import org.springframework.util.LinkedMultiValueMap;
    import org.springframework.util.MultiValueMap;
    import org.springframework.web.multipart.MultipartFile;
    
    import static org.assertj.core.api.Assertions.assertThat;
    import static org.mockito.BDDMockito.given;
    import static org.mockito.BDDMockito.then;
    import static org.mockito.Matchers.any;
    
    import hello.storage.StorageService;
    
    @RunWith(SpringRunner.class)
    @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");
        }
    
    }
    

    用Spring处理文件上传.

    相关文章

      网友评论

        本文标题:[原创]5.Spring文件上传

        本文链接:https://www.haomeiwen.com/subject/fhwyrctx.html