SpringBoot种如何使用 EasyExcel 实现自定义表头导出并实现数据格式化转换
作者:鲁子狄
本文详细介绍了如何使用EasyExcel工具类实现自定义表头导出,并实现数据格式化转换与添加下拉框操作,通过示例和代码,展示了如何处理不同数据结构和注解,确保数据在导出时能够正确显示和格式化,此外,还介绍了如何解决特定数据类型的转换问题,并提供了解决方案
SpringBoot3 使用 EasyExcel 封装工具类实现 自定义表头 导出并实现 数据格式化转换 与 添加下拉框 操作
在现代企业应用中,数据导出功能是非常常见的需求。特别是在处理大量数据时,将数据导出为 Excel
文件不仅方便用户查看和分析,还能提高数据处理的效率。Apache POI
是一个常用的 Java Excel 处理库,但它在处理大数据量时性能较差。为此,阿里巴巴开源了 EasyExcel
,这是一个基于 Java
的简单、优雅的 Excel
操作库,它在处理大数据量时表现优异。
本文将详细介绍如何使用 EasyExcel
实现自定义表头导出,并实现数据格式化转换。
官网 : EasyExcel
封装了很多导出方法针对不同的业务场景,这里直接展示自定义表头的代码 .
1. 使用示例
Vo 示例(部分字段)
使用方法 export
用户自定义表头的 字段显示名称 , 需跟导出注解 @ExcelProperty
的 value
相对应 , 不然无法取到值
页面显示数据
导出 Excel 数据
2. 工具类
/** * Excel相关处理 * * @author 鲁子狄 */ @NoArgsConstructor(access = AccessLevel.PRIVATE) public class ExcelUtil { /** * 导出 Excel 文件 * * @param list 导出数据集合 * @param sheetName 工作表的名称 * @param heads 表头 * @param response 响应体 */ public static <T> void exportExcel(List<T> list, Class<T> clazz, String sheetName, List<List<String>> heads, HttpServletResponse response) { try { // 将对象列表转换为自定义的数据列表 List<List<Object>> data = ExcelUtil.convertObjectsToExcelRows(list, heads); // 重置响应体,设置响应头 ExcelUtil.resetResponse(sheetName, response); // 获取响应输出流 ServletOutputStream os = response.getOutputStream(); // 导出 Excel 文件 ExcelUtil.exportExcel(data, clazz, heads, sheetName, false, os, null); } catch (IOException e) { throw new RuntimeException("导出 Excel 异常"); } } /** * 重置响应体 */ private static void resetResponse(String sheetName, HttpServletResponse response) throws UnsupportedEncodingException { String filename = ExcelUtil.encodingFilename(sheetName); FileUtils.setAttachmentResponseHeader(response, filename); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8"); } /** * 导出 Excel 文件 * * @param data 导出数据集合 * @param heads 表头 * @param sheetName 工作表的名称 * @param merge 是否合并单元格 * @param os 输出流 * @param options 级联下拉选内容 */ public static <T> void exportExcel(List<List<Object>> data, Class<T> clazz, List<List<String>> heads, String sheetName, boolean merge, OutputStream os, List<DropDownOptions> options) { ExcelWriterSheetBuilder builder = EasyExcel.write(os, clazz) .autoCloseStream(false) // 自动适配列宽 .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 大数值自动转换,防止失真 .registerConverter(new ExcelBigNumberConvert()) .sheet(sheetName) .head(heads); if (merge) { // 注册合并单元格处理器 builder.registerWriteHandler(new CellMergeStrategy(data, true)); } // 添加下拉框操作 builder.registerWriteHandler(new ExcelDownHandler(options, heads)); // 写入数据 builder.doWrite(data); } /** * convertObjectsToExcelRows 将对象列表转换为 Excel 行数据 * * @param list 对象集合 * @param heads 表头 * @return {@link List<List<Object>>} Excel 行数据 */ private static <T> List<List<Object>> convertObjectsToExcelRows(List<T> list, List<List<String>> heads) { return list.stream() // 将对象转换为 Map .map(ExcelUtil::convertObjectToMap) .map(map -> { List<Object> row = new ArrayList<>(); for (List<String> header : heads) { // 假设表头只有一个元素 String key = header.get(0); // 根据表头获取对应的值 row.add(map.get(key)); } return row; }) .toList(); } /** * convertObjectToMap 将对象转换为 Map,键为 @ExcelProperty 注解中的列标题,值为字段值。 * * @param vo 对象 * @return {@link Map<String,Object>} 包含列标题和字段值的 Map */ private static Map<String, Object> convertObjectToMap(Object vo) { // 获取对象的所有字段 return Arrays.stream(vo.getClass().getDeclaredFields()) // 过滤带有 @ExcelProperty 注解的字段 .filter(field -> field.isAnnotationPresent(ExcelProperty.class)) .map(field -> { try { // 设置字段可访问 field.setAccessible(true); // 获取字段名和值 String fieldName = field.getDeclaredAnnotation(ExcelProperty.class).value()[0]; Object fieldValue = field.get(vo); // 检查是否有 @ExcelDictFormat 注解 ExcelDictFormat dictFormat = field.getDeclaredAnnotation(ExcelDictFormat.class); if (dictFormat != null && fieldValue != null) { fieldValue = convertExcelDictFormat(fieldValue, dictFormat); } return new AbstractMap.SimpleEntry<>(fieldName, fieldValue); } catch (IllegalAccessException e) { throw new IllegalStateException("获取字段值失败: " + field.getName(), e); } }) // 过滤掉值为 null 的条目 .filter(entry -> entry.getValue() != null) .collect(Collectors.toMap( // 键为字段名 AbstractMap.SimpleEntry::getKey, // 值为字段值 AbstractMap.SimpleEntry::getValue, // 如果键重复,保留第一个值 (existing, replacement) -> existing, // 使用 HashMap 存储结果 HashMap::new )); } /** * convertToExcelData 根据 @ExcelDictFormat 注解转换数据 * * @param value 字段值 * @param dictFormat 注解信息 * @return 转换后的值 */ private static String convertExcelDictFormat(Object value, ExcelDictFormat dictFormat) { String type = dictFormat.dictType(); String label; if (StringUtils.isBlank(type)) { label = convertByExp(value.toString(), dictFormat.readConverterExp(), dictFormat.separator()); } else { label = SpringUtils.getBean(DictService.class).getDictLabel(type, value.toString(), dictFormat.separator()); } return label; } } /** * 解析导出值 0=男,1=女,2=未知 * * @param propertyValue 参数值 * @param converterExp 翻译注解 * @param separator 分隔符 * @return 解析后值 */ public static String convertByExp(String propertyValue, String converterExp, String separator) { StringBuilder propertyString = new StringBuilder(); String[] convertSource = converterExp.split(StringUtils.SEPARATOR); for (String item : convertSource) { String[] itemArray = item.split("="); if (StringUtils.containsAny(propertyValue, separator)) { for (String value : propertyValue.split(separator)) { if (itemArray[0].equals(value)) { propertyString.append(itemArray[1] + separator); break; } } } else { if (itemArray[0].equals(propertyValue)) { return itemArray[1]; } } } return StringUtils.stripEnd(propertyString.toString(), separator); }
注意
- 在使用
EasyExcel
导出 Excel 文件时,convertToExcelData
方法的调用与否取决于你如何传递数据以及数据的结构。 - 传入单个对象或对象列表时
- 当你传入一个对象列表(如
List<User>
),EasyExcel
会根据对象的属性和字段上的注解来决定如何处理每个字段。如果某个字段上有@ExcelDictFormat
注解,并且你已经注册了相应的转换器(如ExcelDictConvert
),那么EasyExcel
会在导出时调用convertToExcelData
方法。- 传入嵌套列表
List<List<Object>>
时 (自定义表头使用) - 当你传入一个嵌套列表
List<List<Object>>
时,EasyExcel
不会自动解析对象的属性和字段注解。这是因为List<List<Object>>
是一个二维数组,每个元素都是一个Object
,EasyExcel
无法知道这些Object
应该如何转换。 - 因为自定义表头使用
List<List<Object>>
, 所以我的解决方案(convertObjectToMap
方法)是:Map
表示动态数据:将List<List<Object>>
转换为List<Map<String, Object>>
,其中键是字段名,值是字段值。- 手动调用转换器:在导出数据之前,手动调用
convertToExcelData
方法进行数据转换。
- 传入嵌套列表
3. ExcelBigNumberConvert 大数值自动转换,防止失真
/** * 大数值转换 * Excel 数值长度位15位 大于15位的数值转换位字符串 */ @Slf4j public class ExcelBigNumberConvert implements Converter<Long> { @Override public Class<Long> supportJavaTypeKey() { return Long.class; } @Override public CellDataTypeEnum supportExcelTypeKey() { return CellDataTypeEnum.STRING; } @Override public Long convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { return Convert.toLong(cellData.getData()); } @Override public WriteCellData<Object> convertToExcelData(Long object, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { if (ObjectUtil.isNotNull(object)) { String str = Convert.toStr(object); if (str.length() > 15) { return new WriteCellData<>(str); } } WriteCellData<Object> cellData = new WriteCellData<>(new BigDecimal(object)); cellData.setType(CellDataTypeEnum.NUMBER); return cellData; } }
4. CellMergeStrategy 注册合并单元格处理器
** * 列值重复合并策略 */ @Slf4j public class CellMergeStrategy extends AbstractMergeStrategy implements WorkbookWriteHandler { private final List<CellRangeAddress> cellList; private final boolean hasTitle; private int rowIndex; public CellMergeStrategy(List<?> list, boolean hasTitle) { this.hasTitle = hasTitle; // 行合并开始下标 this.rowIndex = hasTitle ? 1 : 0; this.cellList = handle(list, hasTitle); } @Override protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) { //单元格写入了,遍历合并区域,如果该Cell在区域内,但非首行,则清空 final int rowIndex = cell.getRowIndex(); if (CollUtil.isNotEmpty(cellList)){ for (CellRangeAddress cellAddresses : cellList) { final int firstRow = cellAddresses.getFirstRow(); if (cellAddresses.isInRange(cell) && rowIndex != firstRow){ cell.setBlank(); } } } } @Override public void afterWorkbookDispose(final WorkbookWriteHandlerContext context) { //当前表格写完后,统一写入 if (CollUtil.isNotEmpty(cellList)){ for (CellRangeAddress item : cellList) { context.getWriteContext().writeSheetHolder().getSheet().addMergedRegion(item); } } } @SneakyThrows private List<CellRangeAddress> handle(List<?> list, boolean hasTitle) { List<CellRangeAddress> cellList = new ArrayList<>(); if (CollUtil.isEmpty(list)) { return cellList; } Field[] fields = ReflectUtils.getFields(list.get(0).getClass(), field -> !"serialVersionUID".equals(field.getName())); // 有注解的字段 List<Field> mergeFields = new ArrayList<>(); List<Integer> mergeFieldsIndex = new ArrayList<>(); for (int i = 0; i < fields.length; i++) { Field field = fields[i]; if (field.isAnnotationPresent(CellMerge.class)) { CellMerge cm = field.getAnnotation(CellMerge.class); mergeFields.add(field); mergeFieldsIndex.add(cm.index() == -1 ? i : cm.index()); if (hasTitle) { ExcelProperty property = field.getAnnotation(ExcelProperty.class); rowIndex = Math.max(rowIndex, property.value().length); } } } Map<Field, RepeatCell> map = new HashMap<>(); // 生成两两合并单元格 for (int i = 0; i < list.size(); i++) { for (int j = 0; j < mergeFields.size(); j++) { Field field = mergeFields.get(j); Object val = ReflectUtils.invokeGetter(list.get(i), field.getName()); int colNum = mergeFieldsIndex.get(j); if (!map.containsKey(field)) { map.put(field, new RepeatCell(val, i)); } else { RepeatCell repeatCell = map.get(field); Object cellValue = repeatCell.getValue(); if (cellValue == null || "".equals(cellValue)) { // 空值跳过不合并 continue; } if (!cellValue.equals(val)) { if ((i - repeatCell.getCurrent() > 1)) { cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum)); } map.put(field, new RepeatCell(val, i)); } else if (i == list.size() - 1) { if (i > repeatCell.getCurrent() && isMerge(list, i, field)) { cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex, colNum, colNum)); } } else if (!isMerge(list, i, field)) { if ((i - repeatCell.getCurrent() > 1)) { cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum)); } map.put(field, new RepeatCell(val, i)); } } } } return cellList; } private boolean isMerge(List<?> list, int i, Field field) { boolean isMerge = true; CellMerge cm = field.getAnnotation(CellMerge.class); final String[] mergeBy = cm.mergeBy(); if (StrUtil.isAllNotBlank(mergeBy)) { //比对当前list(i)和list(i - 1)的各个属性值一一比对 如果全为真 则为真 for (String fieldName : mergeBy) { final Object valCurrent = ReflectUtil.getFieldValue(list.get(i), fieldName); final Object valPre = ReflectUtil.getFieldValue(list.get(i - 1), fieldName); if (!Objects.equals(valPre, valCurrent)) { //依赖字段如有任一不等值,则标记为不可合并 isMerge = false; } } } return isMerge; } @Data @AllArgsConstructor static class RepeatCell { private Object value; private int current; } }
5. ExcelDownHandler 添加下拉框操作
/** * <h1>Excel表格下拉选操作</h1> * 考虑到下拉选过多可能导致Excel打开缓慢的问题,只校验前1000行 * <p> * 即只有前1000行的数据可以用下拉框,超出的自行通过限制数据量的形式,第二次输出 */ @Slf4j public class ExcelDownHandler implements SheetWriteHandler { /** * Excel表格中的列名英文 * 仅为了解析列英文,禁止修改 */ private static final String EXCEL_COLUMN_NAME = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; /** * 单选数据Sheet名 */ private static final String OPTIONS_SHEET_NAME = "options"; /** * 联动选择数据Sheet名的头 */ private static final String LINKED_OPTIONS_SHEET_NAME = "linkedOptions"; /** * 下拉可选项 */ private final List<DropDownOptions> dropDownOptions; /** * 表头 */ private final List<List<String>> heads; /** * 当前单选进度 */ private int currentOptionsColumnIndex; /** * 当前联动选择进度 */ private int currentLinkedOptionsSheetIndex; private final DictService dictService; public ExcelDownHandler(List<DropDownOptions> options, List<List<String>> heads) { dropDownOptions = options; this.heads = heads; currentOptionsColumnIndex = 0; currentLinkedOptionsSheetIndex = 0; dictService = SpringUtils.getBean(DictService.class); } /** * <h2>开始创建下拉数据</h2> * 1.通过解析传入的@ExcelProperty同级是否标注有@DropDown选项 * 如果有且设置了value值,则将其直接置为下拉可选项 * <p> * 2.或者在调用ExcelUtil时指定了可选项,将依据传入的可选项做下拉 * <p> * 3.二者并存,注意调用方式 */ @Override public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) { Sheet sheet = writeSheetHolder.getSheet(); // 开始设置下拉框 HSSFWorkbook DataValidationHelper helper = sheet.getDataValidationHelper(); Workbook workbook = writeWorkbookHolder.getWorkbook(); FieldCache fieldCache = ClassUtils.declaredFields(writeWorkbookHolder.getClazz(), writeWorkbookHolder); for (Map.Entry<Integer, FieldWrapper> entry : fieldCache.getSortedFieldMap().entrySet()) { Integer index = entry.getKey(); FieldWrapper wrapper = entry.getValue(); Field field = wrapper.getField(); // 循环实体中的每个属性 // 可选的下拉值 List<String> options = new ArrayList<>(); if (field.isAnnotationPresent(ExcelDictFormat.class)) { // 如果指定了@ExcelDictFormat,则使用字典的逻辑 ExcelDictFormat format = field.getDeclaredAnnotation(ExcelDictFormat.class); String dictType = format.dictType(); String converterExp = format.readConverterExp(); if (StringUtils.isNotBlank(dictType)) { // 如果传递了字典名,则依据字典建立下拉 Collection<String> values = Optional.ofNullable(dictService.getAllDictByDictType(dictType)) .orElseThrow(() -> new ServiceException(String.format("字典 %s 不存在", dictType))) .values(); options = new ArrayList<>(values); } else if (StringUtils.isNotBlank(converterExp)) { // 如果指定了确切的值,则直接解析确切的值 List<String> strList = StringUtils.splitList(converterExp, format.separator()); options = StreamUtils.toList(strList, s -> StringUtils.split(s, "=")[1]); } } else if (field.isAnnotationPresent(ExcelEnumFormat.class)) { // 否则如果指定了@ExcelEnumFormat,则使用枚举的逻辑 ExcelEnumFormat format = field.getDeclaredAnnotation(ExcelEnumFormat.class); List<Object> values = EnumUtil.getFieldValues(format.enumClass(), format.textField()); options = StreamUtils.toList(values, String::valueOf); } if (ObjectUtil.isNotEmpty(options)) { // 如过自定义表头不为空 if (CollUtil.isNotEmpty(heads)) { // 获取对应的下拉表头的下标 index = IntStream.range(0, heads.size()) .filter(i -> wrapper.getHeads()[0].equals(heads.get(i).get(0))) .findFirst().orElse(0); } // 仅当下拉可选项不为空时执行 if (options.size() > 20) { // 这里限制如果可选项大于20,则使用额外表形式 dropDownWithSheet(helper, workbook, sheet, index, options); } else { // 否则使用固定值形式 dropDownWithSimple(helper, sheet, index, options); } } } if (CollUtil.isEmpty(dropDownOptions)) { return; } dropDownOptions.forEach(everyOptions -> { // 如果传递了下拉框选择器参数 if (!everyOptions.getNextOptions().isEmpty()) { // 当二级选项不为空时,使用额外关联表的形式 dropDownLinkedOptions(helper, workbook, sheet, everyOptions); } else if (everyOptions.getOptions().size() > 10) { // 当一级选项参数个数大于10,使用额外表的形式 dropDownWithSheet(helper, workbook, sheet, everyOptions.getIndex(), everyOptions.getOptions()); } else if (!everyOptions.getOptions().isEmpty()) { // 当一级选项个数不为空,使用默认形式 dropDownWithSimple(helper, sheet, everyOptions.getIndex(), everyOptions.getOptions()); } }); } /** * <h2>简单下拉框</h2> * 直接将可选项拼接为指定列的数据校验值 * * @param celIndex 列index * @param value 下拉选可选值 */ private void dropDownWithSimple(DataValidationHelper helper, Sheet sheet, Integer celIndex, List<String> value) { if (ObjectUtil.isEmpty(value)) { return; } markOptionsToSheet(helper, sheet, celIndex, helper.createExplicitListConstraint(ArrayUtil.toArray(value, String.class))); } /** * <h2>额外表格形式的级联下拉框</h2> * * @param options 额外表格形式存储的下拉可选项 */ private void dropDownLinkedOptions(DataValidationHelper helper, Workbook workbook, Sheet sheet, DropDownOptions options) { String linkedOptionsSheetName = String.format("%s_%d", ExcelDownHandler.LINKED_OPTIONS_SHEET_NAME, currentLinkedOptionsSheetIndex); // 创建联动下拉数据表 Sheet linkedOptionsDataSheet = workbook.createSheet(WorkbookUtil.createSafeSheetName(linkedOptionsSheetName)); // 将下拉表隐藏 workbook.setSheetHidden(workbook.getSheetIndex(linkedOptionsDataSheet), true); // 完善横向的一级选项数据表 List<String> firstOptions = options.getOptions(); Map<String, List<String>> secoundOptionsMap = options.getNextOptions(); // 创建名称管理器 Name name = workbook.createName(); // 设置名称管理器的别名 name.setNameName(linkedOptionsSheetName); // 以横向第一行创建一级下拉拼接引用位置 String firstOptionsFunction = String.format("%s!$%s$1:$%s$1", linkedOptionsSheetName, ExcelDownHandler.getExcelColumnName(0), ExcelDownHandler.getExcelColumnName(firstOptions.size()) ); // 设置名称管理器的引用位置 name.setRefersToFormula(firstOptionsFunction); // 设置数据校验为序列模式,引用的是名称管理器中的别名 markOptionsToSheet(helper, sheet, options.getIndex(), helper.createFormulaListConstraint(linkedOptionsSheetName)); for (int columIndex = 0; columIndex < firstOptions.size(); columIndex++) { // 先提取主表中一级下拉的列名 String firstOptionsColumnName = ExcelDownHandler.getExcelColumnName(columIndex); // 一次循环是每一个一级选项 int finalI = columIndex; // 本次循环的一级选项值 String thisFirstOptionsValue = firstOptions.get(columIndex); // 创建第一行的数据 Optional.ofNullable(linkedOptionsDataSheet.getRow(0)) // 如果不存在则创建第一行 .orElseGet(() -> linkedOptionsDataSheet.createRow(finalI)) // 第一行当前列 .createCell(columIndex) // 设置值为当前一级选项值 .setCellValue(thisFirstOptionsValue); // 第二行开始,设置第二级别选项参数 List<String> secondOptions = secoundOptionsMap.get(thisFirstOptionsValue); if (CollUtil.isEmpty(secondOptions)) { // 必须保证至少有一个关联选项,否则将导致Excel解析错误 secondOptions = Collections.singletonList("暂无_0"); } // 以该一级选项值创建子名称管理器 Name sonName = workbook.createName(); // 设置名称管理器的别名 sonName.setNameName(thisFirstOptionsValue); // 以第二行该列数据拼接引用位置 String sonFunction = String.format("%s!$%s$2:$%s$%d", linkedOptionsSheetName, firstOptionsColumnName, firstOptionsColumnName, secondOptions.size() + 1 ); // 设置名称管理器的引用位置 sonName.setRefersToFormula(sonFunction); // 数据验证为序列模式,引用到每一个主表中的二级选项位置 // 创建子项的名称管理器,只是为了使得Excel可以识别到数据 String mainSheetFirstOptionsColumnName = ExcelDownHandler.getExcelColumnName(options.getIndex()); for (int i = 0; i < 100; i++) { // 以一级选项对应的主体所在位置创建二级下拉 String secondOptionsFunction = String.format("=INDIRECT(%s%d)", mainSheetFirstOptionsColumnName, i + 1); // 二级只能主表每一行的每一列添加二级校验 markLinkedOptionsToSheet(helper, sheet, i, options.getNextIndex(), helper.createFormulaListConstraint(secondOptionsFunction)); } for (int rowIndex = 0; rowIndex < secondOptions.size(); rowIndex++) { // 从第二行开始填充二级选项 int finalRowIndex = rowIndex + 1; int finalColumIndex = columIndex; Row row = Optional.ofNullable(linkedOptionsDataSheet.getRow(finalRowIndex)) // 没有则创建 .orElseGet(() -> linkedOptionsDataSheet.createRow(finalRowIndex)); Optional // 在本级一级选项所在的列 .ofNullable(row.getCell(finalColumIndex)) // 不存在则创建 .orElseGet(() -> row.createCell(finalColumIndex)) // 设置二级选项值 .setCellValue(secondOptions.get(rowIndex)); } } currentLinkedOptionsSheetIndex++; } /** * <h2>额外表格形式的普通下拉框</h2> * 由于下拉框可选值数量过多,为提升Excel打开效率,使用额外表格形式做下拉 * * @param celIndex 下拉选 * @param value 下拉选可选值 */ private void dropDownWithSheet(DataValidationHelper helper, Workbook workbook, Sheet sheet, Integer celIndex, List<String> value) { // 创建下拉数据表 Sheet simpleDataSheet = Optional.ofNullable(workbook.getSheet(WorkbookUtil.createSafeSheetName(ExcelDownHandler.OPTIONS_SHEET_NAME))) .orElseGet(() -> workbook.createSheet(WorkbookUtil.createSafeSheetName(ExcelDownHandler.OPTIONS_SHEET_NAME))); // 将下拉表隐藏 workbook.setSheetHidden(workbook.getSheetIndex(simpleDataSheet), true); // 完善纵向的一级选项数据表 for (int i = 0; i < value.size(); i++) { int finalI = i; // 获取每一选项行,如果没有则创建 Row row = Optional.ofNullable(simpleDataSheet.getRow(i)) .orElseGet(() -> simpleDataSheet.createRow(finalI)); // 获取本级选项对应的选项列,如果没有则创建 Cell cell = Optional.ofNullable(row.getCell(currentOptionsColumnIndex)) .orElseGet(() -> row.createCell(currentOptionsColumnIndex)); // 设置值 cell.setCellValue(value.get(i)); } // 创建名称管理器 Name name = workbook.createName(); // 设置名称管理器的别名 String nameName = String.format("%s_%d", ExcelDownHandler.OPTIONS_SHEET_NAME, celIndex); name.setNameName(nameName); // 以纵向第一列创建一级下拉拼接引用位置 String function = String.format("%s!$%s$1:$%s$%d", ExcelDownHandler.OPTIONS_SHEET_NAME, ExcelDownHandler.getExcelColumnName(currentOptionsColumnIndex), ExcelDownHandler.getExcelColumnName(currentOptionsColumnIndex), value.size()); // 设置名称管理器的引用位置 name.setRefersToFormula(function); // 设置数据校验为序列模式,引用的是名称管理器中的别名 markOptionsToSheet(helper, sheet, celIndex, helper.createFormulaListConstraint(nameName)); currentOptionsColumnIndex++; } /** * 挂载下拉的列,仅限一级选项 */ private void markOptionsToSheet(DataValidationHelper helper, Sheet sheet, Integer celIndex, DataValidationConstraint constraint) { // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列 CellRangeAddressList addressList = new CellRangeAddressList(1, 1000, celIndex, celIndex); markDataValidationToSheet(helper, sheet, constraint, addressList); } /** * 挂载下拉的列,仅限二级选项 */ private void markLinkedOptionsToSheet(DataValidationHelper helper, Sheet sheet, Integer rowIndex, Integer celIndex, DataValidationConstraint constraint) { // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列 CellRangeAddressList addressList = new CellRangeAddressList(rowIndex, rowIndex, celIndex, celIndex); markDataValidationToSheet(helper, sheet, constraint, addressList); } /** * 应用数据校验 */ private void markDataValidationToSheet(DataValidationHelper helper, Sheet sheet, DataValidationConstraint constraint, CellRangeAddressList addressList) { // 数据有效性对象 DataValidation dataValidation = helper.createValidation(constraint, addressList); // 处理Excel兼容性问题 if (dataValidation instanceof XSSFDataValidation) { //数据校验 dataValidation.setSuppressDropDownArrow(true); //错误提示 dataValidation.setErrorStyle(DataValidation.ErrorStyle.STOP); dataValidation.createErrorBox("提示", "此值与单元格定义数据不一致"); dataValidation.setShowErrorBox(true); //选定提示 dataValidation.createPromptBox("填写说明:", "填写内容只能为下拉中数据,其他数据将导致导入失败"); dataValidation.setShowPromptBox(true); sheet.addValidationData(dataValidation); } else { dataValidation.setSuppressDropDownArrow(false); } sheet.addValidationData(dataValidation); } /** * <h2>依据列index获取列名英文</h2> * 依据列index转换为Excel中的列名英文 * <p>例如第1列,index为0,解析出来为A列</p> * 第27列,index为26,解析为AA列 * <p>第28列,index为27,解析为AB列</p> * * @param columnIndex 列index * @return 列index所在得英文名 */ private static String getExcelColumnName(int columnIndex) { // 26一循环的次数 int columnCircleCount = columnIndex / 26; // 26一循环内的位置 int thisCircleColumnIndex = columnIndex % 26; // 26一循环的次数大于0,则视为栏名至少两位 String columnPrefix = columnCircleCount == 0 ? StrUtil.EMPTY : StrUtil.subWithLength(ExcelDownHandler.EXCEL_COLUMN_NAME, columnCircleCount - 1, 1); // 从26一循环内取对应的栏位名 String columnNext = StrUtil.subWithLength(ExcelDownHandler.EXCEL_COLUMN_NAME, thisCircleColumnIndex, 1); // 将二者拼接即为最终的栏位名 return columnPrefix + columnNext; } }
6. ExcelDictFormat 字典格式化注解
/** * 字典格式化 */ @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface ExcelDictFormat { /** * 如果是字典类型,请设置字典的type值 (如: sys_user_sex) */ String dictType() default ""; /** * 读取内容转表达式 (如: 0=男,1=女,2=未知) */ String readConverterExp() default ""; /** * 分隔符,读取字符串组内容 */ String separator() default StringUtils.SEPARATOR; }
7. ExcelDictConvert 字典格式化转换处理 (普通导出方法调用)
/** * 字典格式化转换处理 */ @Slf4j public class ExcelDictConvert implements Converter<Object> { @Override public Class<Object> supportJavaTypeKey() { return Object.class; } @Override public CellDataTypeEnum supportExcelTypeKey() { return null; } @Override public Object convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { ExcelDictFormat anno = getAnnotation(contentProperty.getField()); String type = anno.dictType(); String label = cellData.getStringValue(); String value; if (StringUtils.isBlank(type)) { value = ExcelUtil.reverseByExp(label, anno.readConverterExp(), anno.separator()); } else { value = SpringUtils.getBean(DictService.class).getDictValue(type, label, anno.separator()); } return Convert.convert(contentProperty.getField().getType(), value); } @Override public WriteCellData<String> convertToExcelData(Object object, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { if (ObjectUtil.isNull(object)) { return new WriteCellData<>(""); } ExcelDictFormat anno = getAnnotation(contentProperty.getField()); String type = anno.dictType(); String value = Convert.toStr(object); String label; if (StringUtils.isBlank(type)) { label = ExcelUtil.convertByExp(value, anno.readConverterExp(), anno.separator()); } else { label = SpringUtils.getBean(DictService.class).getDictLabel(type, value, anno.separator()); } return new WriteCellData<>(label); } private ExcelDictFormat getAnnotation(Field field) { return AnnotationUtil.getAnnotation(field, ExcelDictFormat.class); } }
8. 后续修改
1. 添加大标题,自动合并单元格
a. 添加大标题
b. 修改取数据方法
修改取下标为1的元素
/** * convertObjectsToExcelRows 将对象列表转换为 Excel 行数据 * * @param list 对象集合 * @param heads 表头 * @return {@link List<List<Object>>} Excel 行数据 */ private static <T> List<List<Object>> convertObjectsToExcelRows(List<T> list, List<List<String>> heads) { return list.stream() // 将对象转换为 Map .map(ExcelUtil::convertObjectToMap) .map(map -> { List<Object> row = new ArrayList<>(); for (List<String> header : heads) { String key = header.get(1); // 根据表头获取对应的值 row.add(map.get(key)); } return row; }) .toList(); }
c. 导出方法
d. 导出内容
2. 解决 Can not find ‘Converter’ support class LocalTime.
a. 原因
EasyExcel
默认支持一些常见的数据类型,但对于 LocalTime
这样的类型,可能需要自定义一个转换器。
b. 解决方案
- 自定义
LocalTime
转换器 - 直接添加个数据转换 (因为是自定义表头,所以走此方法)
c.直接数据转换(因不同的java版本写法会有不同,我用的java17)
/** * convertToExcelData 数据转换 * * @param value 值 * @return {@link java.lang.Object} */ private static Object convertToExcelData(Object value) { if (ObjectUtil.isEmpty(value)) { return value; } if (value instanceof LocalTime localTime) { return localTime.format(DateTimeFormatter.ofPattern("HH:mm:ss")); } else if (value instanceof LocalDateTime localDateTime) { return localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); } else if (value instanceof LocalDate localDate) { return localDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); } else { return value; } }
d. 修改 convertObjectToMap 方法