java将文件压缩打包后进行下载

今天受到一个需求,需要查出文件,然后将文件打包后下载。看了下项目里默认代码有压缩功能,以此修改了下,项目使用了hutool。项目是若依项目

定义zip的数据传输对象,ossId可以是文件表的id

@Data
public class SysOssZipDTO {


    /**
     * 关联OSS对象存储ID
     */
})
    private String ossId;

    /**
     * 压缩包文件内文件夹路径,例子(main/java/com/haoyu/flowForm/domain/)
     */
    private String zipItemFolderPath;

}

sevice层

void downloadZip(List<SysOssZipDTO> zipDtoList, HttpServletResponse response, String taskId) throws IOException;

serviceImpl层

 @Override
    public void downloadZip(List<SysOssZipDTO> zipDtoList, HttpServletResponse response, String taskId) throws IOException {
        if(zipDtoList == null || zipDtoList.isEmpty()){
            return;
        }
		
        ZipOutputStream zip = new ZipOutputStream(response.getOutputStream());

       final int bufferSize = 1024 * 1024 * 5;      // 5MB
       final byte[] buffer = new byte[bufferSize];
       long totalFileSize = 0;  // 文件总大小,估计值


        // 任务进度key
         final String progressKey = OssConstant.SYS_OSS_PROGRESS+taskId;


        for (int i =0;i<zipDtoList.size();i++){

            // 设置进度(可选,用于压缩进度查询,就是将进度存入redis里,然后设计个接口供前端查询进度)
            /*SysOssProgressBO sysOssProgressBO = new SysOssProgressBO();
            sysOssProgressBO.setProgressNumber(Long.valueOf(i));
            sysOssProgressBO.setTotalProgress(Long.valueOf(zipDtoList.size()));
            RedisUtils.setCacheObject(progressKey,sysOssProgressBO, Duration.ofMinutes(10));
			*/
			
            SysOssZipDTO zipDTO = zipDtoList.get(i);
            if(StrUtil.isEmpty(zipDTO.getOssId())|| StrUtil.isEmpty(zipDTO.getZipItemFolderPath())){
                continue;
            }

			// 通过id查找文件信息
            SysOssVo sysOss = matchingUrl(SpringUtils.getAopProxy(this).getById(zipDTO.getOssId()));


            if (ObjectUtil.isNull(sysOss)) {
                continue;
            }
            
            // 通过文件信息获取文件流,本文用的Oss存储,如果用别的可以自己写,核心就是读取流
            OssClient storage = OssFactory.instance(sysOss.getService());
            try(InputStream inputStream = storage.getObjectContent(sysOss.getUrl());
            ){
                // 这里使用文件名了,拼上前缀序号解决文件名重复问题
                zip.putNextEntry(new ZipEntry(zipDTO.getZipItemFolderPath() +
                    "("+i+")"+sysOss.getOriginalName().replaceAll("/","_" )));

                int available = inputStream.available();
                totalFileSize += available;
         

                // 防Oom,可以用下面注释的也可以用hutool的
                /*int len = 0;
                while((len = inputStream.read(buffer)) != -1){
                    zip.write(buffer, 0, len);
                }*/
                IoUtil.copy(inputStream, zip, bufferSize);

                zip.flush();
                zip.closeEntry();
            }catch (Exception e) {
                throw new ServiceException(e.getMessage());
            }
        }

        IoUtil.close(zip);

        // 生成zip文件
        // 由于前面已经开启了response的outputstream,所以这里不能调用reset()方法,否则会导致流异常关闭
//        response.reset();
        response.addHeader("Access-Control-Allow-Origin", "*");
        response.addHeader("Access-Control-Expose-Headers", "Content-Disposition");
        response.addHeader("Content-Length", "" + totalFileSize);
        response.setContentType("application/octet-stream; charset=UTF-8");

        // 下载后的文件名可以由前端控制,此处由前端控制所以注释掉了
        /*if(StrUtil.isNotEmpty(zipName)){
            FileUtils.setAttachmentResponseHeader(response, zipName + ".zip");
        }*/

    }

其中的文件查询进度是可选的,可以让前端生成个任务id传到服务器
controller层

@GetMapping("/downloadZipByTrainId/{trainId}")
    public void downloadZipByTrainId(HttpServletResponse response, @PathVariable String trainId) throws IOException {
    	// downloadZipByTrainId()方法里面调用downloadZip()
       this.iTrainMaterialsService.downloadZipByTrainId(trainId,response);
    }

前端代码,此处使用的是vue3

// 需要npm安装file-saver
import { saveAs } from 'file-saver'

// 点击按钮触发事件
const clickDownLoadMaterials = async (row) =>{
  if(row.materialCount > 0 ){
    let res = await zip(`/train/trainMaterials/downloadZipByTrainId/${row.id}?projectName=${row.trainName}`,`${row.trainName}`)
  }else{
    ElMessage({
      message: '暂无材料可下载!',
      type: 'warning',
    })
  }
}


const zip = (url, name) => {
  isShowLoadingDownLoadMaterial.value = true
  url = baseURL + url
  axios({
    method: 'get',
    url: url,
    responseType: 'blob',
    headers: {
    	// 请求头token
      'Authorization': 'Bearer ' + getToken()
    }
  }).then(async (res) => {
    if(res.status == 200){
      isShowLoadingDownLoadMaterial.value = false
    }

    const isLogin = await blobValidate(res.data);
    if (isLogin) {
      const blob = new Blob([res.data], { type: 'application/zip' })
      saveAss(blob, name)
    } else {
      await this.printErrMsg(res.data);
    }
  })
}

const saveAss = (text, name, opts) =>{
  saveAs(text, name, opts);
}

// 验证是否为blob格式
export async function blobValidate(data) {
  try {
    const text = await data.text();
    JSON.parse(text);
    return false;
  } catch (error) {
    return true;
  }
}

优化
新增进度条,以及取消下载的功能

const clickDownLoadMaterials = async (row) =>{
  if(row.materialCount > 0 ){
     let taskId = await getZipTaskIdThenStartProgress();
      progressTaskId.value = taskId;
      // 使用axios.CancelToken取消请求,先赋值,后面点取消请求的时候会用到
      cancelSource.value = axios.CancelToken.source();
    let params = {
      trainId:row.id,
        taskId
    }
      let res = await  proxy.$download.zipThenCallback(`/train/trainMaterials/downloadZipByBo`,`${row.trainName}`
          ,params,cancelSource.value.token,closeZipProgress)
  }else{
    ElMessage({
      message: '暂无材料可下载!',
      type: 'warning',
    })
  }
}

/**取消下载压缩文件 */
async function  cancelZipTask (){
    // 中断进度条
    await closeZipProgress();
    cancelSource.value?.cancel("410");
}

/**打开进度条,返回任务id */
const getZipTaskIdThenStartProgress =async () =>{
    showProgress.value = true;
    // 发接口获取任务id
   let taskRes = await generateProgressTaskId();
   let taskId = taskRes.data;
    progressIntervalKey.value = setInterval(() => {
        // 根据任务id获取进度
        getProgressByTaskId(taskId).then(progressRes=>{
            let progressObj = progressRes.data;
            if(progressObj!= null && progressObj.totalProgress != 0){
                   let progressPercentGet = Number.parseInt((progressObj.progressNumber *100) /progressObj.totalProgress)
                     progressPercent.value = progressPercentGet;
                   if(progressPercentGet==100){
                       clearInterval(progressIntervalKey.value);
                   }
            }
        });
    }, 1000);
    return taskId;
}
/** 关闭进度条 */
async function closeZipProgress(){
    // 关闭定时器
    clearInterval(progressIntervalKey.value);
    // 发接口删掉任务id,后端判断如果没有任务id则不再进行压缩处理
    await removeProgressByTaskId(progressTaskId.value);
    showProgress.value=false;
    progressPercent.value=0;
}

// 下载压缩文件,带回调函数,可以中断
    zipThenCallback(url, name,params,cancelToken,callback){
        url = baseURL + url
        axios({
            method: 'get',
            url: url,
            responseType: 'blob',
            cancelToken,
            params,
            headers: {
                'Authorization': 'Bearer ' + getToken(),
            }
        }).then(async (res) => {
            if(res.status == 200 && callback != undefined){
                // 执行回调
                callback();
            }

            const isLogin = await blobValidate(res.data);
            if (isLogin) {
                const blob = new Blob([res.data], { type: 'application/zip' })
                saveAs(blob, name)
            } else {
                await this.printErrMsg(res.data);
            }
        }).catch( (error)=>{
                console.log(error)
                /*if(error.name!='"CanceledError"'){
                    ElMessage.error(error);
                }*/

        })
    },

杂谈
这次的需求整理到了不少东西,一开始担心oom的问题,于是调试时改变了缓冲区大小,发现java使用的内存也会对应的发生变化,这是为什么呢?实际上原因很简单,final byte[] buffer = new byte[bufferSize];这个变量就是占用这么多字节的内存呀!

当缓冲区设成0的时候,会发现读的特别特别慢,这是为什么呢?这是由于读的大小太小了,java调取磁盘io的频率变多,而调取磁盘io是一个特别消耗资源的行为也特别慢,因此就慢了。设置合适的缓冲区大小,可以一次性读取对应大小的内容从而减少磁盘io的交互从而提升效率,但也要避免缓冲区过大导致变量占用内存过大而oom。

同时,java流的操作不是一次性将整个文件都读入内存中的,而是仅仅获取文件的句柄,同时读偏移量,不停的读,移动偏移量,直至文件读写结束(大佬原话)。而且系统加载文件到内存也是按块加载的,并不会一次性全读到内存中(如一个几十g的游戏,打开并不会占用几十g内存,而是用到的时候在加载,不足的时候就会根据算法按页或者按块替换)

最后,开了流一定要关流!response.getOutputStream()自带的流可以不用手动关servlet在请求结束的时候会自己关掉

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值