Java多文件压缩下载解决方案

Summarize 同时被 2 个专栏收录
31 篇文章 0 订阅

Java多文件压缩下载解决方案

需求:

会员运营平台经过改版后页面增加了许多全部下载链接,上周上线比较仓促,全部下载是一个直接下载ZIP压缩文件的链接,每个ZIP压缩文件都是由公司运营人员将页面需要下载的文件全部压缩成一个ZIP压缩文件,然后通过公司的交易运营平台上传至文件资料系统,会员运营平台则可以直接获取ZIP压缩文件地址进行下载

下面是一个页面示例:

需求分析:

通过上面需求和页面可以分析出,公司运营人员将页面全部需要下载的文件进行ZIP压缩后上传文件资料系统确实是一个紧急的解决方案,但是考虑到后面需求变更,页面需要下载的文件也会跟着变更,每次变更需求,公司运营人员都需要重新进行压缩文件,程序也需要进行相应的修改,这样对于程序的维护性不友好,站在使用系统的客户角度,每次都需要重新上传,因此临时解决方案不再符合软件的良好扩展性和操作方便,因此才有了对页面需要全部下载的文件使用程序压缩处理并下载。

 

解决思路:

第一步:前端传递Ids字符串

由于会员运营系统显示需要下载的文件是资料系统中的每条文件记录的Id,因此前端页面只需要将需要下载的所有文件Ids字符串(比如:'12,13,14')传递到后台即可.

第二步:后台处理

首先获取到前端传递的ids字符串,将其转换为Integer[]的ids数组,然后调用文件资料微服务根据id列表查询对应的文件记录(包含文件类型和文件地址路径等信息),获取到所有需要下载的文件路径后压缩成ZIP格式的文件进行下载。

 

具体实现压缩下载方案:

第一种:先压缩成ZIP格式文件,再下载

第二种:边压缩ZIP格式文件边下载(直接输出ZIP流)

 

前端具体实现代码:

由于全部下载是一个a链接标签,于是使用Ajax异步下载,后来功能实现后点击下载一点反应都没有,一度怀疑是后台出错,但是后台始终没有报错,在网上看了一下Ajax异步不能下载文件(也就是Ajax不支持流类型数据),具体原因可以百度https://blog.csdn.net/qq_16877261/article/details/54376430这篇博客,解释的还算是比较好的。后面会写一篇=文章详细分析Ajax异步下载解决方案。

接下来考虑使用form表单标签实现,最终配合使用input标签实现了前端传递ids列表的问题,点击a链接标签触发提交form标签即可。

在每一个需要下载的文件增加一个隐藏的input标签,value值是这个文件的id值

具体点击a链接标签提交表单的JS代码:

 

后端具体实现代码:

第一种方案实现:

第二种方案实现:

附上完整代码:

压缩下载Controller

package com.huajin.jgoms.controller.user;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

import javax.servlet.http.HttpServletResponse;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import com.huajin.baymax.logger.XMsgError;
import com.huajin.baymax.logger.Xlogger;
import com.huajin.common.util.UUIDUtil;
import com.huajin.exchange.domain.sys.FeFileCenter;
import com.huajin.exchange.enums.sys.SysParamKey;
import com.huajin.exchange.po.sys.SysParamPo;
import com.huajin.jgoms.controller.HjBaseController;
import com.huajin.jgoms.service.FeFileCenterService;
import com.huajin.jgoms.service.SysParamService;
import com.huajin.jgoms.util.CompressDownloadUtil;

/**
 * 压缩下载文件
 *
 * @author hongwei.lian
 * @date 2018年9月6日 下午6:29:05
 */
@Controller
@RequestMapping("/compressdownload")
public class CompressDownloadController extends HjBaseController {
	
	@Autowired
	private FeFileCenterService feFileCenterService;
	
	@Autowired
	private SysParamService sysParamService;

	/**
	 * 多文件压缩下载
	 * 
	 * @author hongwei.lian
	 * @date 2018年9月6日 下午6:28:56
	 */
	@RequestMapping("/downloadallfiles")
	public void downloadallfiles() {
		//-- 1、根据ids查询下载的文件地址列表
		String ids = request().getParameter("ids");
		if (StringUtils.isEmpty(ids)) {
			return ;
		}
		//-- 将字符串数组改变为整型数组
		Integer[] idsInteger = CompressDownloadUtil.toIntegerArray(ids);
		List<FeFileCenter> fileCenters = feFileCenterService.getFeFileByIds(super.getExchangeId(), idsInteger);
		if (CollectionUtils.isNotEmpty(fileCenters) && ObjectUtils.notEqual(idsInteger.length, fileCenters.size())) {
			//-- 要下载文件Id数组个数和返回的文件地址个数不一致
			return ;
		}
		
		//-- 2、转换成文件列表
		List<File> files = this.toFileList(fileCenters);
		//-- 检查需要下载多文件列表中文件路径是否都存在
		for (File file : files) {
			if (!file.exists()) {
				//-- 需要下载的文件中存在不存在地址
				return ;
			}
		}
		
		//-- 3、响应头的设置
		String downloadName = UUIDUtil.getUUID() + ".zip";
		HttpServletResponse response = CompressDownloadUtil.setDownloadResponse(super.response(), downloadName);
		
		//-- 4、第一种方案:
		//-- 指定ZIP压缩包路径
//		String zipFilePath = this.setZipFilePath(downloadName);
//		try {
//			//-- 将多个文件压缩到指定路径下
//			CompressDownloadUtil.compressZip(files, new FileOutputStream(zipFilePath));
//			//-- 下载压缩包
//			CompressDownloadUtil.downloadFile(response.getOutputStream(), zipFilePath);
//			//-- 删除临时生成的ZIP文件
//			CompressDownloadUtil.deleteFile(zipFilePath);
//		} catch (IOException e) {
//			Xlogger.error(XMsgError.buildSimple(CompressDownloadUtil.class.getName(), "downloadallfiles", e));
//		}
		
		//-- 5、第二种方案:
	   try {
		    //-- 将多个文件压缩写进响应的输出流
			CompressDownloadUtil.compressZip(files, response.getOutputStream());
		} catch (IOException e) {
			Xlogger.error(XMsgError.buildSimple(CompressDownloadUtil.class.getName(), "downloadallfiles", e));
		}
		
	}

	/**
	 * 设置临时生成的ZIP文件路径
	 *
	 * @param fileName
	 * @return 
	 * @author hongwei.lian
	 * @date 2018年9月7日 下午3:54:13
	 */
	private String setZipFilePath(String fileName) {
		String zipPath = sysParamService.getCompressDownloadFilePath();
		File zipPathFile = new File(zipPath);
		if (!zipPathFile.exists()) {
			zipPathFile.mkdirs();
		}
		return zipPath + File.separator + fileName;
	}

	/**
	 * 将fileCenters列表转换为File列表
	 *
	 * @param fileCenters
	 * @return 
	 * @author hongwei.lian
	 * @date 2018年9月6日 下午6:54:16
	 */
	private List<File> toFileList(List<FeFileCenter> fileCenters) {
		return fileCenters.stream()
                                     .map(feFileCenter -> {
                                    	 //-- 获取每个文件的路径
                                    	 String filePath = this.getSysFilePath(feFileCenter.getFileTypeId());
                                         return new File(filePath + feFileCenter.fileLink());})
                                     .collect(Collectors.toList());
	}
	
	/**
	 * 获取文件类型对应存储路径
	 *
	 * @param fileTypeId
	 * @return 
	 * @author hongwei.lian
	 * @date 2018年9月5日 下午2:01:53
	 */
	private String getSysFilePath(Integer fileTypeId){
		SysParamPo sysmParam = sysParamService.getByParamKey(SysParamKey.FC_UPLOAD_ADDRESS.value);
		String filePath = Objects.nonNull(sysmParam) ? sysmParam.getParamValue() : "";
		return filePath + fileTypeId + File.separator;
	}

}

压缩下载工具类

package com.huajin.jgoms.util;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import javax.servlet.http.HttpServletResponse;

import com.huajin.baymax.logger.XMsgError;
import com.huajin.baymax.logger.Xlogger;

/**
 * 压缩下载工具类
 *
 * @author hongwei.lian
 * @date 2018年9月6日 下午6:34:56
 */
public class CompressDownloadUtil {
	
	private CompressDownloadUtil() {}
	
	/**
	 * 设置下载响应头
	 *
	 * @param response
	 * @return 
	 * @author hongwei.lian
	 * @date 2018年9月7日 下午3:01:59
	 */
	public static HttpServletResponse setDownloadResponse(HttpServletResponse response, String downloadName) {
		response.reset();
		response.setCharacterEncoding("utf-8");
		response.setContentType("application/octet-stream");
		response.setHeader("Content-Disposition", "attachment;fileName*=UTF-8''"+ downloadName);
		return response;
	}
	
	/**
	 * 字符串转换为整型数组
	 *
	 * @param param
	 * @return 
	 * @author hongwei.lian
	 * @date 2018年9月6日 下午6:38:39
	 */
	public static Integer[] toIntegerArray(String param) {
		return Arrays.stream(param.split(","))
                              .map(Integer::valueOf)
                              .toArray(Integer[]::new);
	}
	
	/**
	 * 将多个文件压缩到指定输出流中
	 *
	 * @param files 需要压缩的文件列表
	 * @param outputStream  压缩到指定的输出流
	 * @author hongwei.lian
	 * @date 2018年9月7日 下午3:11:59
	 */
	public static void compressZip(List<File> files, OutputStream outputStream) {
		ZipOutputStream zipOutStream = null;
		try {
			//-- 包装成ZIP格式输出流
			zipOutStream = new ZipOutputStream(new BufferedOutputStream(outputStream));
			// -- 设置压缩方法
			zipOutStream.setMethod(ZipOutputStream.DEFLATED);
			//-- 将多文件循环写入压缩包
			for (int i = 0; i < files.size(); i++) {
				File file = files.get(i);
				FileInputStream filenputStream = new FileInputStream(file);
				byte[] data = new byte[(int) file.length()];
				filenputStream.read(data);
				//-- 添加ZipEntry,并ZipEntry中写入文件流,这里,加上i是防止要下载的文件有重名的导致下载失败
				zipOutStream.putNextEntry(new ZipEntry(i + file.getName()));
				zipOutStream.write(data);
				filenputStream.close();
				zipOutStream.closeEntry();
			}
		} catch (IOException e) {
			Xlogger.error(XMsgError.buildSimple(CompressDownloadUtil.class.getName(), "downloadallfiles", e));
		}  finally {
			try {
				if (Objects.nonNull(zipOutStream)) {
					zipOutStream.flush();
					zipOutStream.close();
				}
				if (Objects.nonNull(outputStream)) {
					outputStream.close();
				}
			} catch (IOException e) {
				Xlogger.error(XMsgError.buildSimple(CompressDownloadUtil.class.getName(), "downloadallfiles", e));
			}
		}
	}
	
	/**
	 * 下载文件
	 *
	 * @param outputStream 下载输出流
	 * @param zipFilePath 需要下载文件的路径
	 * @author hongwei.lian
	 * @date 2018年9月7日 下午3:27:08
	 */
	public static void downloadFile(OutputStream outputStream, String zipFilePath) {
		File zipFile = new File(zipFilePath);
		if (!zipFile.exists()) {
			//-- 需要下载压塑包文件不存在
			return ;
		}
		FileInputStream inputStream = null;
		try {
			inputStream = new FileInputStream(zipFile);
			byte[] data = new byte[(int) zipFile.length()];
			inputStream.read(data);
			outputStream.write(data);
			outputStream.flush();
		} catch (IOException e) {
			Xlogger.error(XMsgError.buildSimple(CompressDownloadUtil.class.getName(), "downloadZip", e));
		} finally {
			try {
				if (Objects.nonNull(inputStream)) {
					inputStream.close();
				}
				if (Objects.nonNull(outputStream)) {
					outputStream.close();
				}
			} catch (IOException e) {
				Xlogger.error(XMsgError.buildSimple(CompressDownloadUtil.class.getName(), "downloadZip", e));
			}
		}
	}
	
	/**
	 * 删除指定路径的文件
	 *
	 * @param filepath 
	 * @author hongwei.lian
	 * @date 2018年9月7日 下午3:44:53
	 */
	public static void deleteFile(String filepath) {
		File file = new File(filepath);
		deleteFile(file);
	}
	
	/**
	 * 删除指定文件
	 *
	 * @param file 
	 * @author hongwei.lian
	 * @date 2018年9月7日 下午3:45:58
	 */
	public static void deleteFile(File file) {
		//-- 路径为文件且不为空则进行删除  
	    if (file.isFile() && file.exists()) {  
	        file.delete();  
	    } 
	}

}

测试

通过交易运营平台上传测试资料

登录会员运营平台进行下载

下载下来的ZIP格式为文件

解压后,打开文件是否可用:

 

总结:

这个过程中出现了很多问题,后面会有文章逐步分析出错和解决方案。

上述两种方案都行,但是为了响应速度更快,可以省略压缩成ZIP的临时文件的时间,因此采用了第二种解决方案。

 

  • 8
    点赞
  • 6
    评论
  • 35
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值