文章目录
最近团队开启了一个古老项目,框架:springmvc + hibernate + jsp,需要将上传到服务器的多个文件打包成zip,并提供下载接口。
背景说明
- 服务器文件统一存放目录,与服务部署同级目录upload
- 打包zip文件也需要按照相应规则存储在upload目录下
获取服务器文件列表 创建生成zip文件目录 zip 写入response
前期准备
获取服务器根路径
注意:本文中提及的服务器文件存放路径是和部署同级,所以应该取根路径的parent级别目录
- 从ContextLoader中获取
ContextLoader.getCurrentWebApplicationContext().getServletContext().getRealPath("/");
- 从HttpServletRequest中获取
request.getSession().getServletContext().getRealPath("/")
ZipOutputStream
了解ZipOutputStream得先了解ZipEntry(压缩添加项),一个ZipEntry代表待压缩的一个文件
压缩文件步骤:
- 遍历待压缩文件列表
- 为当前待压缩文件创建一个ZipEntry,将ZipOutputStream指向zipEntry头部
ZipOutputStream.putNextEntry(entry)
- 获取待压缩文件输入流,写入ZipOutputStream
- 关闭当前ZipEntry
- 继续步骤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
大体意思是,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.
- JSP释放代码
finally {
if (_jspxFactory != null)
_jspxFactory.releasePageContext(_jspx_page_context);
}
解决方法
response.reset();