在互联网上查找了各种资料,大部分资料都说是后端的流关闭顺序不正确。反复试验关闭顺序。发现文件下载到前端一致报这个错误。有的说是前端接收类型要加上responseType:‘arraybuffer’或者‘blob’,但是添加上后仍然不正确。
2022年08月03日发现主要问题可能是因为前端jQuery接受二进制数据流的处理不正确。
我使用的是jQury的ajax,向后端调用接口,可以调用成功,后端返回的是byte[] 文件流(下附后端代码)。调试的时候发现接收到的data数据是string类型,并且显示接收到的数据344KB,明显比我发送的数据122KB多,这个问题很值得深入研究。
在浏览器中直接访问后端接口,可以下载成功,打开后不报错,于是现在可以明确的定位问题到前端了,但是前端代码可以发送请求成功,也可以接收到返回的数据。说明代码是没问题的?但是为什么接收的数据不正确呢?
有可能是公司框架不支持接收二进制流数据,所以强制将二进制流数据转换为了文本数据。
@Controller
@RequestMapping(value="/BPTiwInvoiceManage")
@MultipartConfig
public class InvoiceManagerExportZip {
@RequestMapping(value ="exportzip", method = { RequestMethod.GET ,RequestMethod.POST})
public void exportZip(@RequestParam(value = "filterList",required = false) String filterString,HttpServletRequest request,HttpServletResponse response) throws IOException, ServletException {
ZipFileService zfs= new ZipFileService();
byte[] bytesFile = zfs.getSqlCompress("", true);
response.reset();
response.setHeader("Content-Disposition","attachment;fileName="+"test.zip");
response.addHeader("Content-Length", ""+bytesFile.length);
response.setContentType("application/octet-stream;charset=UTF-8");
ServletOutputStream outputStream = response.getOutputStream();
//new ReturnZip("test.zip",bytesFile).getData()
IOUtils.write(bytesFile, outputStream);
response.flushBuffer();
}}
我尝试了一下,使用原生的ajax去调用接口(大部分情况下,都是用jQuery,原生的还是挺陌生的/(ㄒoㄒ)/~~)。经过反复锤炼代码,最终将代码完善。很重要的一步就是一定要添加xhr.responseType = ‘blob’;,成功下载了文件,调试的时候接收的流数据无法打印(如下图),意味着成功的接收了正确的二进制流数据。打开也不报错。
代码如下:
var url ="http://www.xgp.com:5200/BPTiwInvoiceManage/exportzip"; //要打开的网页地址
// 发送ajax 请求 需要 五步
// 1、异步对象
var xhr = new XMLHttpRequest();
// 2、设置属性.responseType一定要设置。
xhr.open('post', url);
xhr.responseType = 'blob';
// 如果想要使用post提交数据,必须添加此行
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
// 3、将数据通过send方法传递
xhr.send("filterList=" + JSON.stringify(filterList));
// 发送并接受返回值
xhr.onreadystatechange = function () {
// 5、获取异步调用返回的数据。
if (xhr.readyState == 4 && xhr.status == 200) {
//alert(xhr.responseText);二进制流下无法打印
var type = xhr.getResponseHeader('Content-Type');
var blob = new Blob([this.response], {type: type});
var URL = window.URL || window.webkitURL;
var objectUrl = URL.createObjectURL(blob);
var fileName = decodeURI(xhr.getResponseHeader("Content-Disposition").split("filename=")[1]);
if (fileName) {
var a = document.createElement('a');
if (typeof a.download === 'undefined') {
window.location = objectUrl;
} else {
a.href = objectUrl;
a.download = fileName;
document.body.appendChild(a);
a.click();
a.remove();
}
} else {
window.location = objectUrl;
}
}
};
再次寻找资料,发现jQuery的AJAX默认不接收二进制数据,需要其他的参数配合。默认的jQuery的dataType只能设置以下选项,不能设置blob,所以我们需要自己进行改动。
增加以下属性
processData: false,
dataType: 'binary',
xhrFields: { responseType: "blob" },
最佳实践:
$.ajax({
url: downloadZipUrl, //Server script to process data
type: 'POST',
data: "123456789(你的数据)", // Form data
cache: false, //Options to tell jQuery not to process data or worry about content-type.
contentType: 'application/json',
processData: false,
dataType: 'binary',
xhrFields: { responseType: "blob" },
success(data, textStatus, xhr) {
alert('请求成功');
var type = xhr.getResponseHeader('Content-Type');
var blob = new Blob([data], {type: type});
var URL = window.URL || window.webkitURL;
var objectUrl = URL.createObjectURL(blob);
var fileName = decodeURI(xhr.getResponseHeader("Content-Disposition").split("filename=")[1]);
if (fileName) {
var a = document.createElement('a');
if (typeof a.download === 'undefined') {
window.location = objectUrl;
} else {
a.href = objectUrl;
a.download = fileName;
document.body.appendChild(a);
a.click();
a.remove();
}
} else {
window.location = objectUrl;
}
},
error(jqXHR, textStatus, errorThrown) {
console.log('fail');
}
});
最后附上Java后端的压缩代码:
import org.apache.commons.io.IOUtils;
/*
*把sourceFilePath路径下的文件压缩并将压缩包返回浏览器响应response
*/
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class FileZipUtil {
public static void exportZip(HttpServletResponse response, String sourceFilePath) {
//文件名以时间戳作为前缀
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
String filePrefix = sdf.format(new Date());
String downloadName = filePrefix + ".zip";
//将文件进行打包下载
try {
OutputStream out = response.getOutputStream();
//接收压缩包字节
byte[] data = createZip(sourceFilePath);
deleteFile(new File(sourceFilePath));
response.addHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Expose-Headers", "*");
response.setHeader("Content-disposition", "attachment;filename=" + downloadName);
response.addHeader("Content-Length", "" + data.length);
response.setContentType("application/octet-stream;charset=UTF-8");
IOUtils.write(data,out);
} catch (Exception e) {
e.printStackTrace();
}
}
private static void handlerFile(ZipOutputStream zip, File file, String dir) throws Exception {
//如果当前的是文件夹,则进行进一步处理
if (file.isDirectory()) {
//得到文件列表信息
File[] fileArray = file.listFiles();
if (fileArray == null) {
return;
}
//将文件夹添加到下一级打包目录
zip.putNextEntry(new ZipEntry(dir + "/"));
dir = dir.length() == 0 ? "" : dir + "/";
//递归将文件夹中的文件打包
for (File f : fileArray) {
handlerFile(zip, f, dir + f.getName());
}
} else {
//当前的是文件,打包处理
//创建文件缓冲输入流,读取目标文件
FileInputStream fin = new FileInputStream(file);
ZipEntry entry = new ZipEntry(dir);
zip.putNextEntry(entry);
int length;
byte[] buffer = new byte[1024];
while((length = fin.read(buffer)) > 0) {
zip.write(buffer, 0, length);
}
zip.flush();
fin.close();
zip.closeEntry();
}
}
//返回二进制压缩包文件流
private static byte[] createZip(String sourceFilePath) throws Exception{
//1、创建字节数组输出流,用于返回压缩后的输出流字节数组
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
//2、创建压缩输出流
ZipOutputStream zip = new ZipOutputStream(outputStream);
//将目标文件打包成zip导出
File file = new File(sourceFilePath);
handlerFile(zip, file,"");
//IOUtils.closeQuietly(zip);
zip.close();
return outputStream.toByteArray();
}
//删除文件
private static void deleteFile(File file) {
if (file.exists()) {//判断文件是否存在
if (file.isFile()) {//判断是否是文件
file.delete();//删除文件
} else if (file.isDirectory()) {//否则如果它是一个目录
File[] files = file.listFiles();//声明目录下所有的文件 files[];
for (int i = 0; i < files.length; i++) {//遍历目录下所有的文件
deleteFile(files[i]);//把每个文件用这个方法进行迭代
}
file.delete();//删除文件夹
}
} else {
System.out.println("所删除的文件不存在");
}
}
}