java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > springboot EasyExcel数据格式化转换

SpringBoot种如何使用 EasyExcel 实现自定义表头导出并实现数据格式化转换

作者:鲁子狄

本文详细介绍了如何使用EasyExcel工具类实现自定义表头导出,并实现数据格式化转换与添加下拉框操作,通过示例和代码,展示了如何处理不同数据结构和注解,确保数据在导出时能够正确显示和格式化,此外,还介绍了如何解决特定数据类型的转换问题,并提供了解决方案

SpringBoot3 使用 EasyExcel 封装工具类实现 自定义表头 导出并实现 数据格式化转换 与 添加下拉框 操作

在现代企业应用中,数据导出功能是非常常见的需求。特别是在处理大量数据时,将数据导出为 Excel 文件不仅方便用户查看和分析,还能提高数据处理的效率。Apache POI 是一个常用的 Java Excel 处理库,但它在处理大数据量时性能较差。为此,阿里巴巴开源了 EasyExcel,这是一个基于 Java 的简单、优雅的 Excel 操作库,它在处理大数据量时表现优异。

本文将详细介绍如何使用 EasyExcel 实现自定义表头导出,并实现数据格式化转换。

官网 : EasyExcel

封装了很多导出方法针对不同的业务场景,这里直接展示自定义表头的代码 .

1. 使用示例

Vo 示例(部分字段)

使用方法 export

用户自定义表头的 字段显示名称 , 需跟导出注解 @ExcelPropertyvalue 相对应 , 不然无法取到值

页面显示数据

导出 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);
    }

注意

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. 解决方案

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 方法

您可能感兴趣的文章:
阅读全文