EasyExcel 动态导出

本文最后更新于:2024年8月22日 晚上

最近公司有一个系统需要做到所有模块的动态导出,模块有十几个,如果每个模块都按照之前方法写,那么会做很多重复工作,这次进行优化。之前的动态导出设计可以看这篇文章

一、实现步骤

EasyExcel 在导出的时候提供方法来排除,或是只允许导出某些字段。

  • includeColumnFiledNames :参数类型为 List<String> ,传递的是导出类的仅导出的字段名字列表
  • excludeColumnFiledNames :参数类型为 List<String> ,传递的是导出类需要排除导出的字段列表
  • includeColumnIndexs :参数类型为 List<Integer>,传递的是导出类的字段仅导出的索引 ID 列表
  • excludeColumnIndexs :参数类型为 List<Integer>,传递的是导出类需要排除的索引 ID 列表

有了这几个方法就可以很方便的进行动态导出,对于索引和字段名的选择,我这里使用字段名做动态导出,并且配合 includeColumnFiledNames 来做处理。
对于数据库动态查询,我没有做动态的查询,因为涉及到字段的映射转换,比较麻烦,所以直接默认查询全部,动态的仅体现在导出方便,性能上会有一点损耗,目前公司数据不是很多,所以可以忽略不计。
下面实现一个通用工具方法,使用反射的方式提取导出类中含有 @ExcelProperty 的字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static List<String> getExcelPropertyFields(Class<?> clazz) {
List<String> excelFields = new ArrayList<>();

// 获取类的所有字段
Field[] fields = clazz.getDeclaredFields();

// 遍历每个字段,检查是否有 @ExcelProperty 注解
for (Field field : fields) {
Annotation annotation = field.getAnnotation(ExcelProperty.class);
if (annotation != null) {
// 如果有 @ExcelProperty 注解,则获取字段名字
excelFields.add(field.getName());
}
}
return excelFields;
}

提取到字段的下一步,也封装了一个通用的写 Excel 方法,如果传入了 selectFields 也就是指定字段,那么使用指定字段导出,否则,通过反射的方式获取导出类的所有带 @ExcelProperty 的字段集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 通用动态字段导出
* @param entity 标注 @ExcelProperty 的实体类
* @param fileName 文件名
* @param resultList 数据库查询结果列表
* @param selectFields 指定导出字段集合
* @param <T>
*/
public static <T> void commonExport(T entity, String fileName, List<T> resultList, List<String> selectFields) {
List<String> defaultFields;
if (CollectionUtils.isEmpty(selectFields)) {
defaultFields = getExcelPropertyFields(entity.getClass());
} else {
defaultFields = selectFields;
}

EasyExcelFactory.write(RuoYiConfig.getDownloadPath() + "/" + fileName,
entity.getClass()).sheet("结果")
.includeColumnFiledNames(defaultFields)
.doWrite(resultList);
}

然后处理映射关系,怎么让前端知道传哪些字段,下面提供了一个接口,通过不同的模块名,使用反射获取不同的导出类里的导出字段集合,返回给前端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
@RequestMapping("/excel")
@Api(tags = "excel字段控制接口")
public class ExcelSelectController {

public static Map<String, List<String>> excelMap = Maps.newHashMap();

@ApiOperation("根据模块名选择字段")
@GetMapping("/select")
public R<List<String>> getSelectFields(String moduleName) {
return R.ok(excelMap.get(moduleName));
}

@PostConstruct
public static void initExcelMap() {
excelMap.put(ModulesEnum.User.getKey(), CommonUtils.getExcelPropertyFields(User.class));
}
}

在对接的时候发现前端表格头用的是中文名做动态展示,为了方便兼容,下面实现了一个自定义注解进行中文名称和字段名的转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* 转换注解,标识于导出类中携带 @ExcelProperty 注解的字段
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DynamicHead {
String value();
}

public class User{
@ExcelProperty("姓名")
@DynamicHead("姓名")
private String name;
}


/**
* 把转换的中文名映射成字段名
*/
public static <T> List<String> getDynamicHeadMapping(Class<T> clazz, List<String> selectFields) {
List<String> fieldNames = new ArrayList<>();
Field[] fields = clazz.getDeclaredFields();
if (CollectionUtils.isEmpty(selectFields)) {
return fieldNames;
}

// 遍历每个字段,检查是否有 @DynamicHead 注解
for (Field field : fields) {
DynamicHead annotation = field.getAnnotation(DynamicHead.class);
if (annotation != null) {
if (selectFields.contains(annotation.value())) {
fieldNames.add(field.getName());
}
}
}
return fieldNames;
}

通过这样的方式,就可以自由的控制导出的字段。

二、@ExcelIgnore@ExcelIgnoreUnannotated

1、使用

在开发上面的功能的过程中,发现导出的 Excel 表格里会莫名奇妙多出同样的字段,而且是字段名就是导出类的字段名,不是使用的字段上面 @ExcelProperty 注解里的中文名。
下面是我的导出类,并且导出类继承了一个通用的基类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Data
public class BaseEntity implements Serializable{

private Date createTime;

private Date updateTime;
}


@Data
public class User extends BaseEntity {
@ExcelProperty("昵称")
private String nickeName;

@ExcelProperty("用户名")
private Integer username;

@ExcelProperty("状态")
private Integer status;

@ExcelProperty("创建时间")
private Date createTime;

private Date updateTime;
}

然后发现默认情况下,EasyExcel 会写入 @ExcelProperty 字段之外,会写入同样的以字段名为表头的数据,包括父类。这个时候需要加 @ExcelIgnore 来进行排除字段,或者使用 @ExcelIgnoreUnannotated

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Data
@ExcelIgnoreUnannotated
public class User extends BaseEntity {
@ExcelProperty("昵称")
private String nickeName;

@ExcelProperty("用户名")
private Integer username;

@ExcelProperty("状态")
private Integer status;

@ExcelProperty("创建时间")
private Date createTime;

private Date updateTime;
}

2、部分源码浅析

通过 debug 的方式,找到了源码位置。
tempClass 就是导出类的类定义,通过循环的方式确定有多少个导出字段,这里能看出的是除了自身类,也会获取自身类的父类字典,有多少个父类就会一直往上搜寻。
然后是判断类是否有 @ExcelIgnoreUnannotated 注解,需要进入另一个方法查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ClassUtils.java

/**
* clazz 就是导出类的类定义
*/
private static FieldCache doDeclaredFields(Class<?> clazz, ConfigurationHolder configurationHolder) {
List<Field> tempFieldList = new ArrayList<>();
Class<?> tempClass = clazz;
// When the parent class is null, it indicates that the parent class (Object class) has reached the top
// level.
while (tempClass != null) {
Collections.addAll(tempFieldList, tempClass.getDeclaredFields());
// Get the parent class and give it to yourself
tempClass = tempClass.getSuperclass();
}
// Screening of field
Map<Integer, List<FieldWrapper>> orderFieldMap = new TreeMap<>();
Map<Integer, FieldWrapper> indexFieldMap = new TreeMap<>();
Set<String> ignoreSet = new HashSet<>();

ExcelIgnoreUnannotated excelIgnoreUnannotated = clazz.getAnnotation(ExcelIgnoreUnannotated.class);
for (Field field : tempFieldList) {
declaredOneField(field, orderFieldMap, indexFieldMap, ignoreSet, excelIgnoreUnannotated);
}
Map<Integer, FieldWrapper> sortedFieldMap = buildSortedAllFieldMap(orderFieldMap, indexFieldMap);
FieldCache fieldCache = new FieldCache(sortedFieldMap, indexFieldMap);
// ......
}

下面是 declaredOneField 方法部分代码。
这里会判断字段是否存在 @Excelgnore 注解,存在则加入忽略字段集合,但是我的问题不在这里。
然后是下面的一个三元表达式,noExcelProperty 的赋值,就是判断当前字段没有使用 @ExcelProperty 的同时并且类使用了 @ExcelIgnoreUnannotated 的时候,就会把这个字段也加进忽略字段集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ClassUtils.java

private static void declaredOneField(Field field, Map<Integer, List<FieldWrapper>> orderFieldMap,
Map<Integer, FieldWrapper> indexFieldMap, Set<String> ignoreSet,
ExcelIgnoreUnannotated excelIgnoreUnannotated) {
String fieldName = FieldUtils.resolveCglibFieldName(field);
FieldWrapper fieldWrapper = new FieldWrapper();
fieldWrapper.setField(field);
fieldWrapper.setFieldName(fieldName);

ExcelIgnore excelIgnore = field.getAnnotation(ExcelIgnore.class);

if (excelIgnore != null) {
ignoreSet.add(fieldName);
return;
}
ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
boolean noExcelProperty = excelProperty == null && excelIgnoreUnannotated != null;
if (noExcelProperty) {
ignoreSet.add(fieldName);
return;
}
// .....
}

通过这部分源码我也理解了这个注解的原理。


EasyExcel 动态导出
http://aim467.github.io/2024/08/22/EasyExcel-动态导出-二/
作者
Dedsec2z
发布于
2024年8月22日
更新于
2024年8月22日
许可协议