springmvc 文件打包zip并下载

最近团队开启了一个古老项目,框架:springmvc + hibernate + jsp,需要将上传到服务器的多个文件打包成zip,并提供下载接口。

背景说明

  1. 服务器文件统一存放目录,与服务部署同级目录upload
  2. 打包zip文件也需要按照相应规则存储在upload目录下
获取服务器文件列表
创建生成zip文件目录
zip
写入response

前期准备

获取服务器根路径

注意:本文中提及的服务器文件存放路径是和部署同级,所以应该取根路径的parent级别目录

  1. 从ContextLoader中获取
ContextLoader.getCurrentWebApplicationContext().getServletContext().getRealPath("/");
  1. 从HttpServletRequest中获取
request.getSession().getServletContext().getRealPath("/")

ZipOutputStream

了解ZipOutputStream得先了解ZipEntry(压缩添加项),一个ZipEntry代表待压缩的一个文件

压缩文件步骤:

  1. 遍历待压缩文件列表
  2. 为当前待压缩文件创建一个ZipEntry,将ZipOutputStream指向zipEntry头部 ZipOutputStream.putNextEntry(entry)
  3. 获取待压缩文件输入流,写入ZipOutputStream
  4. 关闭当前ZipEntry
  5. 继续步骤1,直至遍历结束
public class ZipOutputStream extends DeflaterOutputStream implements ZipConstants {
    
    
	// ... 
}

实现

注意:以下代码中使用的logger是项目自定义log框架,可自行替换。

定义下载API

/**
* 下载数据zip包
 * @throws IOException 
 */
@ApiOperation("下载数据zip包")
@GetMapping(value = "/downloadImages")
public void downloadImages(@RequestParam("id") @ApiParam(required = true, value = "必填,业务id") String id,
				   HttpServletRequest request, HttpServletResponse response) throws IOException {
    
    
	
	// 此处获取服务器文件路径,可根据具体业务来
	String fileUrls = "/upload/20210823/1.jpg,/upload/20210823/2.jpg,/upload/20210823/3.jpg";
	
	//项目根路径
	final String rootDir = request.getSession().getServletContext().getRealPath("/");
	File dir = new File(rootDir);
	final String projectRootPath = dir.getParent() + File.separator + "/upload"; 
	List<File> files = new ArrayList<File>();
	Stream.of(fileUrls.split(",")).forEach(fileUrl -> {
    
    
		File imgFile = new File(projectRootPath + fileUrl);
		if (imgFile.exists() && imgFile.isFile()) {
    
    
			files.add(imgFile);
		}
	});
	
	// 定位upload文件夹,此处需要调用File.mkdirs()将缺省的父级目录创建!
	File rootFile = new File(projectRootPath);
	if (!rootFile.exists() || !rootFile.isDirectory()) {
    
    
		rootFile.mkdirs();
	}
	
	String zipFileName = String.format("%s-%s.zip", "压缩文件01", "2021-08-23");
	File zipFile = new File(projectRootPath + zipFileName);
	if (!zipFile.exists()) {
    
    
		zipFile.createNewFile();
	}
	
	// 文件输出流
	try (FileOutputStream outStream = new FileOutputStream(zipFile);
			ZipOutputStream zipStream = new ZipOutputStream(outStream);
			BufferedInputStream fis = new BufferedInputStream(new FileInputStream(zipFile));
			ServletOutputStream out = response.getOutputStream();) {
    
    
		
		zipFile(files, zipStream);
		
		// 清空response
        response.reset();
		response.setContentType("application/octet-stream");
		response.setHeader("Content-Disposition", "attachment;filename=" + new String(zipFileName.getBytes("UTF-8"), "ISO-8859-1"));
		
		// 写入response
		byte[] buffer = new byte[fis.available()];
           fis.read(buffer);
           out.write(buffer);
           out.flush();
		
	}  catch (IllegalStateException e) {
    
    
		logger.E(TAG2, "写response流抛出异常IllegalStateException" + e.getMessage());
		e.printStackTrace();
	} catch (FileNotFoundException e) {
    
    
		logger.E(TAG2, "写文件抛出FileNotFoundException异常," + e.getMessage());
		e.printStackTrace();
	} catch (IOException e) {
    
    
		logger.E(TAG2, "写文件抛异常IOException," + e.getMessage());
		e.printStackTrace();
	}
	return;
}

将多个文件压缩成zip文件

注意:此处没有判断file列表为空

private void zipFile(List<File> files, ZipOutputStream outputStream) {
    
    
	
	// 定义最大写入流为5M,超过则分割 
	final int MAX_BYTE = 5 * 1024 * 1024; 
	   
	files.forEach(file -> {
    
    
		
		try (FileInputStream inStream = new FileInputStream(file);
				BufferedInputStream bInStream = new BufferedInputStream(inStream);) {
    
    
			
			ZipEntry entry = new ZipEntry(file.getName());
            outputStream.putNextEntry(entry);

			// 中的字符数
            long total = bInStream.available(); 
            int splitTimes = (int) Math.floor(total / MAX_BYTE); // 取得流文件需要分开的数量
            int leftBytes = (int) total % MAX_BYTE; // 分开文件之后,剩余的数量
            byte[] writeBytes; // byte数组接受文件的数据

            if (splitTimes > 0) {
    
    
                for (int j = 0; j < splitTimes; ++j) {
    
    
                    writeBytes = new byte[MAX_BYTE];
                    // 读入流,保存在byte数组
                    bInStream.read(writeBytes, 0, MAX_BYTE);
                    outputStream.write(writeBytes, 0, MAX_BYTE); // 写出流
                }
            }
            // 写出剩下的流数据
            writeBytes = new byte[leftBytes];
            bInStream.read(writeBytes, 0, leftBytes);
            outputStream.write(writeBytes);
            outputStream.closeEntry(); // Closes the current ZIP entry
            
		} catch (IOException e) {
    
    
			logger.E("文件Zip打包", "抛出IOException" + e.getMessage());
			e.printStackTrace();
		}
            
		} catch (IOException e) {
    
    
			logger.E("文件Zip打包", "抛出IOException" + e.getMessage());
			e.printStackTrace();
		}
        
	});
}

遇到的问题

getOutputStream() has already been called for this response

查阅资料,参考文档1参考文档2收益颇多。

大体意思是,JSP有内置对象out(PageContext.getOut()获取),在JSP释放时,会调用response.getWriter()方法,而我们在下载文件时会使用response.getOutputStream()进行写入,两者是冲突的。J2EE官方文档也有此说明!

Calling flush() on the ServletOutputStream commits the response. Either this method or getWriter() may be called to write the body, not both.

  1. JSP释放代码
finally {
    
    
	if (_jspxFactory != null) 
	_jspxFactory.releasePageContext(_jspx_page_context);
}

解决方法

response.reset();

猜你喜欢

转载自blog.csdn.net/huhui806/article/details/119869355