本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!
前言
最近有一个业务需求是这样的:为了方便展示数据,需要对收集到的数据需要输出为 Excel
文件,并且要按型号及坐标字段合并列。
原始数据如下所示:
tips:此处只是将测试数据放到Excel中展示,实际情况并不是从Excel中读取的。 用Excel是为了更直观的展示列的内容
想要达到的效果如下
:
需要达到的效果就是合并 型号
和 坐标
中相同的内容,
一、思路
当我接到这个需求的时候,第一时间就去度娘上找了半天。发现大部分都是使用POI来操作的,比较繁琐,而且扩展性不高。
于是我就想看一下开源工具类 EasyExcel
原生能不能支持这个功能,不出所料也是没有的。所以只能自己撸一个工具类了。
我充分的考虑到了使用的便利性和扩展性,此处使用了 自定义注解
+ 策略模式
来实现的。大致的思路主要分为以下两个部分:
- 使用自定义注解标识需要合并的列
- 使用策略模式扫描数据列表,利用
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;
}
}
测试代码
测试数据如下图所示:
测试实体类如下所示:
测试代码如下所示(多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();
}
测试结果与预期相符,如下图所示:
三、总结
本文代码均可在Github中找到。
感谢看到最后,非常荣幸能够帮助到你~❤❤❤❤
今天的文章Java实战-优雅的合并Excel列中的相同内容分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/23147.html