问题描述

博主最近要实现一个功能,前端上传一个Excel,后端对表格里面的数据进行处理,处理之后将存在错误的数据的错误原因重新写入一个新的Excel文件里面,并用字节流返回给前端,对于Excel的处理使用了EasyExel,大概后端代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public ResponseEntity<byte[]> uploadDoc(MultipartFile docFile, Long clsId) {
if (docFile == null || docFile.isEmpty()){
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}

if (!Objects.requireNonNull(docFile.getOriginalFilename(),"源文件为空!").endsWith(".xlsx")){
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}

ArrayList<LearnerExcel> errors = new ArrayList<>();
try {
EasyExcel.read(docFile.getInputStream(), LearnerExcel.class,new PageReadListener<LearnerExcel>(list -> {

// 自定义处理逻辑

})).sheet().doRead();

if (!ObjectUtils.isEmpty(errors)){
// 回写给前端
ByteArrayOutputStream out = new ByteArrayOutputStream();
// 将数据写入到 ByteArrayOutputStream 中
EasyExcel.write(out, LearnerExcel.class).sheet(0).doWrite(errors);

// 设置响应头信息
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
String fileName = "错误提示.xlsx";
fileName = URLEncoder.encode(fileName, "UTF-8");
headers.setContentDispositionFormData("attachment", fileName);
return ResponseEntity.ok().headers(headers).body(out.toByteArray());

}

// 前端接收
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
} catch (IOException e) {
log.error("EasyExcel加载文件错误");
throw new RuntimeException(e);
}

}

没啥好说的,就是拿官方的demo改了一下用。之后就是前端接收数据了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
userUpload(formData,config).then(res =>{
// 接收到后端返回的文件数据
if (res.status === 200){
// 利用Blob接收文件
const blob = new Blob([res.data], { type: 'application/octet-stream;charset=utf-8' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = '错误提示.xlsx'; // 指定文件名
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
}

})

也没啥好说的,就是把返回的二进制数据转换成一个Blob对象,然后生成a标签实现下载。

确实也没啥问题,文件也下载好了,但是当我尝试打开文件的时候却发现报错:

image-20240417174650676

image-20240417174737925

也就是文件损坏

问题分析

博主于是进行排查,首先想到的是我后端输出流写Excel的时候是不是方式不对?因为确实没咋用过EasyExcel,难免怀疑自己是不是哪里写错了,于是找了一个已经存在的文件,回写之后依旧是无法打开文件,报错文件损坏,那大概我的后端没啥问题了,应该是前端的接收数据的时候哪里出错了

那么前端看看控制台,打印一下responeblob

image-20240417175456202

然后注意到网络里面对于请求的预览是堆乱码

image-20240417175621735

由于是二进制流,我也不能断言是这个乱码是不是正常现象,貌似现在陷入了死局,我的前端水平也就这种水平,估计是分析不出了。

问题解决

只能自己网上搜索了,但经过上面的分析,大概可以确定问题在前端上,而且还就是那个blob解析数据流,那么凭借这几点直接搜索,果然获得了答案:

其实说的都是一个问题,就是前端请求后端的时候,没有指定返回类型responseType。那么尝试修改请求:

1
2
3
4
5
6
axios({
url: myURL,
method: "POST",
responseType: "blob"
}
// 用的框架不同,改的方式大致也有点区别,不过都是添加:responseType: 'blob'

改起来还挺快的,再次尝试,发现成功解决问题。

问题拓展

那么为什么加上这个就能解决问题了呢?

首先JSBlob对象是什么?

在JavaScript中,Blob(Binary Large Object)对象表示了一个不可变的、原始数据的类文件对象。它通常用于存储二进制数据,比如图像、音频、视频等等。Blob对象可以通过多种方式创建,比如使用Blob构造函数,或者使用File对象的slice()方法创建。一旦创建,Blob对象的数据内容是不可修改的。Blob对象通常用于处理文件上传、处理二进制数据、以及在浏览器中进行文件操作等场景。

那么进一步分析指定和没指定responseType类型时,处理返回结果有何区别:

当使用Axios发送请求时,设置responseTypeBlob会告诉Axios将响应数据以二进制形式返回,而不是默认的JSON格式。如果不指定responseTypeAxios将默认以JSON格式解析响应数据。

  • 设置responseTypeBlob

    如果将responseType设置为Blob,则响应数据将以Blob对象的形式返回给你。你可以直接操作这个Blob对象,比如将其转换为URL以供下载或展示,或者将其作为文件上传到服务器。

  • 未设置responseType

    如果不设置responseTypeAxios将默认以JSON格式解析响应数据。这意味着响应数据将被解析为JavaScript对象,并且你可以直接访问响应数据的属性。

结合上面所说,我的理解是二进制流本身是有个对数据要求很严格,而没指定返回类型,默认就把二进制流转成了字符串,而之后又把字符串转回blob对象的时候,中间可能就产生了意外的错误,比如编码格式啥的,进而造成二进制流损坏,也就是要下载的文件也损坏了;而从一开始指定为blob对象,接收到的二进制流是什么就是什么,中间不会再有变化。

当然这是博主的猜测,但是大致感觉就是这个原因了,究其根本还是自己对前端不是太熟。