本文最后更新于: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(); for (Field field : fields) { Annotation annotation = field.getAnnotation(ExcelProperty.class); if (annotation != null ) { 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 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 @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; } 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 private static FieldCache doDeclaredFields (Class<?> clazz, ConfigurationHolder configurationHolder) { List<Field> tempFieldList = new ArrayList <>(); Class<?> tempClass = clazz; while (tempClass != null ) { Collections.addAll(tempFieldList, tempClass.getDeclaredFields()); tempClass = tempClass.getSuperclass(); } 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 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 ; } }
通过这部分源码我也理解了这个注解的原理。