SpringBoot返回文件让前端下载的几种方式

01 背景

在后端开发中,通常会有文件下载的需求,常用的解决方案有两种:

  1. 不通过后端应用,直接使用nginx直接转发文件地址下载(适用于一些公开的文件,因为这里不需要授权)
  2. 通过后端进行下载,同时进行一些业务处理

本篇主要以方法2进行介绍,方法2的原理步骤如下:

  1. 读取文件,得到文件的字节流
  2. 将字节流写入到响应输出流中
02 一次性读取到内存,通过响应输出流输出到前端
复制代码
    @GetMapping("/file/download")
    public void fileDownload(HttpServletResponse response, @RequestParam("filePath") String filePath) {
        File file = new File(filePath);
        if (!file.exists()) {
            throw new BusinessException("当前下载的文件不存在,请检查路径是否正确");
        }

        // 将文件写入输入流
        try (InputStream is = new BufferedInputStream(Files.newInputStream(file.toPath()))) {
            
            // 一次性读取到内存中
            byte[] buffer = new byte[is.available()];
            int read = is.read(buffer);

            // 清空 response
            response.reset();
            response.setCharacterEncoding("UTF-8");

            // Content-Disposition的作用:告知浏览器以何种方式显示响应返回的文件,用浏览器打开还是以附件的形式下载到本地保存
            // attachment表示以附件方式下载   inline表示在线打开   "Content-Disposition: inline; filename=文件名.mp3"
            // filename表示文件的默认名称,因为网络传输只支持URL编码的相关支付,因此需要将文件名URL编码后进行传输,前端收到后需要反编码才能获取到真正的名称
            response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(file.getName(), "UTF-8"));

            // 告知浏览器文件的大小
            response.addHeader("Content-Length", "" + file.length());

            OutputStream outputStream = new BufferedOutputStream(response.getOutputStream());
            response.setContentType("application/octet-stream");
            outputStream.write(buffer);
            outputStream.flush();
            outputStream.close();

        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

适用于小文件,如果文件过大,一次性读取到内存中可能会出现oom的问题

02 将文件流通过循环写入到响应输出流中(推荐)
复制代码
    @GetMapping("/file/download")
    public void fileDownload(HttpServletResponse response, @RequestParam("filePath") String filePath) {
        File file = new File(filePath);
        if (!file.exists()) {
            throw new BusinessException("当前下载的文件不存在,请检查路径是否正确");
        }

        // 清空 response
        response.reset();
        response.setCharacterEncoding("UTF-8");

        response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(file.getName(), "UTF-8"));
        response.setContentType("application/octet-stream");

        // 将文件读到输入流中
        try (InputStream is = new BufferedInputStream(Files.newInputStream(file.toPath()))) {
            
            OutputStream outputStream = new BufferedOutputStream(response.getOutputStream());
            
            byte[] buffer = new byte[1024];
            int len;

            //从输入流中读取一定数量的字节,并将其存储在缓冲区字节数组中,读到末尾返回-1
            while((len = is.read(buffer)) > 0){
                outputStream.write(buffer, 0, len);
            }

            outputStream.close();

        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }
03 从网络上获取文件并返回给前端
复制代码
    @GetMapping("/net/download")
    public void netDownload(HttpServletResponse response, @RequestParam("fileAddress") String fileAddress, @RequestParam("filename") String filename) {

        try {
            URL url = new URL(fileAddress);
            URLConnection conn = url.openConnection();
            InputStream inputStream = conn.getInputStream();

            response.reset();
            response.setContentType(conn.getContentType());

            response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(filename, "UTF-8"));

            byte[] buffer = new byte[1024];
            int len;
            
            OutputStream outputStream = response.getOutputStream();
            
            while ((len = inputStream.read(buffer)) > 0) {
                outputStream.write(buffer, 0, len);
            }
            
            inputStream.close();
            
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        
    }
04 从网络上获取文本并下载到本地
复制代码
    @GetMapping("/netDownloadLocal")
    public void downloadNet(@RequestParam("netAddress") String netAddress, @RequestParam("filepath") String filepath) {

        try {
            URL url = new URL(netAddress);
            URLConnection conn = url.openConnection();
            InputStream inputStream = conn.getInputStream();

            FileOutputStream fileOutputStream = new FileOutputStream(filepath);
            int byteread;
            byte[] buffer = new byte[1024];

            while ((byteread = inputStream.read(buffer)) != -1) {
                fileOutputStream.write(buffer, 0, byteread);
            }

            fileOutputStream.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }
05 总结

一定要搞清楚InputStreamOutputStream的区别,如果搞不清楚的,可以和字符流进行映射,InputStream -> Reader,OutPutStream -> Writer,换成这样你就知道读取内容需要使用Reader,写入需要使用Writer了。

返回给前端的是输出流,不需要你显示的去返回(return response;),这样会报错

相关推荐
Coder_Shenshen2 小时前
西门子S7CommPlus协议鉴权算法原理与流程详解
网络·后端·算法
大圣编程3 小时前
Python中continue语句的用法是什么?
开发语言·前端·python
yuhaiqiang3 小时前
随手 vibecoding 的浏览器插件已经 6000 多次下载,聊聊他的产品设计
前端·后端·面试
之歆3 小时前
Vue商品详情与放大镜组件
前端·javascript·vue.js
再吃一根胡萝卜4 小时前
如何把小米 MiMo 接入 CodeBuddy,打造私有 Agent
前端
geovindu4 小时前
python: Functional Options Pattern
开发语言·后端·python·设计模式·惯用法模式·函数式选项模式
负责的蛋挞5 小时前
异步HttpModule的实现方式
java·服务器·前端
卷无止境6 小时前
C++ 存储类说明符(Storage Class Specifier)大横评
c++·后端
用户019027581616 小时前
量化数据的 batch 接口有多好用?从 1 只到 500 只,批量拉数据的正确姿势
后端
rruining6 小时前
Java设计模式——结构型
后端