ZetCode

Spring Boot 上传文件

最后修改于 2023 年 7 月 29 日

在本文中,我们将展示如何使用 Spring Boot 框架上传单个文件。

Spring 是一个流行的 Java 应用程序框架,而 Spring Boot 是 Spring 的一个演进,它有助于轻松创建独立的、生产级别的基于 Spring 的应用程序。

HTML 表单编码类型

对于 POST 请求,有三种 HTML 表单编码类型

application/x-www-form-urlencoded 是默认编码,其中值以 key-value 元组的形式编码,用 & 分隔。 = 字符用于分隔 key 和 value。非字母数字字符会被百分比编码。 这种编码类型不适用于二进制文件。

multipart/form-data 用于非 ascii 数据和二进制文件。 input 元素的 type 属性设置为 file

text/plain 用于调试。

Spring 上传文件示例

在下面的示例中,我们有一个 web 表单,用于选择要上传到服务器的文件。 该文件被上传到 /var/www/upload/ 目录。

注意: 为了简单起见,我们不包括检查文件是否确实是图像的代码。 这是一个复杂的代码,可以防止上传 web shell。

上传目录

/var/www/ 目录是 Debian Linux 中用于 web 内容的标准目录。

$ ls -ld /var/www/upload/
drwxrwxr-x 2 www-data www-data 4096 Dec  3 14:29 /var/www/upload/

我们将文件上传到 /var/www/upload/ 目录。 www-data 组中的用户可以修改该目录中的文件。 因此,运行 web 服务器的用户必须在此组中。

应用程序

以下是 Spring Boot web 应用程序的源代码。

build.gradle
...
src
├───main
│   ├───java
│   │   └───com
│   │       └───zetcode
│   │           │   Application.java
│   │           ├───controller
│   │           │       MyController.java
│   │           ├───exception
│   │           │       StorageException.java
│   │           └───service
│   │                   StorageService.java
│   └───resources
│       │   application.properties
│       └───static
│               failure.html
│               index.html
│               success.html
└───test
    └───java

这是 Spring 应用程序的项目结构。

build.gradle
plugins {
    id 'org.springframework.boot' version '3.1.1'
    id 'io.spring.dependency-management' version '1.1.0'
    id 'java'
}

group = 'com.zetcode'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
    useJUnitPlatform()
}

这是 Gradle 构建文件。

com/zetcode/controller/MyController.java
package com.zetcode.controller;

import com.zetcode.exception.StorageException;
import com.zetcode.service.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

@Controller
public class MyController {

    private final StorageService storageService;

    @Autowired
    public MyController(StorageService storageService) {
        this.storageService = storageService;
    }

    @RequestMapping(value = "/doUpload", method = RequestMethod.POST,
            consumes = {"multipart/form-data"})
    public String upload(@RequestParam MultipartFile file) {

        storageService.uploadFile(file);

        return "redirect:/success.html";
    }

    @ExceptionHandler(StorageException.class)
    public String handleStorageFileNotFound(StorageException e) {

        return "redirect:/failure.html";
    }
}

MyController 从请求中读取文件并将其保存到选定的目录中。

private final StorageService storageService;

@Autowired
public MyController(StorageService storageService) {
    this.storageService = storageService;
}

StoreageService 将文件存储在磁盘上。

@RequestMapping(value = "/doUpload", method = RequestMethod.POST,
    consumes = {"multipart/form-data"})
public String upload(@RequestParam MultipartFile file) {

upload 方法映射到 doUpload URL 模式。 由于我们将数据发送到服务器,因此我们使用 POST 请求。 请求参数具有 MultipartFile 类型。

return "redirect:/success.html";

我们在成功上传文件后显示一条消息。

@ExceptionHandler(StorageException.class)
public String handleStorageFileNotFound(StorageException e) {

    return "redirect:/failure.html";
}

我们有一个针对 StorageException 的处理程序。

com/zetcode/exception/StorageException.java
package com.zetcode.exception;

public class StorageException extends RuntimeException {

    public StorageException(String message) {
        super(message);
    }

    public StorageException(String message, Throwable cause) {
        super(message, cause);
    }
}

这是我们的自定义 StorageException。 当文件无法存储在文件系统上时,会抛出它。

com/zetcode/service/StorageService.java
package com.zetcode.service;

import com.zetcode.exception.StorageException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;

@Service
public class StorageService {

    @Value("${upload.path}")
    private String path;

    public void uploadFile(MultipartFile file) {

        if (file.isEmpty()) {

            throw new StorageException("Failed to store empty file");
        }

        try {
            var fileName = file.getOriginalFilename();
            var is = file.getInputStream();

            Files.copy(is, Paths.get(String.format("%s/%s", path, fileName)),
                    StandardCopyOption.REPLACE_EXISTING);
        } catch (IOException e) {

            var msg = String.format("Failed to store file %s", file.getName());

            throw new StorageException(msg, e);
        }
    }
}

StorageService 从输入流复制数据并将其保存在磁盘上。

@Value("${upload.path}")
private String path;

我们使用 @Value 注解从 application.properties 文件中读取上传目录。

if (file.isEmpty()) {
    throw new StorageException("Failed to store empty file");
}

我们使用 isEmpty 方法确保选择了文件。

var fileName = file.getOriginalFilename();

我们使用 getOriginalFilename 方法获取文件名。

var is = file.getInputStream();

我们使用 getInputStream 方法获取输入流。

Files.copy(is, Paths.get(String.format("%s/%s", path, fileName)),
    StandardCopyOption.REPLACE_EXISTING);

该文件从输入流源复制到目标目录,使用 Files.copy

resources/application.properties
upload.path=/var/www/upload/

application.properties 中,我们有一个 upload.path 属性,用于指定上传目录。

resources/static/index.html
<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Uploading file</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
    </head>
    <body>
        <h1>Uploading file</h1>

        <form action="/doUpload" method="post" enctype="multipart/form-data">
            <label>Enter file</label>
            <input type="file" name="file">
            <button type="submit">Upload</button>
        </form>
    </body>
</html>

这是主页。 它是一个静态文件,位于 src/main/resources/static 目录中。 它包含一个表单,用于选择一个文件并将其发送到 Spring 应用程序。

<form action="/doUpload" method="post" enctype="multipart/form-data">

我们选择了 doUpload URL 模式。 由此表单创建的请求将由 Spring 控制器处理。 enctype 属性指定 multipart/form-data 编码类型,这是使用 HTML 表单上传文件所必需的。

<input type="file" name="file">

input 标签的 type 属性允许用户选择一个文件。

<button type="submit">Upload</button>

最后,这是一个提交按钮。

resources/static/success.html
<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Success</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
    </head>
    <body>
        <p>File successfully uploaded</p>
    </body>
</html>

当文件成功上传到服务器时,将显示 success.html

resources/static/failure.html
<!DOCTYPE html>
<htm lang="en"l>
    <head>
        <title>Failure</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
    </head>
    <body>
        <p>Failed to upload file</p>
    </body>
</html>

当文件上传失败时,将显示 failure.html

com/zetcode/Application.java
package com.zetcode;

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 Boot 应用程序。

在本文中,我们学习了如何在 Spring 应用程序中上传文件。

作者

我叫 Jan Bodnar,是一位充满激情的程序员,拥有丰富的编程经验。自 2007 年以来,我一直在撰写编程文章。 迄今为止,我撰写了超过 1,400 篇文章和 8 本电子书。 我拥有超过十年的编程教学经验。

列出 所有 Spring Boot 教程