书接上篇,上篇只是自己简单做的deme,但是真正到项目里,又出现情况了T-T

直接上测试代码,然后看看效果(根据业务改编的,并不是我司业务)

	public void test(HttpServletResponse response) {
        List<List<String>> headList = new ArrayList<>();
        List<String> head0 = new ArrayList<>();
        head0.add("公司名称");
        headList.add(head0);
        List<String> head3 = new ArrayList<>();
        head3.add("截至当前交易总额");
        head3.add("小计");
        headList.add(head3);
        List<String> head4 = new ArrayList<>();
        head4.add("截至当前交易总额");
        head4.add("一、线上交易额");
        headList.add(head4);
        List<String> head5 = new ArrayList<>();
        head5.add("截至当前交易总额");
        head5.add("二、线下交易额");
        headList.add(head5);
        List<String> head6 = new ArrayList<>();
        head6.add("截至当前交易总额");
        head6.add("三、第三方平台交易额");
        head6.add("小计");
        headList.add(head6);
        List<String> head7 = new ArrayList<>();
        head7.add("截至当前交易总额");
        head7.add("三、第三方平台交易额");
        head7.add("美团");
        headList.add(head7);
        List<String> head8 = new ArrayList<>();
        head8.add("截至当前交易总额");
        head8.add("三、第三方平台交易额");
        head8.add("饿了么");
        headList.add(head8);

        for (int i = 0; i < 3; i++) {
            Date calcMonthTime = DateUtil.offsetMonth(DateUtil.date(), i);
            String calcMonthStr = DateUtil.format(calcMonthTime, DatePattern.NORM_MONTH_PATTERN) + "月交易额";

            List<String> monthHead1 = new ArrayList<>();
            monthHead1.add(calcMonthStr);
            monthHead1.add("小计");
            monthHead1.add("收入");
            headList.add(monthHead1);

            List<String> monthHead2 = new ArrayList<>();
            monthHead2.add(calcMonthStr);
            monthHead2.add("小计");
            monthHead2.add("支出");
            headList.add(monthHead2);

            List<String> monthHead3 = new ArrayList<>();
            monthHead3.add(calcMonthStr);
            monthHead3.add("一、线上交易额");
            monthHead3.add("收入");
            headList.add(monthHead3);

            List<String> monthHead4 = new ArrayList<>();
            monthHead4.add(calcMonthStr);
            monthHead4.add("一、线上交易额");
            monthHead4.add("支出");
            headList.add(monthHead4);

            List<String> monthHead5 = new ArrayList<>();
            monthHead5.add(calcMonthStr);
            monthHead5.add("二、线下交易额");
            monthHead5.add("收入");
            headList.add(monthHead5);

            List<String> monthHead6 = new ArrayList<>();
            monthHead6.add(calcMonthStr);
            monthHead6.add("二、线下交易额");
            monthHead6.add("支出");
            headList.add(monthHead6);

            List<String> monthHead7 = new ArrayList<>();
            monthHead7.add(calcMonthStr);
            monthHead7.add("三、第三方平台交易额");
            monthHead7.add("收入");
            headList.add(monthHead7);

            List<String> monthHead8 = new ArrayList<>();
            monthHead8.add(calcMonthStr);
            monthHead8.add("三、第三方平台交易额");
            monthHead8.add("支出");
            headList.add(monthHead8);
        }

        try {
            // 设置响应头
            response.setContentType("application/vnd.ms-excel");
            response.setCharacterEncoding("utf-8");
            String fileName = "test";
            // 设置防止中文名乱码
            String excelName = URLEncoder.encode(fileName, "utf-8");
            response.setHeader("Content-disposition", "attachment;filename=" + excelName + ".xlsx");


            // 先仅仅写入头,再以不写入头的方式写入数据
            ExcelWriterBuilder writerBuilder = EasyExcel.write(response.getOutputStream()).head(headList);

            writerBuilder.sheet("test")
                    .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                    .doWrite(new ArrayList<>());

        } catch (IOException e) {
            throw new BizException("导出失败");
        }
    }

运行出的结果如下图
在这里插入图片描述
看出来啥问题了吗,虽然可以自适应,但这个列宽也太大了!!!

研究一下官方这个自适应策略的代码

package com.alibaba.excel.write.style.column;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.util.MapUtils;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.poi.ss.usermodel.Cell;

/**
 * Take the width of the longest column as the width.
 * <p>
 * This is not very useful at the moment, for example if you have Numbers it will cause a newline.And the length is not
 * exactly the same as the actual length.
 *
 * @author Jiaju Zhuang
 */
public class LongestMatchColumnWidthStyleStrategy extends AbstractColumnWidthStyleStrategy {

    private static final int MAX_COLUMN_WIDTH = 255;

    private final Map<Integer, Map<Integer, Integer>> cache = MapUtils.newHashMapWithExpectedSize(8);

    @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) {
            return;
        }
        Map<Integer, Integer> maxColumnWidthMap = cache.computeIfAbsent(writeSheetHolder.getSheetNo(), key -> new HashMap<>(16));
        Integer columnWidth = dataLength(cellDataList, cell, isHead);
        if (columnWidth < 0) {
            return;
        }
        if (columnWidth > MAX_COLUMN_WIDTH) {
            columnWidth = MAX_COLUMN_WIDTH;
        }
        Integer maxColumnWidth = 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;
        }
        WriteCellData<?> cellData = cellDataList.get(0);
        CellDataTypeEnum type = cellData.getType();
        if (type == null) {
            return -1;
        }
        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;
        }
    }
}

经过debug,发现他的处理方案其实是取的这一列的最宽的宽度,在合并单元格之前。
可以看这个B列,"小计"非常宽,他其实最终取到的宽度是"小计"的父级标题"截至当前交易总额"的宽度,并不是"小计"的宽度,但其实如果父级标题是一样的,最终的展示效果是合并起来的,子标题总计的长度远远超过父标题了,"小计"不应该这么宽
在这里插入图片描述

将导出文件的标题复制一份,然后取消单元格合并,就发现确实是这样,如下图
在这里插入图片描述
官方的代码也比较简单,很容易就能看懂,简单说明一下
在这里插入图片描述
直接给出我改的代码,新建一个类CustomLongestMatchColumnWidthStyleStrategy

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import cn.hutool.core.util.ObjectUtil;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.util.MapUtils;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;

import com.alibaba.excel.write.style.column.AbstractColumnWidthStyleStrategy;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.poi.ss.usermodel.Cell;

/**
 * 自定义 自适应宽度
 *
 * @author FangCheng
 * @since 2023/08/25 10:19
 **/
public class CustomLongestMatchColumnWidthStyleStrategy extends AbstractColumnWidthStyleStrategy {

    private static final int MAX_COLUMN_WIDTH = 255;

    /**
     * 官方给的这个cache存储的是:第几个sheet,第几列,最大宽度
     */
    private final Map<Integer, Map<Integer, Integer>> cache = MapUtils.newHashMapWithExpectedSize(8);

    /**
     * 存储表头宽度 第几个sheet,第几行,第几列,真实宽度
     */
    private final Map<Integer, Map<Integer, Map<Integer, Integer>>> widthCache = MapUtils.newHashMapWithExpectedSize(8);
    /**
     * 存储表头key 第几个sheet,第几行,第几列,key
     */
    private final Map<Integer, Map<Integer, Map<Integer, String>>> headerCache = MapUtils.newHashMapWithExpectedSize(8);

    @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) {
            return;
        }
        Map<Integer, Integer> maxColumnWidthMap = cache.computeIfAbsent(writeSheetHolder.getSheetNo(), key -> new HashMap<>(16));
        // 取当前sheet页数据
        Map<Integer, Map<Integer, Integer>> currentSheetRowColumnWidthMap = widthCache.computeIfAbsent(writeSheetHolder.getSheetNo(), key -> new HashMap<>(16));
        Map<Integer, Map<Integer, String>> currentSheetRowColumnKeyMap =
                headerCache.computeIfAbsent(writeSheetHolder.getSheetNo(), key -> new HashMap<>(16));
        // 取当前行数据
        Map<Integer, Integer> currentRowColumnWidthMap = currentSheetRowColumnWidthMap.computeIfAbsent(cell.getRowIndex(), key -> new HashMap<>(16));
        Map<Integer, String> currentRowColumnKeyMap = currentSheetRowColumnKeyMap.computeIfAbsent(cell.getRowIndex(), key -> new HashMap<>(16));

        Integer columnWidth = dataLength(cellDataList, cell, isHead);
        if (columnWidth < 0) {
            return;
        }
        if (columnWidth > MAX_COLUMN_WIDTH) {
            columnWidth = MAX_COLUMN_WIDTH;
        }

        if (isHead) {
            // 表头数据,才存入
            currentRowColumnWidthMap.put(cell.getColumnIndex(), columnWidth);
            currentRowColumnKeyMap.put(cell.getColumnIndex(), cell.getStringCellValue());
        }

        Integer maxColumnWidth = maxColumnWidthMap.get(cell.getColumnIndex());
        if (maxColumnWidth == null || columnWidth > maxColumnWidth) {
            maxColumnWidthMap.put(cell.getColumnIndex(), columnWidth);
            writeSheetHolder.getSheet().setColumnWidth(cell.getColumnIndex(), columnWidth * 256);
        }

        // 是表头,且存在多级表头(当前行号不为0),才处理,进行宽度重写
        if (isHead && cell.getRowIndex() > 0) {
            // 取前一行的数据信息
            Map<Integer, String> previousRowColumnKeyMap = currentSheetRowColumnKeyMap.get(cell.getRowIndex() - 1);
            Map<Integer, Integer> previousRowColumnWidthMap = currentSheetRowColumnWidthMap.get(cell.getRowIndex() - 1);
            // 是最后一列才处理。依据上一行表头的size判断
            if (previousRowColumnKeyMap.size() - 1 == cell.getColumnIndex()) {
                List<List<Integer>> sameParentColumnIndices = getSameParentColumnIndex(previousRowColumnKeyMap);
                for (List<Integer> sameParentColumnIndex : sameParentColumnIndices) {
                    Integer firstIndex = sameParentColumnIndex.get(0);
                    // 父级的最小长度,即单个父级的长度
                    Integer parentColumnMinWidth = previousRowColumnWidthMap.get(firstIndex);
                    // 父级的最大长度,即N个父级的长度
                    int parentColumnMaxWidth = previousRowColumnWidthMap.get(firstIndex) * sameParentColumnIndex.size();
                    Integer childrenColumnsWidth = 0;
                    for (Integer index : sameParentColumnIndex) {
                        childrenColumnsWidth += currentRowColumnWidthMap.get(index);
                    }
                    /*
                     *一共三种情况:
                     * 1、子表头的总长比 父表头单个都要小
                     * 2、子表头的总长比 父表头单个要大,但是比父表头N个要小
                     * 3、子表头的总长比 父表头N个都要大
                     */
                    if (parentColumnMinWidth < childrenColumnsWidth && parentColumnMaxWidth > childrenColumnsWidth) {
                        // 2、子表头的总长比 父表头单个要大,但是比父表头N个要小,将表头处理成应该有的宽度
                        for (Integer index : sameParentColumnIndex) {
                            writeSheetHolder.getSheet().setColumnWidth(index, currentRowColumnWidthMap.get(index) * 256);
                        }
                    } else if (parentColumnMinWidth > childrenColumnsWidth) {
                        for (Integer index : sameParentColumnIndex) {
                            // 、子表头的总长比 父表头单个都要小,要根据比例重新计算宽度,如果算的宽度比正常还要小,那么就不修改
                            int newWidth = parentColumnMinWidth * currentRowColumnWidthMap.get(index) / childrenColumnsWidth;
                            if (newWidth > currentRowColumnWidthMap.get(index)) {
                                writeSheetHolder.getSheet().setColumnWidth(index, newWidth * 256);
                                currentRowColumnWidthMap.put(index, newWidth);
                            }
                        }
                    } else if (childrenColumnsWidth > parentColumnMaxWidth) {
                        // 3、子表头的总长比 父表头N个都要大,将表头处理成应该有的宽度
                        for (Integer index : sameParentColumnIndex) {
                            writeSheetHolder.getSheet().setColumnWidth(index, currentRowColumnWidthMap.get(index) * 256);
                        }
                    }
                }
            }
        }
    }

    /**
     * 取上一级表头,连续相等的key,所在的列
     *
     * @author FangCheng
     * @since 2023/08/25 11:28
     * @param previousRowColumnKeyMap previousRowColumnKeyMap
     * @return java.util.List<java.util.List < java.lang.Integer>>
     */
    private List<List<Integer>> getSameParentColumnIndex(Map<Integer, String> previousRowColumnKeyMap) {
        List<List<Integer>> result = new ArrayList<>();
        int calcIndex = 0;
        for (Integer columnIndex : previousRowColumnKeyMap.keySet()) {
            if (calcIndex >= columnIndex) {
                continue;
            }
            String headerValue = previousRowColumnKeyMap.get(columnIndex);
            List<Integer> sameIndex = new ArrayList<>();
            sameIndex.add(columnIndex);
            for (Integer nextColumnIndex : previousRowColumnKeyMap.keySet()) {
                // hashMap的key是int自增的,所以可以这样循环处理
                if (columnIndex >= nextColumnIndex) {
                    continue;
                }
                String nextHeaderValue = previousRowColumnKeyMap.get(nextColumnIndex);
                if (ObjectUtil.equals(headerValue, nextHeaderValue)) {
                    sameIndex.add(nextColumnIndex);
                    calcIndex = nextColumnIndex;
                } else {
                    // 如果下一个不相等,应直接跳出此循环
                    break;
                }
            }
            if (sameIndex.size() > 1) {
                result.add(sameIndex);
            }
        }
        return result;
    }

    private Integer dataLength(List<WriteCellData<?>> cellDataList, Cell cell, Boolean isHead) {
        if (isHead) {
            return cell.getStringCellValue().getBytes().length;
        }
        WriteCellData<?> cellData = cellDataList.get(0);
        CellDataTypeEnum type = cellData.getType();
        if (type == null) {
            return -1;
        }
        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;
        }
    }
}

注释写的还比较详细吧,简单说一下思路
1、模仿官方的写法增加了两个map,存储真实的宽度及key值(单元格里的内容)。宽度map用来改写单元格宽度,key值map用来判断哪些单元格需要合并(问题点其实就在这里,没有处理合并的单元格宽度)
2、判断是表头才写入这两个map
3、判断是表头,且存在多级表头(当前行号不为0),才处理,进行宽度重写
4、当前单元格为最后一个单元格才进行改写,一行的宽度一起进行改写
5、分别处理三种场景,writeSheetHolder.getSheet().setColumnWidth改写单元格宽度,并修改宽度map(下一级表头需要使用修改后的数据才行)

  • 1、子表头的总长比 父表头单个都要小
    在这里插入图片描述
    即这种情况,子表头总共才7个汉字,而父表头有10个汉字,按照默认的自适应策略,每个单元格都将是10个汉字的宽度。需要处理成:要根据比例重新计算宽度,如果算的宽度比正常还要小,那么就不修改。

  • 2、子表头的总长比 父表头单个要大,但是比父表头N个要小
    在这里插入图片描述 即这种情况,二级标题26个汉字,比父标题8个汉字多,又比8*4=32个汉字少。需要处理成:直接取子表头的宽度。

  • 3、子表头的总长比 父表头N个都要大
    这种情况也很好理解:直接取子表头的宽度。

修改导出时使用的策略

writerBuilder.sheet("test")
                    .registerWriteHandler(new CustomLongestMatchColumnWidthStyleStrategy())
                    .doWrite(new ArrayList<>());

看效果吧
在这里插入图片描述
是不是比之前舒服很多呢

GitHub 加速计划 / ea / easyexcel
31.64 K
7.47 K
下载
快速、简洁、解决大文件内存溢出的java处理Excel工具
最近提交(Master分支:3 个月前 )
c42183df Bugfix 3 个月前
efa7dff6 * 重新加回 `commons-io` 3 个月前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐