一、官方资料
GitHub - alibaba/easyexcel: 快速、简洁、解决大文件内存溢出的java处理Excel工具
二、导出工具封装
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.2.1</version>
</dependency>
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.util.MapUtils;
import com.alibaba.fastjson.JSON;
import com.alibaba.nacos.common.utils.JacksonUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.*;
import java.util.stream.Collectors;
/**
* 通用 XLS 下载封装
* mybatis 中使用 List<List<?>> 接收多个结果集
* 一个 List<?> 代表一个结果集
*
* @author yushanma
* @since 2023/3/12 16:06
*/
public class CommonExcelExportUtil {
private final static Long PAGE_SIZE = 100000L;
private static final Logger logger = LogManager.getLogger(CommonExcelExportUtil.class.getName());
/**
* 保存到 xls 文件
* @param fileName 文件名
* @param dataArray 数据集
*/
public static void saveLocalXlsFile(String fileName, Object[] dataArray) {
try {
if (dataArray != null && dataArray.length > 0) {
String[] header = getHeader(dataArray[0]);
List<List<String>> xlsHeader = getXlsHeader(header);
List<List<Object>> xlsData = getXlsDataV2(header, dataArray);
EasyExcel.write(fileName).head(xlsHeader).sheet("test_sheet").doWrite(xlsData);
} else {
EasyExcel.write(fileName).head(Collections.emptyList()).sheet("test_sheet").doWrite(Collections.emptyList());
}
} catch (Exception e) {
logger.error(e.getMessage());
}
}
/**
* 返回文件流响应
*
* @param response HttpServletResponse
* @throws IOException IOException
*/
public static void returnXlsFile(HttpServletResponse response, Object[] dataArray) throws IOException {
try {
// 这里注意 使用swagger 会导致各种问题,请直接用浏览器或者用postman
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码
// 下载文件名
String fileName = URLEncoder.encode("test_xls_file", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
if (dataArray != null && dataArray.length > 0) {
String[] header = getHeader(dataArray[0]);
List<List<String>> xlsHeader = getXlsHeader(header);
List<List<Object>> xlsData = getXlsDataV2(header, dataArray);
EasyExcel.write(response.getOutputStream()).head(xlsHeader).sheet("test_sheet").doWrite(xlsData);
} else {
EasyExcel.write(response.getOutputStream()).head(Collections.emptyList()).sheet("test_sheet").doWrite(Collections.emptyList());
}
} catch (Exception e) {
// 重置response
response.reset();
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
Map<String, String> map = MapUtils.newHashMap();
map.put("status", "failure");
map.put("message", "下载文件失败" + e.getMessage());
response.getWriter().println(JSON.toJSONString(map));
}
}
/**
* 解析 header 返回
*
* @return header 列表
*/
private static List<List<String>> getXlsHeader(String[] header) {
List<List<String>> list = new ArrayList<List<String>>();
for (String h : header) {
List<String> head = new ArrayList<String>();
head.add(h);
list.add(head);
}
return list;
}
/**
* 解析数据内容返回
*
* @return data 列表
*/
private static List<List<Object>> getXlsDataV1(String[] header, Object[] dataset) {
List<List<Object>> list = new ArrayList<List<Object>>();
for (Object obj : dataset) {
List<Object> data = new ArrayList<Object>();
for (String s : header) {
// fastjson
// String jsonString = JSON.toJSONString(obj);
// HashMap<String, Object> objectMap = (HashMap<String, Object>)JSON.parseObject(jsonString, HashMap.class);
// jackson
String jsonString = JacksonUtils.toJson(obj);
HashMap<String, Object> objectMap = (HashMap<String, Object>) JacksonUtils.toObj(jsonString, HashMap.class);
data.add(objectMap.get(s));
}
list.add(data);
}
return list;
}
/**
* 解析数据内容返回
*
* @return data 列表
*/
private static List<List<Object>> getXlsDataV2(String[] header, Object[] dataset) {
List<List<Object>> list = new ArrayList<List<Object>>();
long pageMax = dataset.length / PAGE_SIZE;
// 小于10W数据时
if(dataset.length > 0 && pageMax == 0){
pageMax = 1;
}
// 百万数据分页
for (int i = 0; i < pageMax; i++) {
List<Object> collect = Arrays.stream(dataset).skip(i * PAGE_SIZE).limit(PAGE_SIZE).collect(Collectors.toList());
// fastjson
// String jsonString = JSON.toJSONString(collect);
// List<HashMap<String, Object>> objectMapList = (List<HashMap<String, Object>>) JSON.parseObject(jsonString, List.class);
// jackson
String jsonString = JacksonUtils.toJson(collect);
List<HashMap<String, Object>> objectMapList = (List<HashMap<String, Object>>) JacksonUtils.toObj(jsonString, List.class);
for (HashMap<String, Object> objectMap : objectMapList) {
List<Object> data = new ArrayList<Object>();
for (String s : header) {
data.add(objectMap.get(s));
}
list.add(data);
}
}
return list;
}
/**
* 获取 header
*
* @param obj
* @return
*/
private static String[] getHeader(Object obj) {
// fastjson
// String jsonString = JSON.toJSONString(obj);
// HashMap<String, Object> objectMap = (HashMap<String, Object>)JSON.parseObject(jsonString, HashMap.class);
// jackson
String jsonString = JacksonUtils.toJson(obj);
HashMap<String, Object> objectMap = (HashMap<String, Object>) JacksonUtils.toObj(jsonString, HashMap.class);
return objectMap.keySet().toArray(new String[objectMap.keySet().size()]);
}
}
将 List 结果集转 Object 数组,再通过序列化 HashMap<K,V> 获取对于的列与值,这里采用 Jackson 替代 Fastjson 。
三、测试
public static void dynamicHeadWrite() throws IOException {
String fileName = "D:\\test\\" + System.currentTimeMillis() + ".xlsx";
List<TestEntity> list = new ArrayList<>(10);
for (int i = 0; i < 500000; i++) {
TestEntity data = new TestEntity();
data.setFeild1(UUID.randomUUID().toString());
data.setFeild2(UUID.randomUUID().toString());
data.setFeild3(UUID.randomUUID().toString());
data.setFeild4(UUID.randomUUID().toString());
data.setFeild5(UUID.randomUUID().toString());
data.setFeild6(UUID.randomUUID().toString());
data.setFeild7(UUID.randomUUID().toString());
data.setFeild8(UUID.randomUUID().toString());
data.setFeild9(UUID.randomUUID().toString());
data.setFeild10(UUID.randomUUID().toString());
data.setFeild11(UUID.randomUUID().toString());
data.setFeild12(UUID.randomUUID().toString());
data.setFeild13(UUID.randomUUID().toString());
data.setFeild14(UUID.randomUUID().toString());
data.setFeild15(UUID.randomUUID().toString());
data.setFeild16(UUID.randomUUID().toString());
data.setFeild17(UUID.randomUUID().toString());
data.setFeild18(UUID.randomUUID().toString());
data.setFeild19(UUID.randomUUID().toString());
data.setFeild20(UUID.randomUUID().toString());
list.add(data);
}
System.out.println(">>> list size " + list.size());
LocalDateTime start = LocalDateTime.now();
CommonExcelExportUtil.saveLocalXlsFile(fileName, list.toArray());
LocalDateTime end = LocalDateTime.now();
Duration cost = Duration.between(start, end);
System.out.println(">>> start " + start.toString() + " >>> end " + end.toString() + " >>> cost " +cost.toString());
}
50W 行数据,20 列,每列 36 字符,累计 3.6 亿字符,耗时 68 秒。
四、自适应列宽策略
/**
* 重写自适应列宽策略
*/
static class CustomLongestMatchColumnWidthStyleStrategy extends AbstractColumnWidthStyleStrategy {
private static final int MAX_COLUMN_WIDTH = 255;
private final Map<Integer, Map<Integer, Integer>> cache = MapUtils.newHashMapWithExpectedSize(8);
public CustomLongestMatchColumnWidthStyleStrategy() {
}
@Override
protected void setColumnWidth(WriteSheetHolder writeSheetHolder, List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
boolean needSetWidth = isHead || !CollectionUtils.isEmpty(cellDataList);
if (needSetWidth) {
HashMap<Integer, Integer> maxColumnWidthMap = (HashMap)this.cache.computeIfAbsent(writeSheetHolder.getSheetNo(), (key) -> {
return new HashMap(16);
});
Integer columnWidth = this.dataLength(cellDataList, cell, isHead);
if (columnWidth >= 0) {
if (columnWidth > MAX_COLUMN_WIDTH) {
columnWidth = 255;
}
Integer maxColumnWidth = (Integer)maxColumnWidthMap.get(cell.getColumnIndex());
if (maxColumnWidth == null || columnWidth > maxColumnWidth) {
maxColumnWidthMap.put(cell.getColumnIndex(), columnWidth);
writeSheetHolder.getSheet().setColumnWidth(cell.getColumnIndex(), columnWidth * 256);
}
}
}
}
private Integer dataLength(List<WriteCellData<?>> cellDataList, Cell cell, Boolean isHead) {
if (isHead) {
return cell.getStringCellValue().getBytes().length;
} else {
WriteCellData<?> cellData = (WriteCellData)cellDataList.get(0);
CellDataTypeEnum type = cellData.getType();
if (type == null) {
return -1;
} else {
switch(type) {
case STRING:
return cellData.getStringValue().getBytes().length;
case BOOLEAN:
return cellData.getBooleanValue().toString().getBytes().length;
case NUMBER:
return cellData.getNumberValue().toString().getBytes().length;
default:
return -1;
}
}
}
}
}
/**
* 保存到 xls 文件
* @param fileName 文件名
* @param dataArray 数据集
*/
public static void saveLocalXlsFile(String fileName, Object[] dataArray) {
try {
if (dataArray != null && dataArray.length > 0) {
String[] header = getHeader(dataArray[0]);
List<List<String>> xlsHeader = getXlsHeader(header);
List<List<Object>> xlsData = getXlsDataV2(header, dataArray);
EasyExcel
.write(fileName)
//.registerWriteHandler(getCustomHorizontalCellStyleStrategy())
.registerWriteHandler(new CustomLongestMatchColumnWidthStyleStrategy())
.head(xlsHeader)
.sheet("test_sheet")
.doWrite(xlsData);
} else {
EasyExcel.write(fileName).head(Collections.emptyList()).sheet("test_sheet").doWrite(Collections.emptyList());
}
} catch (Exception e) {
logger.error(e.getMessage());
}
}
效果:10W20C,累积 7.2KW 字符,耗时 21 S
五、优化
六、更正错误
这里更正一个错误写法,无需二次加工数据集。
最优正解应为:直接传 List<Object> 给 easyExcel 去写,表头设置可以在 EasyExcel.write() 里加一个实体类的 class 对象。
感谢 @告辞927 指出的错误。