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

2023-01-01 00:00:00 java 实战

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

前言

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

原始数据如下所示

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

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

想要达到的效果如下

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

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

一、思路

当我接到这个需求的时候,第一时间就去度娘上找了半天。发现大部分都是使用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;
    }
}

测试代码

测试数据如下图所示:

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

测试实体类如下所示:

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

测试代码如下所示(多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();
    }

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

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

三、总结

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

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

    原文作者:Coding&Sharing
    原文地址: https://blog.csdn.net/weixin_38500202/article/details/118583949
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。

相关文章