一、优势
- 1.easyexcel的枚举值转换需要 用到 @ExcelProperty 的 converter 属性 针对不同枚举进行映射转换有些麻烦。http接口返回前端数据已经有枚举转换了,因此导出也复用这个枚举,这样写代码的时候字段和注解可以copy过去 不需要额外修改
- 2.支持分页导出,为了降低数据库瞬时压力,针对数据量大的导出通常需要进行分页查询,之后将分页结果拼接进行导出,通过此方法便捷分页导出
- 3.支持map结构的动态列,少部分情况要导出的列表是不固定的 如导出不同物料在各个仓库(每多一个仓库列表增加一列)中的库存。此时无法通过class定义表头,需要map结构动态设置列表头
- 4.自动类型转换,数据集合的类型(数据库DTO/entity)通常与导出类不是同一个 ,工具类中会判断两者是否一致,不一致则自动进行转换
- 5.流式编程,可以很便捷写入多个sheet
- 6.预留扩展接口 可以便捷自定义
二、使用案例
1.示例代码
public class TestVO {@EnumView(value = TestStatusEnum.class)@ExcelProperty("状态")private Integer status;@EnumView(value = TestTypeEnum.class, codeField = "code", nameField = "name")@ExcelProperty("类型")private Integer type;
}List<TestVO> dataList = testService.selectList(reqDTO);
PageVO<TestVO> page = testService.selectPage(TestReqDTO testVO);
ExcelExportUtil.httpResponse("文件名_http导出").addSheet("sheet1_列表导出", dataList, TestVO.class).addSheet("sheet2_列表导出(数据列自动转换为导出类)", dataList, TestExcelVO.class).addSheet("sheet3_分页导出", reqDTO, testService::selectPage, TestVO.class).addSheet("sheet4_分页导出(数据列自动转换为导出类)", reqDTO, testService::selectPage, TestExcelVO.class).execute();
ExcelExportUtil.httpResponse("文件名_动态列导出").addSheet("sheet名字1", reqDTO, testService::selectPage, new ExcelDynamicColumn<TestVO>() {@Overridepublic List<List<String>> head() {List<String> list = new ArrayList<>();list.add("列名一");list.add("列名二");for (String warehouseName : warehouseNameList) {list.add(warehouseName);}return List.of(list);}@Overridepublic List<Object> columnConvert(TestVO resDTO) {List<Object> list = new ArrayList<>();list.add(resDTO.getXxx());list.add(resDTO.getXxx());for (Integer stockNum : resDTO.getStockNumList()) {list.add(stockNum);}return list;}}).execute();
ExcelExportUtil.file("D:\\导出测试.xlsx").addSheet("sheet1_列表导出", testExcelVOList, TestVO.class).addSheet("sheet2_列表导出(数据列自动转换为导出类)", testExcelVOList, TestExcelVO.class).addSheet("sheet3_分页导出", reqDTO, testService::selectPage, TestVO.class).addSheet("sheet4_分页导出(数据列自动转换为导出类)", reqDTO, testService::selectPage, TestExcelVO.class).execute();
ExcelExportUtil.httpResponse("文件名_固定列导出").pageParams(100, 50000, "单词可导出最大数量为${maxTotalRows},当前搜索条件查询数量为{totalRows},请调整搜索条件").headCellStyleConfig(config ->{config.getCell().getCellStyle().setAlignment(HorizontalAlignment.CENTER)}).contentCellStyleConfig(config ->{config.getCell().getCellStyle().setAlignment(HorizontalAlignment.CENTER)}).excelWriterConfig(config ->{config.registerWriteHandler(注册其他处理器);}).addSheet("sheet名字1", reqDTO, testService::selectPage, TestExcelVO.class).execute();
2.效果

三、源码
枚举注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
@JacksonAnnotationsInside
@JsonSerialize(using = EnumViewSerialize.class)
public @interface EnumView {Class<? extends Enum<?>> value();String codeField() default "code";String nameField() default "name";}
动态列接口
public interface ExcelDynamicColumn<RD> {List<List<String>> head();List<Object> columnConvert(RD resDTO);}
工具类
package com.xiaopeng.common.excel;import cn.hutool.core.bean.BeanUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
import com.alibaba.excel.write.builder.ExcelWriterBuilder;
import com.alibaba.excel.write.handler.CellWriteHandler;
import com.alibaba.excel.write.handler.context.CellWriteHandlerContext;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import com.alibaba.fastjson2.JSON;
import com.xiaopeng.common.enumUtil.EnumView;
import com.xiaopeng.common.exception.BusinessException;
import com.xiaopeng.common.exception.util.Assert;
import com.xiaopeng.common.page.BasePageRequest;
import com.xiaopeng.common.page.PageVO;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
import org.apache.poi.xssf.usermodel.XSSFColor;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
@Slf4j
public class ExcelExportUtil {private static final int EXPORT_TYPE_HTTP_RESPONSE = 1;private static final int EXPORT_TYPE_FILE = 2;private static final int EXPORT_TYPE_OUTPUT_STREAM = 3;public static ExcelExportHandler httpResponse(String fileName) {ExcelExportHandler excelExportHandler = new ExcelExportHandler(EXPORT_TYPE_HTTP_RESPONSE);excelExportHandler.fileName = fileName;return excelExportHandler;}public static ExcelExportHandler file(String filePath) {return file(new File(filePath));}public static ExcelExportHandler file(File file) {ExcelExportHandler excelExportHandler = new ExcelExportHandler(EXPORT_TYPE_FILE);String fileName = file.getName();excelExportHandler.fileName = fileName.substring(0, fileName.indexOf('.'));excelExportHandler.file = file;return excelExportHandler;}public static ExcelExportHandler outputStream(OutputStream outputStream) {ExcelExportHandler excelExportHandler = new ExcelExportHandler(EXPORT_TYPE_OUTPUT_STREAM);excelExportHandler.outputStream = outputStream;return excelExportHandler;}public static class DataCollectHandler<RE extends BasePageRequest, RS> {private String sheetName;private List<RS> dataList;private Function<RE, PageVO<RS>> pageApi;private RE pageParams;private Class<?> clazz;private ExcelDynamicColumn<RS> dynamicColumn;}public static class ExcelExportHandler {private int exportType;private String fileName = null;private File file = null;private OutputStream outputStream = null;private int pageSize = 1000;private int maxTotalRows = 10000;private String overMaxTotalRowsLimitErrMsg = "单词可导出最大数量为${maxTotalRows},当前搜索条件查询数量为{totalRows},请调整搜索条件";private List<DataCollectHandler> dataCollectHandlerList = new ArrayList<>();private Consumer<ExcelWriterBuilder> excelWriterConfig;private Consumer<CellWriteHandlerContext> headCellStyleConfig;private Consumer<CellWriteHandlerContext> contentCellStyleConfig;public ExcelExportHandler(int exportType) {this.exportType = exportType;}public ExcelExportHandler pageSize(String fileName) {this.fileName = fileName;return this;}public ExcelExportHandler pageSize(int pageSize) {this.pageSize = pageSize;return this;}public ExcelExportHandler maxTotalRows(int maxTotalRows) {this.maxTotalRows = maxTotalRows;return this;}public ExcelExportHandler overMaxTotalRowsLimitErrMsg(String overMaxTotalRowsLimitErrMsg) {this.overMaxTotalRowsLimitErrMsg = overMaxTotalRowsLimitErrMsg;return this;}public ExcelExportHandler pageParams(int pageSize, int maxTotalRows) {this.pageSize = pageSize;this.maxTotalRows = maxTotalRows;return this;}public ExcelExportHandler pageParams(int maxTotalRows, String overMaxTotalRowsLimitErrMsg) {this.maxTotalRows = maxTotalRows;this.overMaxTotalRowsLimitErrMsg = overMaxTotalRowsLimitErrMsg;return this;}public ExcelExportHandler pageParams(int pageSize, int maxTotalRows, String overMaxTotalRowsLimitErrMsg) {this.pageSize = pageSize;this.maxTotalRows = maxTotalRows;this.overMaxTotalRowsLimitErrMsg = overMaxTotalRowsLimitErrMsg;return this;}public <RS> ExcelExportHandler addSheet(String sheetName, List<RS> dataList, Class<?> clazz) {DataCollectHandler<BasePageRequest, RS> dataCollectHandler = new DataCollectHandler<>();dataCollectHandler.sheetName = sheetName;dataCollectHandler.dataList = dataList;dataCollectHandler.clazz = clazz;dataCollectHandlerList.add(dataCollectHandler);return this;}public <RS> ExcelExportHandler addSheet(String sheetName, List<RS> dataList, ExcelDynamicColumn<RS> dynamicColumn) {DataCollectHandler<BasePageRequest, RS> dataCollectHandler = new DataCollectHandler<>();dataCollectHandler.sheetName = sheetName;dataCollectHandler.dataList = dataList;dataCollectHandler.dynamicColumn = dynamicColumn;dataCollectHandlerList.add(dataCollectHandler);return this;}public <RE extends BasePageRequest, RS> ExcelExportHandler addSheet(String sheetName, RE pageParams, Function<RE, PageVO<RS>> pageApi, Class<?> clazz) {DataCollectHandler<RE, RS> dataCollectHandler = new DataCollectHandler<>();dataCollectHandler.sheetName = sheetName;dataCollectHandler.pageParams = pageParams;dataCollectHandler.pageApi = pageApi;dataCollectHandler.clazz = clazz;dataCollectHandlerList.add(dataCollectHandler);return this;}public <RE extends BasePageRequest, RS> ExcelExportHandler addSheet(String sheetName, RE pageParams, Function<RE, PageVO<RS>> pageApi, ExcelDynamicColumn<RS> dynamicColumn) {DataCollectHandler<RE, RS> dataCollectHandler = new DataCollectHandler<>();dataCollectHandler.sheetName = sheetName;dataCollectHandler.pageParams = pageParams;dataCollectHandler.pageApi = pageApi;dataCollectHandler.dynamicColumn = dynamicColumn;dataCollectHandlerList.add(dataCollectHandler);return this;}public ExcelExportHandler headCellStyleConfig(Consumer<CellWriteHandlerContext> headCellStyleConfig) {this.headCellStyleConfig = headCellStyleConfig;return this;}public ExcelExportHandler contentCellStyleConfig(Consumer<CellWriteHandlerContext> contentCellStyleConfig) {this.contentCellStyleConfig = contentCellStyleConfig;return this;}public ExcelExportHandler excelWriterConfig(Consumer<ExcelWriterBuilder> excelWriterConfig) {this.excelWriterConfig = excelWriterConfig;return this;}private ExcelWriter getExcelWriter(ExcelWriterBuilder excelWriterBuilder) {excelWriterBuilder.registerWriteHandler(new StyleWriteHandler(headCellStyleConfig, contentCellStyleConfig));excelWriterBuilder.registerWriteHandler(new ExcelEnumWriteHandler());if (Objects.nonNull(excelWriterConfig)) {excelWriterConfig.accept(excelWriterBuilder);}return excelWriterBuilder.build();}private void handleHttpResponse() {ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();Assert.notNull(servletRequestAttributes, "HttpServletResponse获取失败");HttpServletResponse response = servletRequestAttributes.getResponse();try {response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");response.setCharacterEncoding("utf-8");String fileNameEncode = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");response.setHeader("Content-disposition", "attachment;filename*=" + fileNameEncode + ".xlsx");ExcelWriterBuilder excelWriterBuilder = EasyExcel.write(response.getOutputStream()).autoCloseStream(Boolean.FALSE);ExcelWriter excelWriter = getExcelWriter(excelWriterBuilder);this.handleDataWriting(excelWriter);} catch (Exception e) {log.error("文件导出异常 文件类型:" + fileName, e);try {response.reset();response.setContentType("application/json");response.setCharacterEncoding("utf-8");Map<String, Object> map = new HashMap<>();map.put("code", 500);map.put("success", false);map.put("message", e.getMessage());response.getWriter().println(JSON.toJSONString(map));} catch (IOException ex) {throw new RuntimeException(ex);}}}private void handleFile() {ExcelWriterBuilder excelWriterBuilder = EasyExcel.write(file);ExcelWriter excelWriter = getExcelWriter(excelWriterBuilder);this.handleDataWriting(excelWriter);}private void handleOutputStream() {ExcelWriterBuilder excelWriterBuilder = EasyExcel.write(outputStream);ExcelWriter excelWriter = getExcelWriter(excelWriterBuilder);this.handleDataWriting(excelWriter);}private void handleDataWriting(ExcelWriter excelWriter) {int sheetNo = 0;try {for (; sheetNo < dataCollectHandlerList.size(); sheetNo++) {DataCollectHandler dataCollectHandler = dataCollectHandlerList.get(sheetNo);WriteSheet writeSheet;if (Objects.nonNull(dataCollectHandler.clazz)) {writeSheet = EasyExcel.writerSheet(sheetNo, dataCollectHandler.sheetName).head(dataCollectHandler.clazz).build();} else if (Objects.nonNull(dataCollectHandler.dynamicColumn)) {writeSheet = EasyExcel.writerSheet(sheetNo, dataCollectHandler.sheetName).head(dataCollectHandler.dynamicColumn.head()).build();} else {throw BusinessException.exception("缺失视图映射");}if (Objects.nonNull(dataCollectHandler.dataList)) {writeSheetData(excelWriter, writeSheet,dataCollectHandler.dataList, dataCollectHandler.clazz, dataCollectHandler.dynamicColumn);} else {int currentPageNum = 1;PageVO page = null;do {page = queryPage(dataCollectHandler.pageApi, dataCollectHandler.pageParams, currentPageNum, pageSize);verifyPageTotalNum(page.getTotalRows());writeSheetData(excelWriter, writeSheet, page.getRoot(), dataCollectHandler.clazz, dataCollectHandler.dynamicColumn);currentPageNum++;} while (currentPageNum <= page.getTotalPages());}}} catch (Exception e) {e.printStackTrace();throw BusinessException.exception("sheet-{} 导出异常 {}", sheetNo, e.getMessage());}excelWriter.finish();}private <RE extends BasePageRequest, RS> PageVO<RS> queryPage(Function<RE, PageVO<RS>> pageApi, RE reqDTO, int pageNum, int pageSize) {reqDTO.setPageNum(pageNum);reqDTO.setPageSize(pageSize);reqDTO.setIsCount(pageNum == 1);return pageApi.apply(reqDTO);}private <RS> void verifyPageTotalNum(Long totalRows) {if (maxTotalRows == -1) {return;}Assert.notNull(totalRows, "page总条数为空");if (totalRows > maxTotalRows) {overMaxTotalRowsLimitErrMsg = overMaxTotalRowsLimitErrMsg.replace("${maxTotalRows}", String.valueOf(maxTotalRows));overMaxTotalRowsLimitErrMsg = overMaxTotalRowsLimitErrMsg.replace("${totalRows}", String.valueOf(totalRows));throw BusinessException.exception(overMaxTotalRowsLimitErrMsg);}}private <RE extends BasePageRequest, RS> void writeSheetData(ExcelWriter excelWriter, WriteSheet writeSheet, List<RS> dataList, Class<?> clazz, ExcelDynamicColumn<RS> dynamicColumn) {if (CollectionUtils.isEmpty(dataList)) {return;}if (Objects.nonNull(clazz)) {List<?> excelVOList = dataList;if (dataList.getFirst().getClass() != clazz) {excelVOList = BeanUtil.copyToList(dataList, clazz);}excelWriter.write(excelVOList, writeSheet);} else if (Objects.nonNull(dynamicColumn)) {List<List<Object>> columnDataList = new ArrayList<>(dataList.size());for (RS res : dataList) {columnDataList.add(dynamicColumn.columnConvert(res));}excelWriter.write(columnDataList, writeSheet);} else {throw BusinessException.exception("缺失视图映射");}}public void execute() {if (exportType == EXPORT_TYPE_HTTP_RESPONSE) {this.handleHttpResponse();} else if (exportType == EXPORT_TYPE_FILE) {this.handleFile();} else if (exportType == EXPORT_TYPE_OUTPUT_STREAM) {this.handleOutputStream();} else {throw new RuntimeException("导出类型无效");}}public <RS> void execute(Class<?> clazz) {this.addSheet(fileName, Collections.emptyList(), clazz);execute();}public <RS> void execute(List<RS> dataList, Class<?> clazz) {this.addSheet(fileName, dataList, clazz);execute();}public <RS> void execute( List<RS> dataList, ExcelDynamicColumn<RS> dynamicColumn) {this.addSheet(fileName, dataList, dynamicColumn);execute();}public <RE extends BasePageRequest, RS> void execute(RE pageParams, Function<RE, PageVO<RS>> pageApi, Class<?> clazz) {this.addSheet(fileName, pageParams, pageApi, clazz);execute();}public <RE extends BasePageRequest, RS> void execute(RE pageParams, Function<RE, PageVO<RS>> pageApi, ExcelDynamicColumn<RS> dynamicColumn) {this.addSheet(fileName, pageParams, pageApi, dynamicColumn);execute();}}public static class StyleWriteHandler implements CellWriteHandler {private Consumer<CellWriteHandlerContext> headCellStyleConfig;private Consumer<CellWriteHandlerContext> contentCellStyleConfig;public StyleWriteHandler() { }public StyleWriteHandler(Consumer<CellWriteHandlerContext> headCellStyleConfig, Consumer<CellWriteHandlerContext> contentCellStyleConfig) {this.headCellStyleConfig = headCellStyleConfig;this.contentCellStyleConfig = contentCellStyleConfig;}@Overridepublic void afterCellDispose(CellWriteHandlerContext context) {if (context.getHead() == null) {return;}if (context.getHead()) {setHeadCellStyle(context);} else {setContentCellStyle(context);}}private void setHeadCellStyle(CellWriteHandlerContext context) {Cell cell = context.getCell();Head headData = context.getHeadData();Sheet sheet = context.getWriteSheetHolder().getSheet();Workbook workbook = context.getWriteWorkbookHolder().getWorkbook();CellStyle cellStyle = workbook.createCellStyle();cellStyle.cloneStyleFrom(cell.getCellStyle());Row row = cell.getRow();row.setHeightInPoints(24); Field field = headData.getField();ColumnWidth columnWidth = field.getAnnotation(ColumnWidth.class);if (columnWidth == null) {sheet.setColumnWidth(cell.getColumnIndex(), Math.min((cell.getStringCellValue().getBytes().length + 3) * 256, 20 * 256));}sheet.createFreezePane(0, 1);if (cellStyle instanceof XSSFCellStyle xssfCellColorStyle) {byte[] rgb = new byte[]{(byte) 24, (byte) 112, (byte) 255};xssfCellColorStyle.setFillForegroundColor(new XSSFColor(rgb, null));cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);}cellStyle.setAlignment(HorizontalAlignment.CENTER);cellStyle.setVerticalAlignment(VerticalAlignment.CENTER);cellStyle.setLeftBorderColor(IndexedColors.WHITE.getIndex());cellStyle.setRightBorderColor(IndexedColors.WHITE.getIndex());cellStyle.setBorderLeft(BorderStyle.THIN);cellStyle.setBorderRight(BorderStyle.THIN);cellStyle.setBorderTop(BorderStyle.THIN);cellStyle.setBorderBottom(BorderStyle.THIN);Font font = workbook.createFont();font.setFontName("微软雅黑");
font.setFontHeightInPoints((short) 11);font.setColor(IndexedColors.WHITE.getIndex());cellStyle.setFont(font);cell.setCellStyle(cellStyle);if(Objects.nonNull(headCellStyleConfig)) {headCellStyleConfig.accept(context);}context.getFirstCellData().setWriteCellStyle(null);}private void setContentCellStyle(CellWriteHandlerContext context) {Cell cell = context.getCell();Workbook workbook = context.getWriteWorkbookHolder().getWorkbook();CellStyle cellStyle = workbook.createCellStyle();Row row = cell.getRow();row.setHeightInPoints(18); cellStyle.setAlignment(HorizontalAlignment.CENTER);cellStyle.setVerticalAlignment(VerticalAlignment.CENTER);Font font = workbook.createFont();font.setFontName("微软雅黑");font.setFontHeightInPoints((short) 10);cellStyle.setFont(font);cell.setCellStyle(cellStyle);if(Objects.nonNull(contentCellStyleConfig)) {contentCellStyleConfig.accept(context);}context.getFirstCellData().setWriteCellStyle(null);}}public static class ExcelEnumWriteHandler implements CellWriteHandler {@Overridepublic void afterCellDispose(CellWriteHandlerContext context) {if (context.getHead() == null || context.getHead()) {return;}ExcelContentProperty excelContentProperty = context.getExcelContentProperty();Field field = excelContentProperty.getField();if (Objects.nonNull(field)) {EnumView excelView = field.getAnnotation(EnumView.class);if (Objects.nonNull(excelView)) {Object originalValue = context.getOriginalValue();if (Objects.nonNull(originalValue)) {Map<Object, String> enumConvertMap = this.getEnumConvertMap(field, excelView);String newValue = enumConvertMap.get(originalValue);if (Objects.nonNull(newValue)) {context.getCell().setCellValue(newValue);}}}}}private final Map<String, Map<Object, String>> fieldEnumConvertCacheMap = new HashMap<>();private Map<Object, String> getEnumConvertMap(Field field, EnumView excelView) {String className = field.getDeclaringClass().getName();String cacheKey = className + ":" + field.getName();try {if (fieldEnumConvertCacheMap.containsKey(cacheKey)) {return fieldEnumConvertCacheMap.get(cacheKey);}Map<Object, String> enumConvertMap = this.getEnumConvertMap(excelView);fieldEnumConvertCacheMap.put(cacheKey, enumConvertMap);return enumConvertMap;} catch (Exception e) {log.error("Excel导出-获取枚举转换异常 映射字段:{} 异常;{}", cacheKey, e.getMessage(), e);throw new RuntimeException(e);}}private Map<Object, String> getEnumConvertMap(EnumView excelView) throws Exception {Class<? extends Enum<?>> enumClass = excelView.value();Map<Object, String> result = new HashMap<>();Enum<?>[] enumConstants = enumClass.getEnumConstants();Field codeField = getField(enumClass, excelView.codeField());Field nameField = getField(enumClass, excelView.nameField());for (Enum<?> enumItem : enumConstants) {Object code = codeField.get(enumItem);;String name = (String) nameField.get(enumItem);;if (Objects.nonNull(code) && Objects.nonNull(name)) {result.put(code, name);}}return result;}private Field getField(Class clazz, String fieldName){try {Field field = clazz.getDeclaredField(fieldName);field.setAccessible(true);return field;} catch (NoSuchFieldException e) {throw new RuntimeException(clazz.getName() + "不存在字段" + fieldName);}}}}