自定义注解完善easyPoi的动态表头生成
基本思路
easyPoi的@Excel注解目前只支持静态表头(列名).
我这两天遇到的一个场景是要基于一个属性,生成很多列名。easyPoi给出的方案是让我们自己去创建 ExcelExportEntity
这个对象,再定义一组数据 List<Map>
, 然后调用它提供的下面这个方法来导出。
package cn.afterturn.easypoi.excel;
public final class ExcelExportUtil {
/**
* 根据Map创建对应的Excel
*
* @param entity 表格标题属性
* @param entityList Map对象列表
* @param dataSet Excel对象数据List
*/
public static Workbook exportExcel(ExportParams entity, List<ExcelExportEntity> entityList,
Collection<?> dataSet) {
Workbook workbook = getWorkbook(entity.getType(), dataSet.size());
;
new ExcelExportService().createSheetForMap(workbook, entity, entityList, dataSet);
return workbook;
}
}
不过在创建ExcelExportEntity
对象的过程又臭又长,很容易造就屎山。那么核心要解决的问题就是让ExcelExportEntity
对象创建的过程自动化,用户无需感知到这个过程最好。
此外,从上面的方法中看到,我们还需要一个Collection<?> dataSet
参数,这个参数的构建过程也是又臭又长的,因此也要考虑自动化。
为了迎合easyPoi使用者的习惯,就写个类似的注解来实现这项功能,话不多说,直接上代码:
自定义注解
@CustomExcel
package com.ssy.file.annotation;
import java.lang.annotation.*;
/**
* Author: SiYang
* Date: 2023/09/26 10:19
* Description:
* easyPoi并没有给出动态表头的注解方案,
* 想要使用动态表头,只能自己构建 ExcelExportEntity 这个对象,比较繁琐
* 我们可以使用自定义注解的方式,使得构建该对象的过程自动化
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomExcel {
/**
* 指定列名,有些列名是固定的,不需要生成
*/
String name() default "";
/**
* 指定生成策略,生成策略指的是数据类内部自定义的方法
* 被CustomExcel注解的属性可以定义多种生成策略
* 该方法用来指定生成策略
*/
String columnNameGenerator() default "";
/**
* 列的序号
*/
int orderNum() default 0;
/**
* 列序号生成器,需要自定义生成方法,这里的值为方法名称
* 注:顺序的体现只需要orderNum有大小关系,而不一定要连续
*/
String orderGenerator() default "";
/**
* 是否需要合并列
* 该字段映射到easyPoi的ExcelExportEntity对象中似乎无效
* easyPoi的合并功能还是一如既往地令人失望,所以合并还是得靠自己实现
*/
boolean needMerge() default false;
/**
* 标题分组
*/
String groupName() default "";
/**
* 指定生成title的方法
* 该方法如果返回的值相同,那么就表示这些列的标题需要合并
*/
String groupGenerator() default "";
int width() default 10;
int height() default 10;
}
@CustomExcelCollection
package com.ssy.file.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Author: SiYang
* Date: 2023/09/26 14:49
* Description: 处理内嵌list的注解
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomExcelCollection {
}
使用注解的工具类
用来生成 List<ExcelExportEntity> entityList
以及 Collection<?> dataSet
简单思路就是利用反射拿到注解,利用注解给到的信息执行一系列操作
package com.ssy.file.utils;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import com.ssy.file.annotation.CustomExcel;
import com.ssy.file.annotation.CustomExcelCollection;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;
/**
* Author: SiYang
* Date: 2023/09/26 10:40
* Description:
* 对于导出行为: 生成 ExcelExportEntity 对象
* 对于导入行为: 以后再说
*/
@Slf4j
@SuppressWarnings("unused")
public class ExcelEntityGenerator {
/**
* 基于数据列表生成 easyPoi 中的 ExcelExportEntity 对象列表
*
* @param dataList 数据列表: 想被导出的字段需要使用 @CustomExcel 注解
* @return ExcelExportEntity 对象列表
*/
public static List<ExcelExportEntity> generateExportEntity(List<Object> dataList) {
try {
return generateExportEntity(dataList, null, null);
} catch (Exception e) {
log.error("生成动态表头错误: ", e);
throw new RuntimeException("生成动态表头错误: " + e.getMessage());
}
}
@SuppressWarnings("rawtypes")
private static List<ExcelExportEntity> generateExportEntity(
List<Object> dataList, List<ExcelExportEntity> result, Set<String> colNameSet)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
colNameSet = CollectionUtils.isEmpty(colNameSet) ? new HashSet<>() : colNameSet;
result = CollectionUtils.isEmpty(result) ? new ArrayList<>() : result;
if (CollectionUtils.isEmpty(dataList)) {
return Collections.emptyList();
}
Class<?> clazz = dataList.get(0).getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
CustomExcel customExcel = field.getAnnotation(CustomExcel.class);
CustomExcelCollection customExcelCollection =
field.getAnnotation(CustomExcelCollection.class);
if (Objects.nonNull(customExcel)) {
// 第一优先
String methodName = customExcel.columnNameGenerator();
// 第二优先
String colName = customExcel.name();
ExcelExportEntityBuilder builder = new ExcelExportEntityBuilder();
if (StringUtils.isNotBlank(methodName)) {
Method method = clazz.getDeclaredMethod(methodName);
method.setAccessible(true);
for (Object data : dataList) {
String dynamicColName = (String) method.invoke(data);
if (colNameSet.contains(dynamicColName)
|| StringUtils.isBlank(dynamicColName)) {
continue;
}
ExcelExportEntity entity =
builder.name(dynamicColName)
.key(dynamicColName)
.width(customExcel.width())
.height(customExcel.height())
.needMerge(customExcel.needMerge())
.orderNum(getOrderNum(clazz, data, customExcel))
.groupName(getGroupName(clazz, data, customExcel))
.build();
result.add(entity);
colNameSet.add(dynamicColName);
}
} else {
if (colNameSet.contains(colName) || StringUtils.isBlank(colName)) {
continue;
}
ExcelExportEntity entity =
builder.name(colName)
.key(field.getName())
.width(customExcel.width())
.height(customExcel.height())
.needMerge(customExcel.needMerge())
.orderNum(customExcel.orderNum())
.groupName(customExcel.groupName())
.build();
result.add(entity);
colNameSet.add(colName);
}
} else if (Objects.nonNull(customExcelCollection)) {
for (Object data : dataList) {
generateExportEntity(
new ArrayList<Object>((List) field.get(data)), result, colNameSet);
}
}
}
return result;
}
/**
* 将被 @CustomExcel 注解的属性值提取到map
*
* @param dataList 数据列表
* @return 键值对列表
*/
public static List<Map<String, Object>> generateDataCollection(List<Object> dataList) {
try {
return generateDataCollection(dataList, null, null, true);
} catch (Exception e) {
log.error("生成excel数据错误: ", e);
throw new RuntimeException("生成excel数据错误: " + e.getMessage());
}
}
@SuppressWarnings(value = {"rawtypes", "unchecked"})
private static List<Map<String, Object>> generateDataCollection(
List<Object> dataList,
List<Map<String, Object>> result,
Map<String, Object> dataMap,
boolean isRoot)
throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
result = CollectionUtils.isEmpty(result) ? new ArrayList<>() : result;
if (CollectionUtils.isEmpty(dataList)) {
return Collections.emptyList();
}
Class<?> clazz = dataList.get(0).getClass();
Field[] fields = clazz.getDeclaredFields();
for (Object data : dataList) {
if (isRoot) {
dataMap = new LinkedHashMap<>();
result.add(dataMap);
}
for (Field field : fields) {
field.setAccessible(true);
CustomExcel customExcel = field.getAnnotation(CustomExcel.class);
CustomExcelCollection customExcelCollection =
field.getAnnotation(CustomExcelCollection.class);
if (Objects.nonNull(customExcel)) {
Object val = field.get(data);
// 第一优先
String methodName = customExcel.columnNameGenerator();
// 第二优先
String colName = customExcel.name();
if (StringUtils.isNotBlank(methodName)) {
Method method = clazz.getDeclaredMethod(methodName);
method.setAccessible(true);
String dynamicColName = (String) method.invoke(data);
if (StringUtils.isBlank(dynamicColName)) {
continue;
}
dataMap.put(dynamicColName, val);
} else if (StringUtils.isNotBlank(colName)) {
dataMap.put(field.getName(), val);
}
} else if (Objects.nonNull(customExcelCollection)) {
generateDataCollection(
new ArrayList<>((List) field.get(data)), result, dataMap, false);
}
}
}
return result;
}
/**
* 优先按指定的序号生成器来获取,如果没有指定生成器则使用orderNum
*
* @param clazz 数据类字节码对象
* @param obj 当前处理的对象
* @param customExcel 注解对象
* @return 序号
*/
private static int getOrderNum(Class<?> clazz, Object obj, CustomExcel customExcel)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
String methodName = customExcel.orderGenerator();
if (StringUtils.isNotBlank(methodName)) {
Method method = clazz.getDeclaredMethod(methodName);
method.setAccessible(true);
return (Integer) method.invoke(obj);
} else {
return customExcel.orderNum();
}
}
/**
* 优先按照group生成策略生成groupName
*
* @param clazz 数据类字节码对象
* @param obj 当前处理的对象
* @param customExcel 注解对象
* @return groupName
*/
private static String getGroupName(Class<?> clazz, Object obj, CustomExcel customExcel)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
String methodName = customExcel.groupGenerator();
if (StringUtils.isNotBlank(methodName)) {
Method method = clazz.getDeclaredMethod(methodName);
method.setAccessible(true);
return (String) method.invoke(obj);
} else {
return customExcel.groupName();
}
}
@NoArgsConstructor
private static class ExcelExportEntityBuilder {
private Object key;
private String name;
private int width = 10;
private int height = 10;
private int orderNum = 0;
private boolean needMerge;
private String groupName;
public ExcelExportEntityBuilder key(Object key){
this.key = key;
return this;
}
public ExcelExportEntityBuilder name(String name){
this.name = name;
return this;
}
public ExcelExportEntityBuilder width(int width) {
if (width > 0) {
this.width = width;
}
return this;
}
public ExcelExportEntityBuilder height(int height){
if (height > 0) {
this.height = height;
}
return this;
}
public ExcelExportEntityBuilder orderNum(int orderNum){
this.orderNum = orderNum;
return this;
}
public ExcelExportEntityBuilder needMerge(boolean needMerge){
this.needMerge = needMerge;
return this;
}
public ExcelExportEntityBuilder groupName(String groupName){
this.groupName = groupName;
return this;
}
public ExcelExportEntity build() {
ExcelExportEntity entity = new ExcelExportEntity(name, key, width);
entity.setHeight(height);
entity.setOrderNum(orderNum);
entity.setNeedMerge(needMerge);
entity.setGroupName(groupName);
return entity;
}
}
}
导出代码示例
其中的convert方法,以及额外计算合并区域的方法细节就不给出了。
// 报表原始数据
List<CarbonThingEnergyDictDTO> originalDataList = energyPriceReport(request);
// 数据类型转换
List<CarbonThingEnergyDictExcel> excelDataList = CarbonThingEnergyDictExcel.convertFromDto(originalDataList, request.getTimeType());
// 生成表头 & 生成数据集合
List<ExcelExportEntity> excelExportEntities = ExcelEntityGenerator.generateExportEntity(new ArrayList<>(excelDataList));
List<Map<String, Object>> dataCollection = ExcelEntityGenerator.generateDataCollection(new ArrayList<>(excelDataList));
// 计算合并区域
List<CellRangeAddress> regions = CarbonThingEnergyDictExcel.calculateMergeRegion(excelDataList, request.getTimeType());
Workbook workbook = ExcelExportUtil.exportExcel(new ExportParams(null, "报表"), excelExportEntities, dataCollection);
regions.forEach(workbook.getSheetAt(0)::addMergedRegion);
// 导出
ExcelUtils.downLoadExcel("报表.xls", response, workbook);