跳至主要內容

SpringBoot 文件上传下载

zedo2023年4月16日Java文件传输springboot大约 4 分钟约 1120 字

SpringBoot 文件上传下载

文件上传

以下给出 Spring Boot 上传文件的常见方式。

使用 @RequestParam 注解

后端代码:

@PostMapping("/upload")
public Result<?> upload1(@RequestParam("file") MultipartFile file) {
    // 处理上传的文件
    return "上传成功";
}

Web 前端需要定义 input 标签

<input type="file" id="fileInput" />

上传时将文件加到 FormDataopen in new window 对象:

const fileInput = document.querySelector<HTMLInputElement>("#fileInput");

function handleFileUpload() {
    const file = fileInput?.files?.[0];
    if (!file) return;
    const formData = new FormData();
    formData.append("file", file);
    fetch("/upload", {
        method: "POST",
        body: formData,
    }).then((response) => {
        console.log(response);
    });
}

fileInput?.addEventListener("change", handleFileUpload);

一般组件库上传组件的 FormData 存放文件的 key 为 "file" ,可以在调试网络面板中查看实际请求并做修改。

从请求中获取

使用 Spring 的 MultipartHttpServletRequest 类来处理上传的文件。

@PostMapping("/upload")
public String handleFileUpload(HttpServletRequest request) {
    // 检查文件是否为空
    MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
    MultipartFile file = multipartRequest.getFile("file");
    if (file.isEmpty()) {
        return "上传失败,请选择文件";
    }

    // 保存文件到服务器
    try {
        byte[] bytes = file.getBytes();
        Path path = Paths.get("/path/to/save/file/" + file.getOriginalFilename());
        Files.write(path, bytes);
    } catch (IOException e) {
        e.printStackTrace();
        return "上传失败:" + e.getMessage();
    }
    return "上传成功";
}

在此示例中,@PostMapping 注解表示此方法将处理 HTTP POST 请求,并将请求映射到 /upload 路径。我们首先将 HttpServletRequest 转换为 MultipartHttpServletRequest,然后使用 getFile("file") 方法获取要上传的文件。您可以根据需要更改这些值。

在方法主体中,我们首先检查文件是否为空,然后使用 getBytes() 方法获取文件内容并将其保存到服务器。请注意,此示例假定您的服务器上已经有一个名为 /path/to/save/file/ 的目录,需要根据实际情况更改此路径。

保存文件也可以使用 File.transferTo 方法:

String savePath = "/path/to/save/file/" + file.getOriginalFilename();
try {
    file.transferTo(new File(savePath));
} catch (IOException e) {
    e.printStackTrace();
}

使用 @RequestPart 注解

后端代码:

@PostMapping("/upload")
public  Result<?> upload2(@RequestPart("file") MultipartFile file, @RequestPart("name") String name) {
    // 处理上传的文件和其他参数
    // 处理 URL 编码(主要针对中文)
    name = URLDecoder.decode(path, StandardCharsets.UTF_8);
    return "上传成功";
}

这里的参数除了 file 还有另一个 name,注意如果 name 包含中文,需要用 encodeURIComponentopen in new window 进行编码,否则后端得到的是乱码。

java 中的 URLDecoder.decode(s, StandardCharsets.UTF_8) 对应于 js 中的 decodeURIComponent() 方法。

前端代码:

const fileInput = document.querySelector<HTMLInputElement>("#fileInput");

function handleFileUpload() {
    const file = fileInput?.files?.[0];
    if (!file) return;
    const formData = new FormData();
    formData.append("file", file);
    formData.append("name", encodeURIComponent("中文" + file.name));
    fetch("/upload", {
        method: "POST",
        body: formData,
    })
        .then((response) => {
            console.log(response);
        })
        .catch((err) => {
            console.error(err);
        });
}

fileInput.addEventListener("change", handleFileUpload);

文件下载

@RestController
@RequestMapping("/files")
public class MinioController extends BaseController {
    @GetMapping("/download/**")
    public void download(HttpServletRequest request, HttpServletResponse response) {
        String path = request.getRequestURI().replace("/files/download", "");
        File file = new File(path);
        // todo 检查文件名合法性
        if (!file.exists()) {
            System.out.println("文件不存在");
            return;
        }
        String realFilename = file.getName();

        response.setCharacterEncoding("UTF-8");
        response.setHeader(
                HttpHeaders.CONTENT_DISPOSITION,
                "attachment;filename=" + URLEncoder.encode(realFilename, StandardCharsets.UTF_8)
        );
        // 可设置缓存策略:etag、lastModified 等
        try (InputStream inputStream = new FileInputStream(file)) {
            // 通用 MIME 类型
            response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE);

            ServletOutputStream outputStream = response.getOutputStream();
            byte[] buffer = new byte[1024];
            int len;

            // 从输入流中读取定量的字节,并存储在缓冲区字节数组中,读到末尾返回 -1
            while ((len = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, len);
            }
            // try-with-resource 不需要手动关闭 inputStream
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

response.setContentType 也可替换为 response.setHeader(HttpHeaders.CONTENT_TYPE, xxx)

在 HTTP 响应头中,Content-Disposition 有两个可选值:

当 Content-Disposition 设置为 "inline" 时,浏览器将尝试在页面中显示文件,如果浏览器支持该文件类型和插件,则文件将直接嵌入到页面中;否则,浏览器将提示用户下载该文件。

当 Content-Disposition 设置为 "attachment" 时,浏览器将始终提示用户下载该文件。

后面的 filename 指定了下载文件的名称,不过这个响应头只是一种约定,不是 HTTP 标准规范中定义的标头。

前端代码:

// 下载保存文件
function saveFile(blob: Blob, filename: string) {
    const link = document.createElement("a");
    link.href = URL.createObjectURL(blob);
    link.download = filename;
    link.click();
    // 释放内存中的 blob
    URL.revokeObjectURL(link.href);
}

如果使用 axios

function downloadApi(filepath: string) {
    axios
        .get("/api/files/download/" + filepath, {
            responseType: "blob",
        })
        .then(({ headers, data }) => {
            const cd: string = headers["content-disposition"];
            const type = headers["content-type"].split(";")[0];
            const blob = new Blob([data] /*, { type } */);
            const filename = decodeURI(cd.replace("attachment;filename=", ""));
            saveFile(blob, filename);
        });
}

如果使用 fetch

function downloadApi(filepath: string) {
    fetch("/api/files/download/" + filepath, {
        headers: {},
    })
        .then(async (response) => ({
            name:
                response.headers
                    .get("content-disposition") // 可以大写
                    ?.replace("attachment;filename=", "") || "filename",
            blob: await response.blob(),
        }))
        .then(({ blob, name }) => {
            saveFile(blob, name);
        });
}
2023-9-14 更新
重启 search-pro,css 样式调整