🔵 (一) 功能现状
🍭目前大部分SpringBoot框架中自带了Excel导出功能,但其中并不支持自定义导出效果的可能性很大。比如很多框架中都能直接支持自动生成关于单表的增删改查操作的前后端代码,但是复杂的多表操作就无法做到,因为复杂的业务操作跟着需求走,自定义Excel表格导出也是如此。
🔵 (二) 核心问题
🍯想要解决这个自定义问题的我们,大部分是卡在合并单元格的这个环节以及如何做到动态处理单元格这个核心问题的两个方面。那么定义需要做合并单元格处理的字段以及动态处理单元格策略成为了我们的解决这个核心问题的组合拳。
🔴 (三) 合并单元格字段
🍬在我们实际操作Excel中会发现并不是所有的字段都需要做合并处理,所以Java层面需要指定哪些字段要做合并;而且某些字段内的数据量比较大或者字符数据较长,也需要在Java层面需要指明字段的列宽;同时,我们还需要指明合并字段的主键,以此来作为所导Excel行数以及序列计数的依据。
1️⃣自定义注解CustomMerge,用于判断是否需要合并,以及合并后哪个字段作为主键。
/**
* 自定义注解,用于判断是否需要合并以及合并的主键
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface CustomMerge {
/**
* 是否需要合并单元格
*/
boolean needMerge() default false;
/**
* 是否是主键,即该字段相同的行合并
*/
boolean isPk() default false;
}
2️⃣在实体类RoadSectionTrafficRep中写明@ExcelProperty (字段属性名称),@ColumnWidth (列宽),@CustomMerge (上面的自定义注解)
@Data
@EqualsAndHashCode(callSuper = false)
public class RoadSectionTrafficRep {
@ExcelProperty(value = "***")
@ColumnWidth(20)
@CustomMerge(needMerge = true,isPk = true)
@ApiModelProperty("***")
private String NO;
@ExcelProperty(value = "***")
@ColumnWidth(20)
@CustomMerge(needMerge = true)
@ApiModelProperty("***")
private String statisticalPeriod;
@ExcelProperty(value = "***")
@ColumnWidth(20)
@CustomMerge(needMerge = true)
@ApiModelProperty("***")
private String roadSectionName;
@ExcelProperty(value = "***")
@ColumnWidth(20)
@ApiModelProperty("***")
private Integer smallVehicleTraffic;
@ExcelProperty(value = "***")
@ColumnWidth(20)
@ApiModelProperty("***")
private Integer middleVehicleTraffic;
@ExcelProperty(value = "***")
@ColumnWidth(20)
@ApiModelProperty("***")
private Integer bigVehicleTraffic;
@ExcelProperty(value = "***")
@ColumnWidth(20)
@ApiModelProperty("***")
private Integer vehicleCnt;
}
🔴 (四) 单元格合并策略
🍪标记好需要合并的字段信息后,核心的合并策略参考以下代码,这里不做过多的阐述。
/**
* 自定义单元格合并策略
*/
public class CustomMergeStrategy implements RowWriteHandler {
/**
* 主键下标
*/
private Integer pkIndex;
/**
* 需要合并的列的下标集合
*/
private List<Integer> needMergeColumnIndex = new ArrayList<>();
/**
* DTO数据类型
*/
private Class<?> elementType;
public CustomMergeStrategy(Class<?> elementType) {
this.elementType = elementType;
}
@Override
public void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) {
// 如果是标题,则直接返回
if (isHead) {
return;
}
// 获取当前sheet
Sheet sheet = writeSheetHolder.getSheet();
// 获取标题行
Row titleRow = sheet.getRow(0);
if (null == pkIndex) {
this.lazyInit(writeSheetHolder);
}
// 判断是否需要和上一行进行合并
// 不能和标题合并,只能数据行之间合并
if (row.getRowNum() <= 1) {
return;
}
// 获取上一行数据
Row lastRow = sheet.getRow(row.getRowNum() - 1);
// 将本行和上一行是同一类型的数据(通过主键字段进行判断),则需要合并
if (lastRow.getCell(pkIndex).getStringCellValue().equalsIgnoreCase(row.getCell(pkIndex).getStringCellValue())) {
for (Integer needMerIndex : needMergeColumnIndex) {
CellRangeAddress cellRangeAddress = new CellRangeAddress(row.getRowNum() - 1, row.getRowNum(),
needMerIndex, needMerIndex);
sheet.addMergedRegionUnsafe(cellRangeAddress);
}
}
}
/**
* 初始化主键下标和需要合并字段的下标
*/
private void lazyInit(WriteSheetHolder writeSheetHolder) {
// 获取当前sheet
Sheet sheet = writeSheetHolder.getSheet();
// 获取标题行
Row titleRow = sheet.getRow(0);
// 获取DTO的类型
Class<?> eleType = this.elementType;
// 获取DTO所有的属性
Field[] fields = eleType.getDeclaredFields();
// 遍历所有的字段,因为是基于DTO的字段来构建excel,所以字段数 >= excel的列数
for (Field theField : fields) {
// 获取@ExcelProperty注解,用于获取该字段对应在excel中的列的下标
ExcelProperty easyExcelAnno = theField.getAnnotation(ExcelProperty.class);
// 为空,则表示该字段不需要导入到excel,直接处理下一个字段
if (null == easyExcelAnno) {
continue;
}
// 获取自定义的注解,用于合并单元格
CustomMerge customMerge = theField.getAnnotation(CustomMerge.class);
// 没有@CustomMerge注解的默认不合并
if (null == customMerge) {
continue;
}
for (int index = 0; index < fields.length; index++) {
Cell theCell = titleRow.getCell(index);
// 当配置为不需要导出时,返回的为null,这里作一下判断,防止NPE
if (null == theCell) {
continue;
}
// 将字段和excel的表头匹配上
if (easyExcelAnno.value()[0].equalsIgnoreCase(theCell.getStringCellValue())) {
if (customMerge.isPk()) {
pkIndex = index;
}
if (customMerge.needMerge()) {
needMergeColumnIndex.add(index);
}
}
}
}
// 没有指定主键,则异常
if (null == this.pkIndex) {
throw new IllegalStateException("使用@CustomMerge注解必须指定主键");
}
}
}
🔴 (五) Controller层代码
🍰导出说到底是对于查询到的数据再做一层处理的功能,所以将其代码放在Controller层即可。中间涉及到的包都是Alibaba中的依赖包,能自动导入,大家可以多点进去看看里面内部的实现机制。
1️⃣导出表格处理,中间自定义的合并处理可以自行设置。
@ApiOperation("导出*****表")
@SneakyThrows
@GetMapping("/exportRoadSectionTrafficReport")
public void exportRoadSectionTrafficReport(HttpServletResponse response){
List<RoadSectionTrafficRep> orderList = dataReportService.getRoadSectionTraffic();
//中间自定义的合并处理,自由发挥咯... ...
setExcelRespProp(response, "******表");
EasyExcel.write(response.getOutputStream())
.head(RoadSectionTrafficRep.class)
.registerWriteHandler(new CustomMergeStrategy(RoadSectionTrafficRep.class))
.excelType(ExcelTypeEnum.XLSX)
.sheet("*****表")
.doWrite(orderList);
}
2️⃣设置Excel下载响应头属性
/**
* 设置excel下载响应头属性
*/
private void setExcelRespProp(HttpServletResponse response, String rawFileName) throws UnsupportedEncodingException {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
String fileName = URLEncoder.encode(rawFileName, "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
}
🍆🍆🍆路过的小伙伴,如果本篇博文对你的学习或者工作有所帮助,可以点赞+收藏+关注一波呀~👊👊👊小编后续每过一段时间会整理出相关项目实例的博文,感谢您的支持哦!!!✈️✈️✈️