目录
- 前言
- 一、情景介绍
- 二、问题分析
- 三、代码实现
- 方式一:head 设置
- 方式二:模板导出
- 方式三:自定义工具类
前言
Java-easyExcel入门教程:https://blog.csdn.net/xhmico/article/details/134714025
之前有介绍过如何使用 easyExcel
,以及写了两个入门的 demo
,这两个 demo
能应付在开发中大多数的导入和导出需求,不过有时候面对一些复杂的表格,就会有点不够用,该篇讲述的是如何实现复杂表头编写
一、情景介绍
在实际的开发过程中可能会遇到需要导出一些带有复杂表头的表格,比如以下案例:
该表头占了两行,其中 橙色 部分的信息是需要动态生成的
二、问题分析
关于如何实现类似于上述复杂表头,有多种方式均可实现,首先这个表头是一个复杂表头,其次还有动态的部分
查看官方文档,对应复杂表头的实现
官方文档:复杂头写入
从中可以看出,多行表头就是由 多个纵向
的列表组成,并且表头 相同的部分会自动合并居中对齐
再查阅官方文档关于如何实现动态头的写入
官方文档:动态头、实时生成头写入
官方给了一个 head()
方法允许我们在代码中自定义表头
public T head(List<List<String>> head) {
this.parameter().setHead(head);
return this.self();
}
三、代码实现
方式一:head 设置
可以将上述表头看作是以下 6 个集合组成的表头,然后使用 head()
方法去设置
代码示例:
/**
* 复杂表头编写:方式一
*/
@Test
public void complexHeadDemo01() {
// 输出文件路径
String outFilePath = "D:\\excel-files\\demo01.xlsx";
// 表格数据
List<Object> data = new ArrayList<>();
EasyExcel.write(outFilePath)
// 动态头
.head(head())
.sheet()
// 表格数据
.doWrite(data);
}
private List<List<String>> head() {
List<List<String>> list = new ArrayList<List<String>>();
List<String> head0 = new ArrayList<String>();
head0.add("部门");
head0.add("用户名称");
List<String> head1 = new ArrayList<String>();
head1.add("运营部");
head1.add("性别");
List<String> head2 = new ArrayList<String>();
head2.add("运营部");
head2.add("年龄");
List<String> head3 = new ArrayList<String>();
head3.add("时间");
head3.add("出生日期");
List<String> head4 = new ArrayList<String>();
head4.add("2024-04-09");
head4.add("学历");
List<String> head5 = new ArrayList<String>();
head5.add("2024-04-09");
head5.add("电话号码");
list.add(head0);
list.add(head1);
list.add(head2);
list.add(head3);
list.add(head4);
list.add(head5);
return list;
}
结果展示:
可以看到是能够实现这种表头的,不过需要自己定义表头的样式
方式二:模板导出
可以使用模板导出的方式,设置一个模板文件,例如:
实体类:
DeptUserExcelEntity.java
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class DeptUserExcelEntity {
@ApiModelProperty(value = "用户名称")
private String realName;
@ApiModelProperty(value = "性别")
private String gender;
@ApiModelProperty(value = "年龄")
private Integer age;
@ApiModelProperty(value = "出生日期")
private String birthdate;
@ApiModelProperty(value = "学历")
private String education;
@ApiModelProperty(value = "电话号码")
private String telephone;
}
代码示例:
/**
* 复杂表头编写:方式二
*/
@Test
public void complexHeadDemo02() {
// 模板文件路径
String templateFilePath = "D:\\excel-files\\template.xlsx";
// 输出文件路径
String outFilePath = "D:\\excel-files\\demo02.xlsx";
// 创建 ExcelWriter 实例
ExcelWriter writer = EasyExcel
// 写入到
.write(outFilePath)
// 指定模板
.withTemplate(templateFilePath)
.build();
WriteSheet sheet = EasyExcel.writerSheet().build();
Map<String, String> replaceMap = new HashMap<>();
replaceMap.put("deptName", "运营部");
replaceMap.put("currentDate", "2024-04-09");
// 执行填充普通占位符操作
writer.fill(replaceMap, sheet);
// 获取员工信息
List<DeptUserExcelEntity> data = new ArrayList<>();
FillConfig fillConfig = FillConfig.builder()
// 开启填充换行
.forceNewRow(true)
.build();
// 执行填充列表操作
writer.fill(data, fillConfig, sheet);
// 结束
writer.finish();
}
结果展示:
可以看到效果是比较好的,也不用担心表格样式的问题
关于如何使用 easyexcel
实现按模板导出,可参考:easyExcel - 按模板导出 有较为详细的说明
方式三:自定义工具类
根据官方实现复杂表头的写法,自定义输出对象为
DeptUserExcelEntity.java
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.*;
import com.alibaba.excel.enums.poi.BorderStyleEnum;
import com.alibaba.excel.enums.poi.FillPatternTypeEnum;
import com.alibaba.excel.enums.poi.HorizontalAlignmentEnum;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
// 头背景设置
@HeadStyle(fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND, horizontalAlignment = HorizontalAlignmentEnum.CENTER, borderLeft = BorderStyleEnum.THIN, borderTop = BorderStyleEnum.THIN, borderRight = BorderStyleEnum.THIN, borderBottom = BorderStyleEnum.THIN)
//标题高度
@HeadRowHeight(40)
//内容高度
@ContentRowHeight(30)
//内容居中,左、上、右、下的边框显示
@ContentStyle(horizontalAlignment = HorizontalAlignmentEnum.CENTER, borderLeft = BorderStyleEnum.THIN, borderTop = BorderStyleEnum.THIN, borderRight = BorderStyleEnum.THIN, borderBottom = BorderStyleEnum.THIN)
public class DeptUserExcelEntity {
@ApiModelProperty(value = "用户名称")
@ExcelProperty({"部门","用户名称"})
@ColumnWidth(15)
private String realName;
@ApiModelProperty(value = "性别")
@ExcelProperty({"deptName","性别"})
@ColumnWidth(15)
private String gender;
@ApiModelProperty(value = "年龄")
@ExcelProperty({"deptName","年龄"})
@ColumnWidth(15)
private Integer age;
@ApiModelProperty(value = "出生日期")
@ExcelProperty({"时间","出生日期"})
@ColumnWidth(15)
private String birthdate;
@ApiModelProperty(value = "学历")
@ExcelProperty({"currentDate","学历"})
@ColumnWidth(20)
private String education;
@ApiModelProperty(value = "电话号码")
@ExcelProperty({"currentDate","电话号码"})
@ColumnWidth(20)
private String telephone;
}
如果依照之前的导出案例,代码如下:
@Test
public void complexHeadDemo03_test() {
// 输出文件路径
String outFilePath = "D:\\excel-files\\demo03.xlsx";
List<DeptUserExcelEntity> excelEntities = new ArrayList<>();
EasyExcel.write(outFilePath, DeptUserExcelEntity.class)
.sheet()
.doWrite(excelEntities);
}
最后得到的表格也是根据输出对象定义而来的
所以如果能让表格在写入的时候,输出对象 DeptUserExcelEntity
中 @ExcelProperty
里面的 deptName
和 currentDate
替换成想要的不就行了,所以我就自定义一个工具类,在需要的时候改变注解的属性值就行了
工具类:
AnnotationUtils.java
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.CellType;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
/**
* 注解工具类
*/
public class AnnotationUtils {
/**
* 变更注解的属性值再处理业务,处理完业务之后恢复类的属性
*
* @param clazz 注解所在的实体类
* @param tClass 注解类
* @param attrName 要修改的注解属性名
* @param attrTypeEnum 要修改的注解属性的类型
* @param valueMap 要设置的属性值
*/
public static <A extends Annotation> void changeAnnotationValueToDealProcess(
Class<?> clazz,
Class<A> tClass,
String attrName,
AttrTypeEnum attrTypeEnum,
Map<String, String> valueMap,
DealProcess dealProcess) {
try {
Map<String, Object> fieldAnnotationValueMap = new HashMap<>();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
A annotation = field.getAnnotation(tClass);
if (annotation == null) continue;
Object value = setAnnotationValue(annotation, attrName, attrTypeEnum, valueMap);
String fieldName = field.getName();
fieldAnnotationValueMap.put(fieldName, value);
}
// 处理业务逻辑
dealProcess.deal();
// 恢复
for (Field field : fields) {
A annotation = field.getAnnotation(tClass);
String fieldName = field.getName();
if (annotation == null) continue;
Object value = fieldAnnotationValueMap.get(fieldName);
InvocationHandler handler = Proxy.getInvocationHandler(annotation);
Field memberValuesField = handler.getClass().getDeclaredField("memberValues");
memberValuesField.setAccessible(true);
@SuppressWarnings("all")
Map<String, Object> memberValues = (Map) memberValuesField.get(handler);
memberValues.put(attrName, value);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 设置注解中的字段值
*
* @param annotation 要修改的注解实例
* @param attrName 要修改的注解属性名
* @param attrTypeEnum 要修改的注解属性的类型
* @param valueMap 替换属性集的map
*/
@SuppressWarnings("all")
private static Object setAnnotationValue(Annotation annotation, String attrName,
AttrTypeEnum attrTypeEnum, Map<String, String> valueMap) throws NoSuchFieldException, IllegalAccessException {
InvocationHandler handler = Proxy.getInvocationHandler(annotation);
Field field = handler.getClass().getDeclaredField("memberValues");
field.setAccessible(true);
Map memberValues = (Map) field.get(handler);
Object value = memberValues.get(attrName);
switch (attrTypeEnum) {
case STRING: {
String oldValue = (String) value;
String newValue = valueMap.get(oldValue);
if (StringUtils.isNotBlank(newValue)) {
memberValues.put(attrName, newValue);
}
}
break;
case STRING_ARR: {
String[] oldValue = (String[]) value;
String[] newValue = new String[oldValue.length];
for (int i = 0; i < oldValue.length; i++) {
String replace = valueMap.get(oldValue[i]);
newValue[i] = replace != null ? replace : oldValue[i];
}
memberValues.put(attrName, newValue);
}
break;
}
return value;
}
public enum AttrTypeEnum {
STRING,
STRING_ARR
}
public interface DealProcess {
void deal() throws Exception;
}
}
代码示例:
/**
* 复杂表头编写:方式三
*/
@Test
public synchronized void complexHeadDemo03() {
// 输出文件路径
String outFilePath = "D:\\excel-files\\demo03.xlsx";
// 替换注解中的属性值为
HashMap<String, String> replaceMap = new HashMap<>();
replaceMap.put("deptName", "运营部");
replaceMap.put("currentDate", "2024-04-09");
/*
* 这里简单的说明一下:
* attrName: 对应的是 @ExcelProperty 中的 value 属性
* attrTypeEnum: 对应的是 @ExcelProperty 中的 value 属性 的类型
*/
AnnotationUtils.changeAnnotationValueToDealProcess(
DeptUserExcelEntity.class, ExcelProperty.class, "value", AnnotationUtils.AttrTypeEnum.STRING_ARR, replaceMap, new AnnotationUtils.DealProcess() {
@Override
public void deal() {
List<DeptUserExcelEntity> excelEntities = new ArrayList<>();
EasyExcel.write(outFilePath, DeptUserExcelEntity.class)
.sheet().doWrite(excelEntities);
}
}
);
}
结果:
这里要注意的是因为调用该方法时回去修改对应的 class
对象,所以这里最好加锁
这种方式不仅可以处理 easyexcel
的注解动态表头问题,也可以处理传统的 poi
的注解动态表头,目前也是我用得比较多的一种方式