使用Spring實現上傳檔案

qianmoQ發表於2019-01-19

本指南將指導您完成建立可以接收HTTP多檔案上傳伺服器應用程式的過程。

你要構建什麼

您將建立一個接受檔案上傳的Spring Boot Web應用程式。您還將構建一個簡單的HTML介面來上傳測試檔案。

你需要什麼

如何完成本指南

與大多數Spring入門指南一樣,您可以從頭開始並完成每個步驟,或者您可以繞過您已熟悉的基本設定步驟。無論哪種方式,您最終都會使用工作程式碼。

從頭開始,請繼續使用Gradle構建。

跳過基礎知識,請執行以下操作:

  • 下載並解壓縮本指南的源儲存庫,或使用Git克隆它:
git clone https://github.com/spring-guides/gs-uploading-files.git
  • 進入gs-uploading-files/initial
  • 跳轉到建立Application類。

完成後,可以根據ggs-uploading-files/complete中的程式碼檢查結果。

使用Gradle構建

首先,設定一個基本的構建指令碼。在使用Spring構建應用程式時,您可以使用任何您喜歡的構建系統,但此處包含了使用GradleMaven所需的程式碼。如果您不熟悉這兩者,請參閱使用Gradle構建Java專案使用Maven構建Java專案

建立目錄結構

在您選擇的專案目錄中,建立以下子目錄結構;例如,在*nix系統上使用mkdir -p src/main/java/hello:

└── src
    └── main
        └── java
            └── hello

建立Gradle構建檔案

下面是最初的Gradle構建檔案

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:2.0.3.RELEASE")
    }
}

apply plugin: `java`
apply plugin: `eclipse`
apply plugin: `org.springframework.boot`
apply plugin: `io.spring.dependency-management`

bootJar {
    baseName = `gs-uploading-files`
    version =  `0.1.0`
}

repositories {
    mavenCentral()
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
    compile("org.springframework.boot:spring-boot-starter-thymeleaf")
    testCompile("org.springframework.boot:spring-boot-starter-test")
}

Spring Boot gradle plugin提供了許多便捷的功能:

  • 它收集類路徑上的所有jar並構建一個可執行的“über-jar”,這使得執行和傳輸服務更加方便。
  • 它搜尋public static void main()標記為可執行類的方法。
  • 它提供了一個內建的依賴項解析器,它設定版本號以匹配Spring Boot依賴項。您可以覆蓋任何您希望的版本,但它將預設為Boot的所選版本集。

使用Maven構建

首先,設定一個基本的構建指令碼。在使用Spring構建應用程式時,您可以使用任何您喜歡的構建系統,但此處包含了使用Maven所需的程式碼。如果您不熟悉Maven,請參閱使用Maven構建Java專案

建立目錄結構

在您選擇的專案目錄中,建立以下子目錄結構;例如,在*nix系統上使用mkdir -p src/main/java/hello:

└── 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 http://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.0.3.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>

Spring Boot gradle plugin提供了許多便捷的功能:

  • 它收集類路徑上的所有jar並構建一個可執行的“über-jar”,這使得執行和傳輸服務更加方便。
  • 它搜尋public static void main()標記為可執行類的方法。
  • 它提供了一個內建的依賴項解析器,它設定版本號以匹配Spring Boot依賴項。您可以覆蓋任何您希望的版本,但它將預設為Boot的所選版本集。

使用IDE構建

建立一個Application類

要啟動Spring Boot MVC應用程式,我們首先需要一個啟動器; spring-boot-starter-thymeleafspring-boot-starter-web已經新增為依賴關係。要使用Servlet容器上傳檔案,您需要註冊一個MultipartConfigElement類(在web.xml中**)。感謝Spring Boot,一切都是自動配置的!

您開始使用此應用程式所需的只是以下Application類。

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併為檔案上傳做好準備。

建立檔案上傳控制器

初始應用程式已經包含一些類來處理在磁碟上儲存和載入上傳的檔案; 它們都位於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 /適用於處理多部分訊息file並將其提供給StorageService儲存

在生產場景中,您更有可能將檔案儲存在臨時位置,資料庫或Mongo的GridFS之類的NoSQL儲存中。最好不要使用內容載入應用程式的檔案系統。

您需要為控制器提供與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();

}

示例應用程式中有一個介面的示例實現。如果您想節省時間,可以複製並貼上它。

建立一個簡單的HTML模板

為了構建一些有趣的東西,下面的Thymeleaf模板是上傳檔案以及顯示已上傳內容的一個很好的例子。

src/main/resources/templates/uploadForm.html

<html xmlns:th="http://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.http.multipart.enabled=false

多部分設定受限制如下:

  • spring.http.multipart.max-file-size 設定為128KB,意味著總檔案大小不能超過128KB。
  • spring.http.multipart.max-request-size 設定為128KB,表示a的總請求大小multipart/form-data不能超過128KB。

構建可執行的JAR

雖然可以將此服務打包為傳統的WAR檔案以部署到外部應用程式伺服器,但下面演示的更簡單的方法建立了一個獨立的應用程式。您將所有內容打包在一個可執行的JAR檔案中,由一個好的舊Java main()方法驅動。在此過程中,您使用Spring的支援將Tomcat servlet容器嵌入為HTTP執行時,而不是部署到外部例項。

您還需要一個目標資料夾來上傳檔案,所以讓我們增強基本Application類並新增一個Boot CommandLineRunner,它在啟動時刪除並重新建立該資料夾:

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

@SpringBootApplication 是一個便利註釋,新增了以下所有內容:

  • @Configuration 標記該類作為應用程式上下文的bean定義的源。
  • @EnableAutoConfiguration 告訴Spring Boot開始根據類路徑設定,其他bean和各種屬性設定新增bean。
  • 通常你會新增@EnableWebMvc一個Spring MVC應用程式,但Spring Boot會在類路徑上看到spring-webmvc時自動新增它。這會將應用程式標記為Web應用程式並啟用關鍵行為,例如設定DispatcherServlet。
  • @ComponentScan告訴Spring在包中尋找其他元件,配置和服務,允許它找到控制器。

main()方法使用Spring Boot的SpringApplication.run()方法來啟動應用程式。您是否注意到沒有一行XML?也沒有web.xml檔案。此Web應用程式是100%純Java,您無需處理配置任何管道或基礎結構。

構建可執行的JAR

您可以使用Gradle或Maven從命令列執行該應用程式。或者,您可以構建一個包含所有必需依賴項,類和資源的可執行JAR檔案,並執行該檔案。這使得在整個開發生命週期中,跨不同環境等將服務作為應用程式釋出,版本和部署變得容易。

如果您使用的是Gradle,則可以使用執行該應用程式./gradlew bootRun。或者您可以使用構建JAR檔案./gradlew build。然後你可以執行JAR檔案:

java -jar build/libs/gs-uploading-files-0.1.0.jar

如果您使用的是Maven,則可以使用該應用程式執行該應用程式./mvnw spring-boot:run。或者您可以使用構建JAR檔案./mvnw clean package。然後你可以執行JAR檔案:

java -jar target/gs-uploading-files-0.1.0.jar

上面的過程將建立一個可執行的JAR。您也可以選擇構建經典WAR檔案

它執行接收檔案上傳的伺服器端部分。顯示記錄輸出。該服務應在幾秒鐘內啟動並執行。

在伺服器執行時,您需要開啟瀏覽器並訪問http://localhost:8080/以檢視上傳表單。選擇一個(小)檔案並按“Upload”,您應該從控制器中看到成功頁面。選擇一個太大的檔案,你會得到一個醜陋的錯誤頁面。

然後,您應該在瀏覽器視窗中看到類似的內容:

You successfully uploaded <name of your file>!

測試您的應用程式

在我們的應用程式中有多種方法可以測試此特定功能。這是一個利用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());
    }

}

在那些測試中,我們使用各種模擬來設定與Controller的互動,以及StorageService使用Servlet容器本身的MockMultipartFile互動。

概要

恭喜!您剛剛編寫了一個使用Spring處理檔案上傳的Web應用程式。

相關文章