需求:前台传过来一个url数组,其中是每个文件的路径,后台需要获取到每个文件然后将其打成压缩包返回流给前台下载
核心是:ZipOutputStream,废话不多说,上代码
- savePath 是我在yml配置文件里配置好的文件存储路径
- 在使用谷歌(火狐可以)下载过程中遇到问题:文件名中含有半角符号无法下载,因为文件名是前台命名的,所以这个我就随便命名个1.zip解决一下
- 文件名有中文要用iso_8859_1转义一下,不然会乱码(使用swagger测试时一直都是乱码,是swagger的问题,使用浏览器直接下载没事)
- 接口写完之后使用swagger或者postMan等测试工具无法识别流,返回的都是乱码,这是正常的,前台处理一下就可以了
package com.hongseng.app.config.file;
import com.hongseng.app.mapper.task.TaskMapper;
import model.dto.dwonload.DownloadZipDto;
import model.entity.task.TaskList;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import result.Result;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@Value("${spring.servlet.multipart.location}")
private String savePath;
/**
* 下载压缩包
*
* @param downloadZipDto
* @param response
* @throws IOException
*/
public void downloadZipFromUrl(DownloadZipDto downloadZipDto, HttpServletResponse response) throws IOException {
// 创建临时路径,存放压缩文件
String zipFilePath = savePath + "\\11.zip";
// 压缩输出流,包装流,将临时文件输出流包装成压缩流,将所有文件输出到这里,打成zip包
ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(zipFilePath));
// 循环调用压缩文件方法,将一个一个需要下载的文件打入压缩文件包
@NotEmpty List<String> urls = downloadZipDto.getUrls();
for (String url : urls) {
int lastIndexOf = url.lastIndexOf('/');
String name = url.substring(lastIndexOf + 1);
String newPath = savePath + "\\" + name;
// 该方法在下面定义
fileToZip(newPath, zipOut);
}
zipOut.flush();
// 压缩完成后,关闭压缩流
zipOut.close();
// 压缩包名称拼接
@NotBlank String departmentName = downloadZipDto.getDepartmentName();
TaskList taskList = taskMapper.selectById(downloadZipDto.getTaskListId());
//拼接下载默认名称并转为ISO-8859-1格式
String fileName = new String(("1.zip").getBytes(), StandardCharsets.ISO_8859_1);
response.setHeader("Content-Disposition", "attchment;filename="+fileName);
//该流不可以手动关闭,手动关闭下载会出问题,下载完成后会自动关闭
ServletOutputStream outputStream = response.getOutputStream();
FileInputStream inputStream = new FileInputStream(zipFilePath);
// 如果是SpringBoot框架,在这个路径
// org.apache.tomcat.util.http.fileupload.IOUtils产品
// 否则需要自主引入apache的 commons-io依赖
// copy方法为文件复制,在这里直接实现了下载效果
IOUtils.copy(inputStream, outputStream);
outputStream.close();
// 关闭输入流
inputStream.close();
//下载完成之后,删掉这个zip包
File fileTempZip = new File(zipFilePath);
fileTempZip.delete();
}
private void fileToZip(String filePath,ZipOutputStream zipOut) throws IOException {
// 需要压缩的文件
File file = new File(filePath);
// 获取文件名称,如果有特殊命名需求,可以将参数列表拓展,传fileName
String fileName = file.getName();
FileInputStream fileInput = new FileInputStream(filePath);
// 缓冲
byte[] bufferArea = new byte[1024 * 10];
BufferedInputStream bufferStream = new BufferedInputStream(fileInput, 1024 * 10);
// 将当前文件作为一个zip实体写入压缩流,fileName代表压缩文件中的文件名称
zipOut.putNextEntry(new ZipEntry(fileName));
int length = 0;
// 最常规IO操作,不必紧张
while ((length = bufferStream.read(bufferArea, 0, 1024 * 10)) != -1) {
zipOut.write(bufferArea, 0, length);
}
//关闭流
fileInput.close();
// 需要注意的是缓冲流必须要关闭流,否则输出无效
bufferStream.close();
// 压缩流不必关闭,使用完后再关
}
补充:测试遇到问题
这个代码会有压缩文件错误,无法解压和解压后文件丢失的情况
关键代码:
byte[] bufferArea = new byte[1024 * 10];
BufferedInputStream bufferStream = new BufferedInputStream(fileInput, 1024 * 10);
// 将当前文件作为一个zip实体写入压缩流,fileName代表压缩文件中的文件名称
zipOut.putNextEntry(new ZipEntry(fileName));
int length = 0;
// 最常规IO操作,不必紧张
while ((length = bufferStream.read(bufferArea, 0, 1024 * 10)) != -1) {
zipOut.write(bufferArea, 0, length);
}
看了博友的博客说是因为有可能一次读取的byte数组太大了导致问题,修改后的代码可以使用
byte[] bufferArea = new byte[1024];
BufferedInputStream bufferStream = new BufferedInputStream(fileInput);
// 将当前文件作为一个zip实体写入压缩流,fileName代表压缩文件中的文件名称
zipOut.putNextEntry(new ZipEntry(fileName));
int length = 0;
// 最常规IO操作,不必紧张
while ((length = bufferStream.read(bufferArea)) != -1) {
zipOut.write(bufferArea, 0, length);
}
// 解决剩余的
int remain = bufferStream.available();
byte[] last = new byte[remain];
bufferStream.read(last);
zipOut.write(last);
还增加了一点:关闭压缩流之前调用了flush(),刷新压缩流
zipOut.flush();
优化:使用了try-with-resource关闭流
新代码
/**
* 下载压缩包
*
* @param downloadZipDto
* @param response
* @throws IOException
*/
public void downloadZipFromUrl(DownloadZipDto downloadZipDto, HttpServletResponse response) {
// 创建临时路径,存放压缩文件
String zipFilePath = savePath + "\\11.zip";
try (ServletOutputStream outputStream = response.getOutputStream()){
// 压缩输出流,包装流,将临时文件输出流包装成压缩流,将所有文件输出到这里,打成zip包
ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(zipFilePath));
// 循环调用压缩文件方法,将一个一个需要下载的文件打入压缩文件包
@NotEmpty List<String> urls = downloadZipDto.getUrls();
for (String url : urls) {
int lastIndexOf = url.lastIndexOf('/');
String name = url.substring(lastIndexOf + 1);
String newPath = savePath + "\\" + name;
// 该方法在下面定义
fileToZip(newPath, zipOut);
}
zipOut.flush();
// 压缩完成后,关闭压缩流
zipOut.close();
response.setHeader("Content-Disposition", "attchment;filename="+"1.zip");
//该流不可以手动关闭,手动关闭下载会出问题,下载完成后会自动关闭
FileInputStream inputStream = new FileInputStream(zipFilePath);
// 如果是SpringBoot框架,在这个路径
// org.apache.tomcat.util.http.fileupload.IOUtils产品
// 否则需要自主引入apache的 commons-io依赖
// copy方法为文件复制,在这里直接实现了下载效果
IOUtils.copy(inputStream, outputStream);
outputStream.close();
// 关闭输入流
inputStream.close();
//下载完成之后,删掉这个zip包
File fileTempZip = new File(zipFilePath);
fileTempZip.delete();
}catch (IOException e){
e.printStackTrace();
}
}
private void fileToZip(String filePath, ZipOutputStream zipOut) {
// 需要压缩的文件
File file = new File(filePath);
// 获取文件名称,如果有特殊命名需求,可以将参数列表拓展,传fileName
String fileName = file.getName();
FileInputStream fileInput = null;
BufferedInputStream bufferStream = null;
try {
fileInput = new FileInputStream(filePath);
// 缓冲
byte[] bufferArea = new byte[1024];
bufferStream = new BufferedInputStream(fileInput);
// 将当前文件作为一个zip实体写入压缩流,fileName代表压缩文件中的文件名称
zipOut.putNextEntry(new ZipEntry(fileName));
int length = 0;
// 最常规IO操作,不必紧张
while ((length = bufferStream.read(bufferArea)) != -1) {
zipOut.write(bufferArea, 0, length);
}
// 解决剩余的
int remain = bufferStream.available();
byte[] last = new byte[remain];
bufferStream.read(last);
zipOut.write(last);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
//关闭流
fileInput.close();
// 需要注意的是缓冲流必须要关闭流,否则输出无效
bufferStream.close();
// 压缩流不必关闭,使用完后再关
} catch (IOException e) {
e.printStackTrace();
}
}
}
不影响运行,但是看着糟心:
java.lang.IllegalStateException: Cannot call sendError() after the response has been committed
出现这个错误,应该是多次response导致的,可以这么理解,http server发送response后就关闭了socket,这个时候再次发送response给http client就会出现这个问题。
解决方法,不要返回值 使用void
/**
* 文件批量打包下载
*
* @param downloadZipDto 文件名称
* @return result
*/
@ApiOperation("文件批量打包下载")
@AnonymousAccess
@PostMapping(value = "/download-zip")
public void downloadZipFromUrl(@RequestBody @Valid DownloadZipDto downloadZipDto, HttpServletResponse response) {
downloadConfig.downloadZipFromUrl(downloadZipDto, response);
}