Skip to content

SpringMVC教程 - 13 文件上传与下载

在Web应用中,文件上传和下载是非常常见的功能。比如用户上传头像、上传文档、下载报告等。

SpringMVC6 为我们提供了方便的文件上传和下载功能,基于 Servlet 5.0+ 的原生文件上传支持,无需额外引入 Apache commons-fileupload 组件,只需要配置标准的 StandardServletMultipartResolver 即可。

13.1 文件上传

1 web.xml配置

DispatcherServlet 中添加 multipart-config 配置,如下:

xml
<!-- DispatcherServlet 配置 -->
<servlet>
    <servlet-name>springmvc</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <load-on-startup>1</load-on-startup>

    <!-- 开启文件上传支持(Servlet 原生配置) -->
    <multipart-config>
        <!-- 临时目录(文件太大时会写入这里) -->
        <location>/tmp</location>
        <!-- 单个文件最大 50MB -->
        <max-file-size>52428800</max-file-size>
        <!-- 设置整个表单上传的所有文件总大小的最大值 100MB -->
        <max-request-size>104857600</max-request-size>
        <!-- 超过 2MB 才写入磁盘 -->
        <file-size-threshold>2097152</file-size-threshold>
    </multipart-config>
</servlet>

2 配置上传解析器

在 SpringMVC 配置文件 spring-mvc.xml 中加入:

xml
<!-- 使用 Servlet 5.0 原生上传机制 -->
<bean id="multipartResolver"
      class="org.springframework.web.multipart.support.StandardServletMultipartResolver"/>
  • 这是Spring6的配置,如果是 Spring5 ,需要配置 CommonsMultipartResolver,这里就不介绍了 。
  • StandardServletMultipartResolver 的主要作用,就是把原本普通的 HTTP 请求解析成带有 MultipartFile 的请求对象,从而让我们在 SpringMVC 控制器中,能够直接拿到 MultipartFile 对象进行处理,待会 Controller 中会使用 MultipartFile 接收上传的文件,非常的方便。

基础配置已经配置好了,下面开始编写页面和 Controller,完成上传功能。

3 编写页面

编写一个 upload.html 的 thymeleaf 模板,用来演示上传文件,在页面上添加一个上传文件的控件:

html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"><!-- 引入Thymeleaf命名空间 -->
<head>
  <meta charset="UTF-8">
  <title>上传文件</title>
</head>
<body>

<form id="uploadForm">
  <input type="file" name="file">
  <button type="button" onclick="upload()">上传</button>
</form>

<div id="result"></div>

<script>
  function upload() {
    // 这里需要注意,FormData对象会自动添加Content-Type: multipart/form-data
    // 如果通过提交表单的方式上传文件,表单需要设置enctype="multipart/form-data"
    let data = new FormData(document.getElementById("uploadForm"));

    fetch("/file/upload", { method: "POST", body: data })
            .then(response => {
              console.log("收到响应,状态码:", response.status);
              if (response.status !== 200) {
                // 上传失败,将返回的错误信息显示在result div中
                document.getElementById("result").innerText = "上传失败";
                return;
              }

              return response.text();  // 将响应转换为文本,后续then中可以使用
            })
            .then(data => {
              // 解析 JSON 字符串为 JavaScript 对象
              let jsonData = JSON.parse(data);
              // 检查返回的状态码是否为 200
              if (jsonData.code !== 200) {
                // 上传失败,将返回的错误信息显示在result div中
                document.getElementById("result").innerText = jsonData.message;
              }
              else {
                // 上传成功,将返回的文本显示在result div中,可以进行不同的处理
                document.getElementById("result").innerText = "上传成功";
              }
            });
  }
</script>

</body>
</html>
  • 点击上传按钮,触发调用 JavaScript 的 upload() 函数进行上传操作,最终得到服务器返回的信息,显示在页面上。
  • 这里使用 Ajax 方式来处理,不是直接提交表单,如果直接表单,会触发页面跳转。

4 编写Controller接口

重点来了,编写一个 Controller 处理上传的请求:

java
package com.foooor.hellospringmvc.controller;

import com.foooor.hellospringmvc.common.Result;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;

@Controller
@RequestMapping("/file")
public class FileController {

    /**
     * 跳转到上传文件页面
     */
    @RequestMapping("/test-upload")
    public String upload() {
        return "upload";
    }

    /**
     * 处理上传文件
     * @RequestParam("file") 与页面的 name="file" 对应
     */
    @PostMapping("/upload")
    @ResponseBody
    public Result upload(@RequestParam("file") MultipartFile file) throws IOException {

        if (file.isEmpty()) {
            return Result.error(400, "上传失败:文件不能为空");
        }

        // 获取文件名称
        String fileName = file.getOriginalFilename();

        // 这里是上传到当前磁盘根目录下的/upload目录,也可以指定磁盘的其他目录,例如 D:\\upload
        String uploadDir = "/upload";
        File dir = new File(uploadDir);
        // 如果upload目录不存在,则创建
        if (!dir.exists()) {
            dir.mkdirs();
        }

        // 保存文件
        file.transferTo(new File(dir, fileName));

        // 返回成功结果
        return Result.success("上传成功:" + file.getOriginalFilename());
    }
}
  • 首先是定义了两个接口,一个跳转到上传页面,一个处理文件上传;
  • 上传文件首先由 Servlet 容器接收并存储(小文件在内存,大文件写临时目录);Spring 通过 MultipartFile 接管后,注入到 Controller 的方法参数。
  • 在 Controller 中需要调用 transferTo() 才会写入你指定的永久位置,否则文件只停留在临时存储。当然,你也可以通过 MultipartFile 获取输入流,自己处理流,自己保存。本质上就是流的处理。

5 测试

启动项目,使用 http://localhost:8080/file/test-upload 访问上传页面:

选择文件后,点击上传,就可以将文件上传到指定的文件夹了:

13.2 文件下载

文件下载更简单一些。

1 在页面添加下载链接

在页面添加下载链接:

html
<!-- 文件下载,下载abc.jpg文件 -->
<a th:href="@{/file/download/abc.jpg}">文件下载</a>
  • 就是添加一个超链接而已,下载 abc.jpg 文件

2 编写Controller接口

在 Controller 中编写 /file/download 接口

java
package com.foooor.hellospringmvc.controller;

import com.foooor.hellospringmvc.common.Result;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

@Controller
@RequestMapping("/file")
public class FileController {

    /**
     * 跳转到上传文件页面
     * 略...
     */

    /**
     * 处理上传文件
     * 略...
     */

    /**
     * 下载文件
     */
    @GetMapping("/download/{fileName}")
    public ResponseEntity download(@PathVariable("fileName") String fileName, HttpServletResponse response) throws IOException {
        // 基本的非法路径过滤,防止 ../../ 切换目录
        if (fileName.contains("..")) {
            return ResponseEntity.badRequest().body(null);
        }

        // 找到下载的文件
        File file = new File("/upload/" + fileName);
        if (!file.exists()) {
            return ResponseEntity.status(404).body("下载失败:文件不存在");
        }
        // 会使用流式传输,不会一次性加载到内存中,适用于大文件下载
        Resource resource = new FileSystemResource(file);

        HttpHeaders headers = new HttpHeaders();
        // 设置相应内容类型为二进制流
        headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
        // 设置下载头,指定下载时候的文件名
        headers.setContentDispositionFormData("attachment", URLEncoder.encode(fileName, StandardCharsets.UTF_8));

        return ResponseEntity.ok().headers(headers).contentLength(resource.contentLength()).body(resource);
    }
}
  • 通过 @PathVariable("fileName") 读取路径中的文件名称,然后找到指定目录下的文件;
  • 设置 response 的响应头信息,通过 ResponseEntity 返回 文件即可;
  • 这里使用 FileSystemResource 包装一下 ,下载文件时不需要一次性把整个文件加载到内存里,而是 按流(streaming)方式发送给客户端,可以避免大内存占用。

我们也可以使用 response 获取输出流,直接输出:

java
/**
 * 下载文件
 */
@GetMapping("/download/{fileName}")
public void download(@PathVariable String fileName, HttpServletResponse response) throws IOException {
    // 基本的非法路径过滤,防止 ../../ 切换目录
    if (fileName.contains("..")) {
        response.setStatus(404);
        response.getWriter().write("文件不存在");
        return;
    }

    File file = new File("/Users/kang/Documents/upload/" + fileName);
    if (!file.exists()) {
        response.setStatus(404);
        response.getWriter().write("文件不存在");
        return;
    }

    response.setContentType("application/octet-stream");
    response.setHeader("Content-Disposition",
            "attachment;filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8));

    try (InputStream in = new FileInputStream(file);
         OutputStream out = response.getOutputStream()) {

        byte[] buffer = new byte[8192];
        int len;
        while ((len = in.read(buffer)) != -1) {
            out.write(buffer, 0, len);
        }
        out.flush();
    }
}
  • 上面直接通过 response.getOutputStream() 获取输出流,然后进行输出;
  • response 获取到的 OutputStream 不用手动关闭(手动关闭会报错),会自动关闭。
内容未完......