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;),这样会报错

相关推荐
涡能增压发动积1 天前
同样的代码循环 10次正常 循环 100次就抛异常?自定义 Comparator 的 bug 让我丢尽颜面
后端
Wenweno0o1 天前
0基础Go语言Eino框架智能体实战-chatModel
开发语言·后端·golang
于慨1 天前
Lambda 表达式、方法引用(Method Reference)语法
java·前端·servlet
石小石Orz1 天前
油猴脚本实现生产环境加载本地qiankun子应用
前端·架构
swg3213211 天前
Spring Boot 3.X Oauth2 认证服务与资源服务
java·spring boot·后端
从前慢丶1 天前
前端交互规范(Web 端)
前端
tyung1 天前
一个 main.go 搞定协作白板:你画一笔,全世界都看见
后端·go
gelald1 天前
SpringBoot - 自动配置原理
java·spring boot·后端
@yanyu6661 天前
07-引入element布局及spring boot完善后端
javascript·vue.js·spring boot
CHU7290351 天前
便捷约玩,沉浸推理:线上剧本杀APP功能版块设计详解
前端·小程序