项目场景
需求:
- 前端上传Excel文件,导入数据库,后端校验不通过直接返回并下载带有失败信息的Excel文件,例如:返回一个Excel文件以及失败的条数、成功的条数。
问题描述
后端需求
-
解析Excel,并导入数据库,返回数据导入成功和失败的条数。
-
将导入失败的数据,写入Excel文件中返回的前端。
前端需求:
-
根据后端返回的响应对象,展示成功以及失败数据的条数。
-
根据后端返回的响应对象中的文件流,下载Excel文件。
解决方案
三种解决方案:
第一种方案(取巧方式):
- Excel文件:
- 将数据导出到Excel文件,并将其作为HTTP servlet响应发送。
/**
* 方法用于将数据导出到Excel文件,并将其作为HTTP servlet响应发送。
*
* @param response HttpServletResponse对象,用于编写响应
* @param filename 要下载的Excel文件的名称
* @param sheetName 工作表的名称
* @param head Excel文件的标题类(通常是数据对象的类)
* @param data 要导出到Excel文件的数据列表
* @throws IOException 如果发生IO错误
*/
public static <T> void exportExcel(HttpServletResponse response, String filename, String sheetName,
Class<T> head, List<T> data) throws IOException {
// 对文件名进行编码以处理中文文件名
String encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8);
response.setHeader("Content-Disposition", "attachment; filename=" + encodedFilename);
// 设置响应的内容类型为Excel
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=GBK");
// 使用EasyExcel库将数据写入Excel文件
EasyExcel.write(response.getOutputStream(), head)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 自动适配列宽
.sheet(sheetName).doWrite(data);
}
- 其他数据(少量数据):
- 将设置为HTTP头部的值,前端通过读取header中对应的值达到数据传输的效果。
注意:
虽然response.setHeader()可以用来设置HTTP响应头,但通常情况下,它用于设置元数据而不是传递大量数据。
HTTP头部通常用于传递元数据,如内容类型、内容长度、缓存控制等。
如果尝试传递大量数据,特别是文本数据,可能会遇到一些限制,因为HTTP头部的大小通常是有限制的。
使用response.setHeader()来传递数据,可以将数据转换为字符串,并且确保数据量不会太大以超出HTTP头部的大小限制。
response.setHeader("success_count", String.valueOf(importList.size()));
response.setHeader("error_count", String.valueOf(exceptList.size()));
响应结果:
文件流:
其他信息:
第二种方案:
创建一个响应对象(包含:字节数组(base64) 和 其他属性)
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ResponseObject {
/**
* 成功数量
*/
private Integer success_count;
/**
* 失败数量
*/
private Integer fail_count;
/**
* 导出文件字节数组
*/
private byte[] excelStream;
}
- Excel文件:
- 将生成的outputStream 转换为字节数组并返回。可以读取 outputStream 中的数据并将其存储到字节数组中,然后将字节数组返回给 前端。前端可以使用这个字节数组来下载文件或进行其他操作。
/**
* 处理输入流的实用工具类。
*/
public class ExcelUtils {
/**
* 将输出流转换为字节数组。
*
* @param outputStream 要转换的输出流
* @return 包含从输出流中读取的数据的字节数组
* @throws IOException 如果在从输出流中读取数据时发生 I/O 错误
*/
public static byte[] outputStreamToByteArray(OutputStream outputStream) throws IOException {
try {
// 检查传入的输出流是否是 ByteArrayOutputStream 的实例
if (!(outputStream instanceof ByteArrayOutputStream)) {
throw new IllegalArgumentException("Output stream is not a ByteArrayOutputStream");
}
// 将输出流转换为 ByteArrayOutputStream
ByteArrayOutputStream byteArrayOutputStream = (ByteArrayOutputStream) outputStream;
// 获取 ByteArrayOutputStream 中的字节数组
byte[] byteArray = byteArrayOutputStream.toByteArray();
return byteArray;
} finally {
closeStream(outputStream);
}
}
/**
* 关闭给定的输出流。
*
* @param outputStream 要关闭的输出流
*/
private static void closeStream(OutputStream outputStream) {
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
// 记录异常或根据情况进行处理
e.printStackTrace();
}
}
}
}
/**
* @description: TODO 生成单个工作表的 Excel 文件并返回输出流
* @author: LLong
* @param response HttpServletResponse 对象
* @param filename 导出文件名
* @param sheetName 工作表名称
* @param head Excel 表头类
* @param data Excel 数据列表
* @return OutputStream 包含 Excel 数据的输出流
* @throws IOException
*/
public static <T> OutputStream exportExcelOutputStream(HttpServletResponse response, String filename, String sheetName,
Class<T> head, List<T> data) throws IOException {
// 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了
String encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8);
response.setHeader("Content-Disposition", "attachment; filename=" + encodedFilename);
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=GBK");
// 创建 ByteArrayOutputStream
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
// 写入当前工作表的数据
EasyExcel.write(outputStream, head)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度
.sheet(sheetName).doWrite(data);
// 返回包含 Excel 数据的输出流
return outputStream;
}
/**
* @description: TODO 生成多个工作表的 Excel 文件并返回输出流
* @author: LLong
* @param response HttpServletResponse 对象
* @param filename 导出文件名
* @param sheetNames 工作表名称列表
* @param head Excel 表头类列表
* @param data Excel 数据列表
* @return OutputStream 包含 Excel 数据的输出流
* @throws IOException
*/
public static <T> OutputStream exportBatchExcelInputStream(HttpServletResponse response, String filename, List<String> sheetNames,
List<Class<?>> head, List<List<?>> data) throws IOException {
// 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了
String encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8);
response.setHeader("Content-Disposition", "attachment; filename=" + encodedFilename);
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=GBK");
// 创建 ByteArrayOutputStream
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
// 创建 ExcelWriter
ExcelWriter excelWriter = EasyExcel.write(outputStream).build();
// 循环写入每个工作表
for (int i = 0; i < sheetNames.size(); i++) {
// 获取当前工作表的数据和表头 (数据类型不同)
List<?> dataItem = data.get(i);
Class<?> headItem = head.get(i);
// 创建工作表
WriteSheet writeSheet = EasyExcel.writerSheet(i, sheetNames.get(i))
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
.head(headItem)
.build();
// 写入当前工作表的数据
excelWriter.write(dataItem, writeSheet);
}
// 关闭 ExcelWriter
excelWriter.finish();
// 返回包含 Excel 数据的输出流
return outputStream;
}
// 导出 Excel 数据到 OutputStream
OutputStream outputStream = ExcelUtils.exportExcelOutputStream(response, "导入数据.xlsx", "导入文件", EvaluateLogImportExcelVo.class, exceptList);
// 将 OutputStream 转换为字节数组
byte[] excelByteArray = null;
if (outputStream != null) {
excelByteArray = ExcelUtils.outputStreamToByteArray(outputStream);
}
// 将字节数组设置到 responseObject 中
responseObject.setExcelStream(excelByteArray);
- 其他数据:将其他数据写入返回的响应对象中。
// 设置成功数量和失败数量
responseObject.setSuccessCount(importList.size()); // 设置成功数量
responseObject.setFailCount(exceptList.size()); // 设置失败数量
响应结果:
第三种方案:
创建一个响应对象(包含:字节数组 和 其他属性)
- Excel文件:
- 可以将生成的 InputStream 中的数据写入到一个临时文件中,然后返回该文件的下载链接给前端。前端可以通过这个链接来下载文件。
/**
* 将输入流写入临时文件并返回文件下载链接。
*
* @param inputStream 要写入临时文件的输入流
* @return 写入的临时文件的路径
* @throws IOException 如果在写入临时文件时发生 I/O 错误
*/
public static String writeInputStreamToTempFile(InputStream inputStream) throws IOException {
// 创建临时文件
Path tempFile = Files.createTempFile("temp-file-", ".tmp");
// 写入 InputStream 数据到临时文件
try (FileOutputStream outputStream = new FileOutputStream(tempFile.toFile())) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
}
// 返回临时文件的路径
return tempFile.toString();
}
- 其他数据:将其他数据写入返回的响应对象中。
该方法更适用于大文件,而且前端处理起来更加方便。但是会存在临时文件,会对服务器造成一定的压力,需要定期删除等问题。
响应结果:
前端接收(base64转换为文件)
- 前端接收数据
/**
* 将 Base64 编码的字节数组转换为 Excel 文件格式,并返回 File 对象。
*
* @param {string} data Base64 编码的字节数组
* @param {string} filename 下载的文件名
* @returns {File} Excel 文件的 File 对象
*/
export function getExcelFile(data, filename) {
// 解码 Base64 字符串
let bstr = window.atob(data);
let n = bstr.length;
// 创建 Uint8Array 以保存字节数据
let u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
// 创建 File 对象,并指定文件类型为 Excel
return new File([u8arr], filename, { type: 'application/vnd.ms-excel' });
}
- 文件前端下载导出
/**
* 下载当前的 Excel 文件到本地。
*/
downloadExcel() {
// 调试信息
// console.log(sheet.getAllSheets())
// exportExcel(sheet.getAllSheets(), "导出数据");
// console.log("excelData", this.excelData)
// 使用 FileSaver 库保存文件到本地
FileSaver.saveAs(this.excelData, '导出数据.xlsx');
}
调用函数
// 假设当前的 Excel的base64数据为 excelData,需要下载的文件名为 filename
let file = getExcelFile(excelData, filename);
downloadExcel(file);