跳过正文

如何在 Spring Boot 中上传文件

Spring Spring-Boot File-Upload Thymeleaf
作者
Harpal Singh
Software Engineer
翻译者
Namastecode
目录

1. 介绍

在这个简短的教程中,我们将学习如何使用 Spring Boot 上传文件。

让我们看看如何通过表单提交来实现。

2. 为什么文件上传很重要?

上传文件是现代 web 应用程序中非常常见的用例,例如,在表单中附加文档或上传个人资料照片。虽然这一过程简单易懂,但正确处理文件仍然面临一些挑战。例如,允许用户上传文件时不对文件大小进行限制可能会使我们的应用程序不堪重负或消耗所有可用存储。因此,在接受任何文件之前,我们需要验证其类型和内容,并优雅地处理故障。

因此,在我们的应用程序中,我们将使用 Spring Boot 提供的选项来使其更加健壮,强制限制文件大小,并提供反馈,以便用户理解发生了什么。

3. 上传流程

以下序列图直观地展示了用户、浏览器与我们的 Spring Boot 应用程序之间的交互:

sequenceDiagram participant User as User participant Browser as Browser participant Controller as UploadController participant Filesystem as File System User->>Browser: Selects file and submits form Browser->>Controller: POST /upload (multipart/form-data) Controller->>Controller: Check if file is empty alt File is valid Controller->>Filesystem: Save file bytes to disk Controller-->>Browser: Redirect to / with success message else File is invalid Controller-->>Browser: Redirect to / with error message end Browser->>User: Display upload status message


4. Maven 配置

让我们首先引入我们的依赖项。确保获取最新版的 spring-boot-starter-webspring-boot-starter-thymeleaf

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>3.4.2</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
        <version>3.4.2</version>
    </dependency>
</dependencies>

对于一个新项目,最好从 Spring Initializr 开始,并包含以下依赖项:

  • Spring Web: MVC框架和嵌入式Tomcat
  • Thymeleaf: 现代的服务器端Java模板引擎,适用于网页和独立环境。
提示: 使用 Spring Boot DevTools 以实现开发期间的自动重启。

5. 实现文件上传

让我们开始实施我们的应用程序。

5.1 在Thymeleaf中的上传表单

首先,我们将创建一个包含基本表单的Thymeleaf模板,表单中有一个文件输入和一个按钮,用于将表单提交到我们的端点。

我们需要在应用程序的资源文件夹中创建一个名为 templates 的文件夹,并在其中创建一个 file-upload.html 文件:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mini.css/3.0.1/mini-default.min.css">
</head>
<body>
    <div class="container">
        <header>
            <h2>File Upload</h2>
        </header>
        <main>
            <form action="#" th:action="@{/upload}" method="post" enctype="multipart/form-data">
                <div class="input-group vertical">
                    <input type="file" name="file" required>
                    <button type="submit">Upload</button>
                </div>
            </form>
            <div th:if="${message}" class="alert"
                 th:classappend="${message.contains('Success') ? 'alert-success' : 'alert-error'}"
                 th:text="${message}"></div>
        </main>
    </div>
</body>
</html>
一个非常常见的错误是忘记添加 enctype=multipart/form-data 属性。如果没有这个属性,浏览器将不会以 multipart 数据的形式发送文件,上传将不会如预期般工作。

在表单之后,我们添加了一个占位符来显示消息。

在我们的模板中,我们使用 mini.css,这样我们就不必处理应用程序的样式,同时又能够避免浏览器的默认外观。

前端部分就这些。

5.2. 存储配置

接下来,我们需要决定将用户上传的文件保存在哪里。让我们在 application.properties 文件中添加一个 files.upload-dir 属性:

server.port=8090
files.upload-dir=./uploadedfiles

这将是我们保存文件的地点。

5.3 文件上传控制器

现在,我们将实现一个名为 FileUploadController 的控制器,该控制器暴露一个端点,将我们创建的模板提供给浏览器:

@Controller
public class FileUploadController {

    @GetMapping("/")
    public String fileUpload() {
        return "file-upload";
    }

}

我们只需要确保我们返回的字符串与我们之前创建的模板名称相匹配。

接下来,在同一个控制器中,我们将创建一个 POST /upload 端点来处理文件上传请求。这个端点将使用 MultipartFile 作为请求参数来获取文件。MultipartFile 是一个扩展了 InputStreamSource 的接口:

@Value("${files.upload-dir}")
private String uploadDir;

@PostMapping("/upload")
public String handleUpload(
    @RequestParam("file") MultipartFile file,
    RedirectAttributes redirectAttributes
) {
    if (file.isEmpty()) {
        redirectAttributes.addFlashAttribute("message", "Please select a file");
        return "redirect:/";
    }
    try {
        String filename = StringUtils.cleanPath(file.getOriginalFilename());
        Path destination = Paths.get(uploadDir, filename);
        Files.createDirectories(destination.getParent());
        file.transferTo(destination);
        redirectAttributes.addFlashAttribute("message", "Success: " + filename + " (" + file.getSize() + " bytes)");
        return "redirect:/";
    } catch (IOException e) {
        redirectAttributes.addFlashAttribute("message", "Error: " + e.getMessage());
        return "redirect:/";
    }
}

简而言之,首先,我们从输入的 file 对象中获取文件名。然后,我们使用 StringUtils.cleanPath 创建保存 destination 所需的文件夹,以防止 简单 的路径遍历攻击。接着,我们将传入的文件数据保存到 destination,并发送成功消息。如果出现失败,我们会相应地通知用户,让他们了解发生了什么。

在控制器中,我们使用 @Value 注入了上传目录,并实现了一个 @PostMapping 方法,该方法接收 MultipartFile 和一个 RedirectAttributes 对象。Spring MVC 中的 RedirectAttributes 接口用于在重定向期间传递数据。具体而言,我们使用 addFlashAttribute 暂时将消息保存在会话中,以便在重定向后,当模板被处理时,可以显示正确的反馈。

StringUtils.cleanPath 不应在安全上下文中依赖。应采用其他机制来防止路径遍历问题。

现在,如果我们使用 mvn spring-boot:run 启动应用程序并访问 localhost:8090,我们应该能够看到我们的表单,并能够上传小文件。

6. 处理大文件

我们现在可以顺利上传小文件了。但如果文件大小超过 1MB(默认限制),我们的应用程序将会失败,浏览器可能会显示错误。如果我们希望允许用户上传更大的文件,我们可以配置更高的阈值。

6.1 配置最大文件大小

要配置允许的最大文件大小,我们需要在 application.properties 中更改 spring.servlet.multipart.max-file-sizespring.servlet.multipart.max-request-size 属性:

spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=11MB
常见错误: 请求大小必须大于文件大小,以便考虑表单数据的开销。

这单凭这一点是不够的。我们还需要配置我们的 Servlet 容器,以便在输入请求超过其默认最大大小值之前,不会中止连接,而是等待 Spring 的 multipart 处理介入。
我们的 Spring Boot 应用默认使用嵌入式的 Tomcat。我们可以配置它以接受更大的(或有效无限大小的)POST 请求,这样大小检查将仅由 Spring 的 multipart 配置处理:

server.tomcat.max-http-form-post-size=-1
server.tomcat.max-swallow-size=-1
安全问题: 将限制设置为 -1 通常不推荐,因为这可能允许几乎无限的请求大小,从而导致服务拒绝(DoS)攻击。理想情况下,我们应将请求的大小限制在适合应用程序用例的合理值。

现在,如果我们尝试上传大于10MB的文件,Spring MVC会阻止上传,但默认行为可能仍会导致一个未处理的错误页面(例如显示与MaxUploadSizeExceededException相关的500错误的Whitelabel错误页面)。如果我们想自定义这种行为,以使用户界面的交互更加流畅,我们可以添加自定义错误处理。

6.2 自定义错误处理

在后台,Spring MVC 当文件超过配置的大小限制时,会抛出 MaxUploadSizeExceededException。我们可以创建一个 @ControllerAdvice 来全局捕获 MultipartException 并优雅地处理这些错误。我们捕获 MultipartException 是因为 MaxUploadSizeExceededException 继承了它,这样我们就可以处理其他潜在的与 multipart 相关的错误:

@ControllerAdvice
public class FileUploadExceptionHandler {

    @ExceptionHandler(MultipartException.class)
    public String handleSizeExceeded(RedirectAttributes attrs) {
        attrs.addFlashAttribute("message",
            "File exceeds size limit!");
        return "redirect:/";
    }

}

配置 @ControllerAdvice 提供了一种集中处理多个控制器中的异常的方法,并返回用户友好的错误消息或适当的反馈进行重定向。

7. 生产环境的提示

虽然本教程解释了简单的文件上传,但还有一些未在此处涵盖的生产环境的额外考虑因素。

首先,我们并不限制允许上传的文件类型。我们可以通过简单的文件扩展名检查来解决这个问题,或者实施更深入的验证,例如检查 MIME 类型或扫描病毒和恶意软件。

在预期有大文件的生产环境中,考虑使用流式处理方法,以避免将整个文件加载到服务器内存中。 另外,可以考虑从客户端使用分块上传,将文件拆分为更小的部分,分别上传,然后在服务器端重新组装。

8. 结论

在这篇文章中,我们学习了如何使用一个简单的 HTML form、Thymeleaf 和 Spring MVC 控制器在 Spring Boot 中实现文件上传。我们还学习了如何配置大文件的限制,并定制大小异常的错误处理过程。

您可以在 GitHub 上找到完整的代码。

相关文章

在 Spring Framework 6 中使用 Rest Client 进行 Http 请求
Spring Restclient HTTP Rest Openai
在 Spring 中获取 Properties 文件中定义的值
Spring Properties Basics
在Spring应用程序中返回HTTP 4XX错误
Spring HTTP