前后端分离下载文件

背景

基于SpringBoot+Vue前后端分离项目中进行文件下载
SpringBoot版本:2.0.3.RELEASE
vue版本:2.5.2

本博客中前端实现文件下载的方式有3种方式如下:

  1. 通过a链接下载(需要绕过安全校验框架的token验证);
  2. axios+Blob发送post请求实现下载(经过安全校验框架的登录或者token验证,但是下载复杂类型文件异常,尽可以支持.txt或者csv文件);
  3. XMLHttpRequest+Blob发送post请求实现下载(经过安全校验框架的登录或者token验证,可以下载所有的文件)。

1.后台

1.1后台下载文件的工具类

该工具类型的主要处理逻辑是设置响应头属性,将待下载文件转换为字节流写入HttpServletResponse 实例中。

  1. Content-type:说明了实体主体内对象的媒体类型;不同文件对应的value ,而下面代码中使用的application/octet-stream则表示以流方式下载文件,可以匹配所有类型的文件。
  2. Content-Disposition:下载文件的一个标识字段,filename属性值是下载得到的文件的文件名;
  3. Content-Length:该资源的大小,单位为字节。
package com.mark.common.utils;

import com.mark.common.exception.Campuso2oException;
import org.apache.commons.lang3.StringUtils;
import sun.misc.BASE64Encoder;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

/**
 * @Description: 下载文件的工具类
 * @Author: Mark
 * @CreateDate: 2019/2/23 16:29
 * @Version: 2.0
 * @Copyright : 豆浆油条个人非正式工作室
 */
public class DownloadFileUtil {

    /**
     * 下载文件
     * @param originalFileName :下载文件的原始文件名
     * @param file             :下载的文件
     * @param response         :相应对象
     */
    public static void downloadFile(String originalFileName, File file, HttpServletResponse response, HttpServletRequest request) {
        // 数据校验
        checkParam(originalFileName,file);

        //相应头的处理
        //清空response中的输出流
        response.reset();
        //设置文件大小
        response.setContentLength((int) file.length());
        //设置Content-Type头
        response.setContentType("application/octet-stream;charset=UTF-8");
        //设置Content-Disposition头 以附件形式解析
        String encodedFilename = getEncodedFilename(request, originalFileName);
        response.addHeader("Content-Disposition", "attachment;filename=" + encodedFilename);

        //将来文件流写入response中
        FileInputStream fileInputStream = null;
        ServletOutputStream outputStream = null;
        try {
            //获取文件输入流
            fileInputStream = new FileInputStream(file);
            //创建数据缓冲区
            byte[] buffers = new byte[1024];
            //通过response中获取ServletOutputStream输出流
            outputStream = response.getOutputStream();
            int length;
            while ((length = fileInputStream.read(buffers)) > 0) {
                //写入到输出流中
                outputStream.write(buffers, 0, length);
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //流的关闭
            if(fileInputStream != null){
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (outputStream != null) {
                try {
                    outputStream.flush();
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 下载文件的参数的校验,如果参数不合法则抛出自定义异常
     * @param originalFileName :文件原始文件名
     * @param file :待下载的文件
     */
    private static void checkParam(String originalFileName, File file) {
        if(StringUtils.isBlank(originalFileName)){
            throw new Campuso2oException("输入的文件原始文件名为空");
        }
        if(file == null || !file.exists() ){
            throw new Campuso2oException("待在下载的文件不存在!");
        }
    }

    /**
     * 获取URL编码后的原始文件名
     * @param request :客户端请求
     * @param originalFileName :原始文件名
     * @return :
     */
    private static String getEncodedFilename(HttpServletRequest request, String originalFileName) {
        String encodedFilename = null;
        String agent = request.getHeader("User-Agent");
        if(agent.contains("MSIE")){
            //IE浏览器
            try {
                encodedFilename = URLEncoder.encode(originalFileName, "utf-8");
                encodedFilename = encodedFilename.replace("+", " ");
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }else if(agent.contains("Firefox")){
            //火狐浏览器
            BASE64Encoder base64Encoder = new BASE64Encoder();
            encodedFilename = "=?utf-8?B?" + base64Encoder.encode(originalFileName.getBytes(StandardCharsets.UTF_8))+"?=";
        }else{
            //其他浏览器
            try {
                encodedFilename = URLEncoder.encode(originalFileName, "utf-8");
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }
        return encodedFilename;
    }
}

补充:自定义异常

处理业务逻辑时,如参数有误,操作有误可以主动抛出该异常,然后在全局异常统一处理。

package com.mark.common.exception;

/**
 * @Description: 自定义的校园二手交易平台业务处理过程发生异常
 * @Author: Mark
 * @CreateDate: 2018/10/9 18:06
 * @Version: 2.0
 * @Copyright : 豆浆油条个人非正式工作室
 */
public class Campuso2oException extends RuntimeException{
    public Campuso2oException(String origin){
        super("{"+origin+"}发生了异常:");
    }
    public Campuso2oException(String origin,Exception ex){
        super("{"+origin+"}发生了异常:"+parseErrMsg(ex));

    }

    private static String parseErrMsg(Exception ex) {
        if(ex == null){
            return "异常堆栈获为空";
        }
        String errMsg = "";
        StackTraceElement[] stackTrace = ex.getStackTrace();
        for (StackTraceElement s : stackTrace) {
            errMsg+="\tat " + s + "\r\n";
        }
        return errMsg;
    }
}

1.2.Controller层的调用测试

package com.mark.web.common.controller;

import com.mark.common.utils.DownloadFileUtil;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;

/**
 * @Description: 通用文件下载类
 * @Author: Kingsley
 * @CreateDate: 2019/2/23 20:22
 * @Version: 2.0
 * @Copyright : 豆浆油条个人非正式工作室
 */
@RestController()
@RequestMapping("/common/file/")
public class FileDownloadController {

    /**
     * 下载文件的demo
     */
    @RequestMapping(value = "download")
    public void downloadFile(HttpServletRequest request, HttpServletResponse response) {
        File file = new File("C:\\Users\\Administrator\\Desktop\\bak\\test.jpg");
        String fileName = "测试文件下载.png";
        DownloadFileUtil.downloadFile(fileName,file,response,request);
    }
}

2.前端下载

2.1前端使用a连接标签下载

a标签的download属性

<a href="http://localhost:8080/common/file/download" download="true">下载文件</a>

下载结果演示

左边为原始文件,右边为下载的前端演示以及下载成功后的得到的文件。下载完成后比较两个文件的大小,发现是一致的!
在这里插入图片描述

问题

以上可以实现通过a链接下载文件。但这种下载文件的有一个安全问题,通过a链接下载是无法进行token验证的,如果后台有实现了权限验证框架,例如Shiro。那么这种下载方式是需要在Shiro中设置url放行。


2.2优化1-axios发送post请求下载(有bug)

前端vue框架中居于axios组件通过post请求实现文件的下载,这样前端会被后台的权限验证拦截,验证token信息。
注意响应类型需要设置为blob ,Blob对象使用说明

/**
   * 文件的下载
   * @param url : 请求的Url
   * @param data : 请求的数据
   * @param errorHandler : 请求发生异常的回调函数
   */
  loadLoadFile:(url, data, errorHandler) =>{
    axios.post(url,data,{
      headers :{
        responseType: 'blob'
      }
    }).then(res=>{
      const blob = new Blob([res.data])
      let url = window.URL.createObjectURL(blob)

      //创建一个a标签元素,设置下载属性,点击下载,最后移除该元素
      let link = document.createElement('a')
      link.href = url
      link.style.display = 'none'
      //res.headers.fileName 取出后台返回下载的文件名
      const downlaodFileName = decodeURIComponent(res.headers.filename)
      console.log(res.headers)
      link.setAttribute('download',downlaodFileName)
      link.click()
      window.URL.revokeObjectURL(url)
    }).catch(errorHandler)
  },

工具需要在响应头信息中添加一个自定义的下载文件名
注意:虽然后台设置文件名的key为fileName,请求返回客户端的时候会转为小写即filename

//添加下载文件名
response.addHeader("fileName",encodedFilename);

结果演示

在这里插入图片描述
从下载结果中可以看出虽然可以下载文件,但是下载后的文件的大小比原始的文件的大小不一致。更重要的经过测试发现如果下载文件为文档、图片、表格等文件会出现乱码或者文件打不开的情况。搜索一堆网上的博客文档,提到可能是axios下载的锅,响应得到文件流已经是乱码不正确的了。基于axios请求下载文件还没找到更好的解决方法。换个方式呢?下面通过原始XHR请求可以解决这个问题。

2.3优化2:使用XMLHttpRequest发送post请求

关于XMLHttpRequest的使用
window.URL.createObjectURL()和 window.URL.revokeObjectURL() 的使用说明

 /**
   * 通过XMLHttpRequest发送post请求下载文件
   *@param url : 请求的Url
   * @param data : 请求的数据
   */
  XHRLoadLoadFile:(url, data)=>{
    let xhr = new XMLHttpRequest()
    xhr.open('post',url)
    //如果需要请求头中这是token信息可以在这设置
    xhr.setRequestHeader('Content-Type','application/json;charset=UTF-8')
    xhr.responseType = 'blob'
    xhr.send(JSON.stringify(data))
    xhr.onreadystatechange = function(){
      if(xhr.readyState ===4 && xhr.status === 200){
        const blob = new Blob([xhr.response])
        let url = window.URL.createObjectURL(blob)
  
        //创建一个a标签元素,设置下载属性,点击下载,最后移除该元素
        let link = document.createElement('a')
        link.href = url
        link.style.display = 'none'
        //取出下载文件名
        const fileName = xhr.getResponseHeader('filename')
        const downlaodFileName = decodeURIComponent(fileName)
  
        link.setAttribute('download',downlaodFileName)
        link.click()
        window.URL.revokeObjectURL(url)
      }
    }
  },

结果演示:终于可以正常下载了

在这里插入图片描述

  • 5
    点赞
  • 48
    收藏
  • 打赏
    打赏
  • 12
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:编程工作室 设计师:CSDN官方博客 返回首页
评论 12

打赏作者

GZ-豆浆油条

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值