EasyExcel虽然对excel导出做了很好的封装,但是对于动态表头导出做的却不是很好,今天接到一个需求,记录一下思路。
需要根据传入的字段,动态导出数据,且这个顺序是无序的,传入的字段范围是固定的。
当时乍一看这个需求,感觉还好,除了要排序麻烦点,其他都还好。粗想一下已经有了思路:
构造一个映射类,然后在写的时候利用includeColumnFiledNames这个API去限定导出的字段即可。但是这样有个问题,就是传入的字段顺序不是无序的,是根据你类里面构造的默认顺序来的。 这样动态调整顺序不能满足,那有其他办法么?我第一想法是在index这个注解上做文章,每次在去写excel的时候,对映射类字段的index去动态排序,这里用index而不是order是官方说的优先级是index>order,感觉应该可行,想到就写吧。
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IntrospectionException {
List<String> fieldList = new ArrayList<>();
fieldList.add("girlFriend");
fieldList.add("hobby");
fieldList.add("name");
for (int i = 0; i < headList.size(); i++) {
ExcelProperty ano = Domain.class.getDeclaredField(fieldList.get(i)).getDeclaredAnnotation(ExcelProperty.class);
InvocationHandler h = Proxy.getInvocationHandler(ano);
Field f = h.getClass().getDeclaredField("memberValues");
f.setAccessible(true);
Map memberValues = (Map) f.get(h);
memberValues.put("index", i);
}
EasyExcel.write("test.xlsx",Domain.class)
.sheet("test")
.includeColumnFiledNames(fieldList)
.doWrite(init());
}
private static List<Domain> init() {
List<Domain> domainList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Domain domain = new Domain();
domain.setName("li"+i);
domain.setSex("男");
domain.setAge(20 + i);
domain.setHobby("看书");
domain.setGirlFriend("小可爱");
domainList.add(domain);
}
return domainList;
}
按照上面这样写,跑了一次,好像能行,然后换个数据,在来一次之后,好像不生效了?于是debug看下,index的值是赋上了的,但是好像只有第一次生效,我当时第一反应,擦,这框架不会是用了缓存的吧,而且是不刷新的那种。然后一看源码,果然…
在ClassUtils里declaredFields方法,会创建一个FIELD_CACHE,每次字段映射其实用的就是这样一个Map。比较坑爹的是,这个Map构建的时候是用的computeIfAbsent(clazz, key -> {})这种写法,所以只要是同一个类,只会使用第一次构建的结构,后续重建的不会覆盖。
private static FieldCache declaredFields(Class<?> clazz) {
if (clazz == null) {
return null;
}
return FIELD_CACHE.computeIfAbsent(clazz, key -> {
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<Field>> orderFieldMap = new TreeMap<Integer, List<Field>>();
Map<Integer, Field> indexFieldMap = new TreeMap<Integer, Field>();
Map<String, Field> ignoreMap = new HashMap<String, Field>(16);
ExcelIgnoreUnannotated excelIgnoreUnannotated = clazz.getAnnotation(ExcelIgnoreUnannotated.class);
for (Field field : tempFieldList) {
declaredOneField(field, orderFieldMap, indexFieldMap, ignoreMap, excelIgnoreUnannotated);
}
return new FieldCache(buildSortedAllFieldMap(orderFieldMap, indexFieldMap), indexFieldMap, ignoreMap);
});
}
那如果还要走这条路的话,就只有每次去生成的时候,动态的生成一个新类,继承该类的所有属性,并设置index值。这个就可能要使用动态代理了,但是感觉真的走通有点耗精力,万一走不通还会得不偿失。
于是改方案二,第二种就是用很原始的办法去写了,header采用List>,data采用List<>,然后注意对应顺序,基本放弃官方的给的API了,完整代码如下:
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IntrospectionException {
List<List<String>> headList = new ArrayList<>();
for (int i = 0; i < 3; i++) {
List<String> head = new ArrayList<>();
head.add("test" + i);
headList.add(head);
}
List<String> fieldList = new ArrayList<>();
fieldList.add("girlFriend");
fieldList.add("hobby");
fieldList.add("name");
EasyExcel.write("test.xlsx",Domain.class)
.head(headList)
.sheet("test")
//因为head不是字段,所以这个属性不生效
// .includeColumnFiledNames(fieldList)
.doWrite(initMyData(fieldList));
}
private static List<List<Object>> initMyData(List<String> fieldList) throws NoSuchFieldException, IllegalAccessException {
List<Domain> list = init();
List<List<Object>> dataList = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
for (int j = 0; j <fieldList.size() ; j++) {
dataList.add(new ArrayList<>());
Field f = null;
try {
f = Domain.class.getDeclaredField(fieldList.get(j));
f.setAccessible(true);
dataList.get(i).add(f.get(list.get(i)));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
return dataList;
}
private static List<Domain> init() {
List<Domain> domainList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Domain domain = new Domain();
domain.setName("li"+i);
domain.setSex("男");
domain.setAge(20 + i);
domain.setHobby("看书");
domain.setGirlFriend("小可爱");
domainList.add(domain);
}
return domainList;
}
以上写法可以完美支持动态无序表格导出了,只是感觉代码没有动态设置index优雅(如果那条路能走通的话)。