Java实战-优雅的合并Excel列中的相同内容

Java实战-优雅的合并Excel列中的相同内容最近有一个业务需求是这样的:为了方便展示数据,需要对收集到的数据需要输出为 Excel 文件,并且要按型号及坐标合并列…

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

前言

最近有一个业务需求是这样的:为了方便展示数据,需要对收集到的数据需要输出为 Excel 文件,并且要按型号及坐标字段合并列

原始数据如下所示

tips:此处只是将测试数据放到Excel中展示,实际情况并不是从Excel中读取的。 用Excel是为了更直观的展示列的内容

image.png

想要达到的效果如下

需要达到的效果就是合并 型号坐标 中相同的内容,

image.png

一、思路

当我接到这个需求的时候,第一时间就去度娘上找了半天。发现大部分都是使用POI来操作的,比较繁琐,而且扩展性不高

于是我就想看一下开源工具类 EasyExcel 原生能不能支持这个功能,不出所料也是没有的。所以只能自己撸一个工具类了。

我充分的考虑到了使用的便利性扩展性,此处使用了 自定义注解 + 策略模式 来实现的。大致的思路主要分为以下两个部分:

  1. 使用自定义注解标识需要合并的列
  2. 使用策略模式扫描数据列表,利用 CellRangeAddress 生成合并策略,即告诉 POI 在写入Excel时需要合并的列的起始位置、列的结束位置、行的起始位置以及行的结束位置

因为 是基于 POI 来实现的,所以它是支持在写入时指定合并策略的。

涉及到的一些概念:
POI:Apache开源的Java处理Office文档的工具包
EasyExcel:阿里开源的读写Excel的工具包(基于POI)
WriteHandler:写入策略接口(CellWriteHandler 继承此接口)
CellRangeAddress:单元格合并对象,new 实例时的四个参数分别为列的起始位置、列的结束位置、行的起始位置以及行的结束位置

二、实现

因为考虑到后期的维护,所以对实现部分做细致的划分。

实现代码

自定义注解

合并模式注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface MergeModel {

    /** * 1 means col merge * 2 means row merge */
    int type() default 1;
}

待合并的字段注解

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface CellMerge {
    // default:merger the col with same val
    MergeType type() default MergeType.Col_Repeat_Val;

    // col index
    int index() default -1;
}

上下文对象

/** * 合并上下文 */
public class MergeContext<T> {
    // 具体的策略对象
    private MergeStrategy<T> strategy;
    private Class<?> clazz;

    private MergeContext(MergeStrategy<T> strategy) {
        this.strategy = strategy;
    }

    /** * 外部需走此处 */
    public static class Holder {
        /** * 构建上下文 */
        public static MergeContext build(Class clazz) {
            MergeContext context = null;
            if (clazz.isAnnotationPresent(MergeModel.class)) {
                MergeModel model = (MergeModel) clazz.getAnnotation(MergeModel.class);
                switch (model.type()) {
                    case 1:
                        context = new MergeContext(new ColRepeatStrategy<>());
                        break;
                    case 2:
                        context = new MergeContext(new RowRepeatStrategy());
                        break;
                }
            }
            return context;
        }
    }



    /** * 构建写入处理者 */
    public WriteHandler buildWriteHandler(List<T> data, Class<T> clazz){
        return buildWriteHandler(data, clazz, true);
    }

    /** * 构建写入处理者 */
    public WriteHandler buildWriteHandler(List<T> data, Class<T> clazz, boolean hasTitle){
        List<CellRangeAddress> l = strategy.handle(data, clazz, hasTitle);
        return strategy.build(l);
    }
}

合并策略类

/** * 合并策略 */
public interface MergeStrategy<T> {

    List<CellRangeAddress> handle(List<T> list, Class<T> clazz);

    List<CellRangeAddress> handle(List<T> list, Class<T> clazz, boolean hasTitle);

    WriteHandler build(List<CellRangeAddress> list);
}

列值重复合并策略

扫描数据列表并生成合并单元格对象的逻辑

/** * 列值重复合并策略 */
@Slf4j
public class ColRepeatStrategy<T> implements MergeStrategy<T> {

    @Override
    public List<CellRangeAddress> handle(List list, Class clazz, boolean hasTitle) {
        List<CellRangeAddress> ret = new ArrayList<>();
        Field[] fields = clazz.getDeclaredFields();
        List<Field> mergeFields = new ArrayList<>();   // field with merge annotation
        List<Integer> mergeFieldsIndex = new ArrayList<>(); // col index with merge cell
        for (int i=0; i<fields.length; i++) {
            Field field = fields[i];
            if (field.isAnnotationPresent(CellMerge.class)) {
                CellMerge cm = field.getAnnotation(CellMerge.class);
                if (MergeType.Col_Repeat_Val.getVal() == cm.type().getVal()) {
                    mergeFields.add(field);
                    mergeFieldsIndex.add(cm.index() == -1 ? i : cm.index());
                } else {
                    log.warn("currently only support merge row repeat val");
                }
            }
        }
        // row merger begin index
        int rowIndex = hasTitle ? 1 : 0;
        // dictionary
        Map<Field, RepeatCell> map = new HashMap<>();
        // generate CellRangeAddress 2 merge cells
        for (int i=0; i<list.size(); i++) {
            for (int j=0; j<mergeFields.size(); j++) {
                Field field = mergeFields.get(j);
                Object val = ClassUtil.getProperty(field, list.get(i), clazz);
                int colNum = mergeFieldsIndex.get(j);
                if (!map.containsKey(field)) {
                    // dictionary without the field
                    map.put(field, new RepeatCell(val, i));
                } else {
                    // current val not equal with the val in dictionary
                    if (map.get(field).getVal() != val) {
                        if (i - map.get(field).getCurrent() > 1)
                            ret.add(new CellRangeAddress(map.get(field).getCurrent()+rowIndex, i+rowIndex-1, colNum, colNum));
                        map.put(field, new RepeatCell(val, i));
                    } else if (i == list.size() - 1) {
                        if (i > map.get(field).getCurrent())
                            ret.add(new CellRangeAddress(map.get(field).getCurrent() + rowIndex, i+rowIndex, colNum, colNum));
                    }
                }
            }
        }
        return ret;
    }

    @Override
    public WriteHandler build(List<CellRangeAddress> list) {
        return new AbstractMergeStrategy() {
            // merge cells
            @Override
            protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
                // judge the list is not null
                if (CollectionUtils.isNotEmpty(list)) {
                    // the judge is necessary
                    if (cell.getRowIndex() == 1 && cell.getColumnIndex() == 0) {
                        for (CellRangeAddress item : list) {
                            sheet.addMergedRegion(item);
                        }
                    }
                }
            }
        };
    }

    static class RepeatCell {
        private Object val;
        private int current;
    }
}

测试代码

测试数据如下图所示:

image.png

测试实体类如下所示:

image.png

测试代码如下所示(多sheet工作区):

    /** * 多个Sheet写入 */
    @Test
    public void multiWriteTest() {
        List<PartsData> list = PartsData.buildList();
        String fileName = new Date() + "_multi.xlsx";
        ExcelWriter writer = EasyExcelPlus.buildWriter(fileName, PartsData.class);
        for (int i=0; i<10; i++) {
            WriteSheet sheet = EasyExcelPlus.buildWriterSheet(list, PartsData.class, true, i, "模板"+i);
            writer.write(list, sheet);
        }
        writer.finish();
    }

测试结果与预期相符,如下图所示:

image.png

三、总结

本文代码均可在Github中找到。

感谢看到最后,非常荣幸能够帮助到你~❤❤❤❤

今天的文章Java实战-优雅的合并Excel列中的相同内容分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/23147.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注