• EasyExcel复杂表头导出(一对多)升级版


    一、前言

            在之前写的 EasyExcel复杂表头导出(一对多)的博客的结尾,受限于当时的能力和精力,留下一些问题及展望。现在写下此博客,目的就是解决之前遗留的问题。

            背景介绍,见上述链接指向的博客,这里主要通过自定义拦截器的形式来完美解决。

    二、导出功能的实现

    2.1 Entity 对象

    1. import com.alibaba.excel.annotation.ExcelProperty;
    2. import com.alibaba.excel.annotation.write.style.ColumnWidth;
    3. import com.alibaba.excel.annotation.write.style.ContentRowHeight;
    4. import com.alibaba.excel.annotation.write.style.HeadRowHeight;
    5. import com.alibaba.excel.annotation.write.style.HeadStyle;
    6. import com.alibaba.excel.converters.string.StringImageConverter;
    7. import lombok.AllArgsConstructor;
    8. import lombok.Data;
    9. import lombok.EqualsAndHashCode;
    10. import lombok.NoArgsConstructor;
    11. import java.net.URL;
    12. @Data
    13. @EqualsAndHashCode
    14. @HeadRowHeight(30)
    15. @ContentRowHeight(80)
    16. @ColumnWidth(15)
    17. @HeadStyle(fillForegroundColor = 44)
    18. @NoArgsConstructor
    19. @AllArgsConstructor
    20. class Customer {
    21. @ExcelProperty({"客户编号"})
    22. private String userCode;
    23. @ExcelProperty({"客户名称"})
    24. private String userName;
    25. @ColumnWidth(25)
    26. @ExcelProperty({"客户所在地址"})
    27. private String address;
    28. @ExcelProperty({"联系人信息", "联系人姓名"})
    29. private String personName;
    30. @ExcelProperty({"联系人信息", "联系电话"})
    31. private String telephone;
    32. @ExcelProperty({"图片"})
    33. private URL picture;
    34. /**
    35. * 你也可以通过字符串的形式来保存图片,具体说明见注意事项3.1
    36. */
    37. //@ExcelProperty(converter = StringImageConverter.class, value = {"本地图片"})
    38. //private String localPic;
    39. }

    2.2 Controller 层

    1. @PostMapping("/exportExcel")
    2. @ApiOperation("导出Excel")
    3. public void exportExcel(HttpServletResponse response) throws Exception {
    4. // 查询需要导出的数据
    5. List result = getData();
    6. // 1设置表头样式
    7. WriteCellStyle headStyle = new WriteCellStyle();
    8. // 1.1设置表头数据居中
    9. headStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
    10. // 2设置表格内容样式
    11. WriteCellStyle bodyStyle = new WriteCellStyle();
    12. // 2.1设置表格内容水平居中
    13. bodyStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
    14. // 2.2设置表格内容垂直居中
    15. bodyStyle.setVerticalAlignment(VerticalAlignment.CENTER);
    16. // 3设置表格sheet样式
    17. WriteSheet sheet = EasyExcel.writerSheet("客户信息").head(Customer.class).sheetNo(1).build();
    18. // 4拿到表格处理对象
    19. ExcelWriter writer = EasyExcel.write(response.getOutputStream()).needHead(true).excelType(ExcelTypeEnum.XLSX)
    20. // 设置需要待合并的行和列。参数1:数值数组,指定需要合并的列;参数2:数值,指定从第几行开始合并
    21. .registerWriteHandler(new ExcelMergeCellHandler(new int[]{0, 1, 2, 5}, 0))
    22. // 设置单元格的风格样式
    23. .registerWriteHandler(new HorizontalCellStyleStrategy(headStyle, bodyStyle))
    24. .build();
    25. // 5写入excel数据
    26. writer.write(result, sheet);
    27. // 6通知浏览器以附件的形式下载处理,设置返回头要注意文件名有中文
    28. response.setHeader("Content-disposition", "attachment;filename=" + new String("客户信息表".getBytes("gb2312"), "ISO8859-1") + ".xlsx");
    29. response.setContentType("multipart/form-data");
    30. response.setCharacterEncoding("utf-8");
    31. writer.finish();
    32. }

    2.3 自定义拦截器(ExcelMergeCellHandler)

    1. import com.alibaba.excel.metadata.Head;
    2. import com.alibaba.excel.metadata.data.WriteCellData;
    3. import com.alibaba.excel.write.handler.CellWriteHandler;
    4. import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
    5. import com.alibaba.excel.write.metadata.holder.WriteTableHolder;
    6. import lombok.AllArgsConstructor;
    7. import lombok.Data;
    8. import lombok.NoArgsConstructor;
    9. import org.apache.poi.ss.usermodel.Cell;
    10. import org.apache.poi.ss.usermodel.CellType;
    11. import org.apache.poi.ss.usermodel.Sheet;
    12. import org.apache.poi.ss.util.CellRangeAddress;
    13. import java.util.List;
    14. /**
    15. * @author DaHuaJia
    16. * @Description 自定义单元格合并处理Handler类
    17. * @Date 2022-08-18 19:25:58
    18. */
    19. @Data
    20. @NoArgsConstructor
    21. @AllArgsConstructor
    22. public class ExcelMergeCellHandler implements CellWriteHandler {
    23. // 需要合并的列,从0开始算
    24. private int[] mergeColIndex;
    25. // 从指定的行开始合并,从0开始算
    26. private int mergeRowIndex;
    27. /**
    28. * 在单元格上的所有操作完成后调用,遍历每一个单元格,判断是否需要向上合并
    29. */
    30. @Override
    31. public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
    32. // 获取当前单元格行下标
    33. int currRowIndex = cell.getRowIndex();
    34. // 获取当前单元格列下标
    35. int currColIndex = cell.getColumnIndex();
    36. // 判断是否大于指定行下标,如果大于则判断列是否也在指定的需要的合并单元列集合中
    37. if (currRowIndex > mergeRowIndex) {
    38. for (int i = 0; i < mergeColIndex.length; i++) {
    39. if (currColIndex == mergeColIndex[i]) {
    40. /**
    41. * 获取列表数据的唯一标识。不同集合的数据即使数值相同也不合并
    42. * 注意:我这里的唯一标识为客户编号(Customer.userCode),在第一列,即下标为0。大家需要结合业务逻辑来做修改
    43. */
    44. // 获取当前单元格所在的行数据的唯一标识
    45. Object currCode = cell.getRow().getCell(0).getStringCellValue();
    46. // 获取当前单元格的正上方的单元格所在的行数据的唯一标识
    47. Object preCode = cell.getSheet().getRow(currRowIndex - 1).getCell(0).getStringCellValue();
    48. // 判断两条数据的是否是同一集合,只有同一集合的数据才能合并单元格
    49. if(preCode.equals(currCode)){
    50. // 如果都符合条件,则向上合并单元格
    51. mergeWithPrevRow(writeSheetHolder, cell, currRowIndex, currColIndex);
    52. break;
    53. }
    54. }
    55. }
    56. }
    57. }
    58. /**
    59. * 当前单元格向上合并
    60. *
    61. * @param writeSheetHolder 表格处理句柄
    62. * @param cell 当前单元格
    63. * @param currRowIndex 当前行
    64. * @param currColIndex 当前列
    65. */
    66. private void mergeWithPrevRow(WriteSheetHolder writeSheetHolder, Cell cell, int currRowIndex, int currColIndex) {
    67. // 获取当前单元格数值
    68. Object currData = cell.getCellTypeEnum() == CellType.STRING ? cell.getStringCellValue() : cell.getNumericCellValue();
    69. // 获取当前单元格正上方的单元格对象
    70. Cell preCell = cell.getSheet().getRow(currRowIndex - 1).getCell(currColIndex);
    71. // 获取当前单元格正上方的单元格的数值
    72. Object preData = preCell.getCellTypeEnum() == CellType.STRING ? preCell.getStringCellValue() : preCell.getNumericCellValue();
    73. // 将当前单元格数值与其正上方单元格的数值比较
    74. if (preData.equals(currData)) {
    75. Sheet sheet = writeSheetHolder.getSheet();
    76. List mergeRegions = sheet.getMergedRegions();
    77. // 当前单元格的正上方单元格是否是已合并单元格
    78. boolean isMerged = false;
    79. for (int i = 0; i < mergeRegions.size() && !isMerged; i++) {
    80. CellRangeAddress address = mergeRegions.get(i);
    81. // 若上一个单元格已经被合并,则先移出原有的合并单元,再重新添加合并单元
    82. if (address.isInRange(currRowIndex - 1, currColIndex)) {
    83. sheet.removeMergedRegion(i);
    84. address.setLastRow(currRowIndex);
    85. sheet.addMergedRegion(address);
    86. isMerged = true;
    87. }
    88. }
    89. // 若上一个单元格未被合并,则新增合并单元
    90. if (!isMerged) {
    91. CellRangeAddress cellRangeAddress = new CellRangeAddress(currRowIndex - 1, currRowIndex, currColIndex, currColIndex);
    92. sheet.addMergedRegion(cellRangeAddress);
    93. }
    94. }
    95. }
    96. }

    2.4 getDate方法(用于模拟service层拿到的数据)

    1. public static List getData() throws Exception {
    2. List data = new ArrayList<>();
    3. Customer customer = new Customer("JiangXi", "江西电信公司", "江西省南昌市东湖区", "张三", "12345678910", new URL("https://m.360buyimg.com/babel/jfs/t1/221733/11/14107/61280/62fde84dE467522ce/79bbd42aa93f5a83.jpg"));
    4. data.add(customer);
    5. Customer customer2 = new Customer("JiangXi", "江西电信公司", "江西省南昌市东湖区", "李四", "15848563521", new URL("https://m.360buyimg.com/babel/jfs/t1/221733/11/14107/61280/62fde84dE467522ce/79bbd42aa93f5a83.jpg"));
    6. data.add(customer2);
    7. Customer customer3 = new Customer("GuangDong", "广东电信公司", "广东省广州市花都区", "小明", "15847953624", new URL("https://m.360buyimg.com/babel/jfs/t1/215924/36/19623/23344/62baa985E4df523c6/4893237860b306d6.jpg"));
    8. data.add(customer3);
    9. Customer customer4 = new Customer("GuangDong", "广东电信公司", "广东省广州市天河区", "小红", "16849531548", new URL("https://m.360buyimg.com/babel/jfs/t1/189640/15/26493/35837/62baa97eE6abda209/461f91e682d0e81a.jpg"));
    10. data.add(customer4);
    11. Customer customer5 = new Customer("GuangDong", "广东电信公司", "广东省广州市天河区", "小华", "16985632481", new URL("https://m.360buyimg.com/babel/jfs/t1/189640/15/26493/35837/62baa97eE6abda209/461f91e682d0e81a.jpg"));
    12. data.add(customer5);
    13. Customer customer6 = new Customer("BeiJing", "北京电信公司", "北京市东城区", "姜维", "16598645874", new URL("https://m.360buyimg.com/babel/jfs/t1/31481/11/16081/24873/62baa97dE6f3991d0/94ae13d66b9bbfdd.jpg"));
    14. data.add(customer6);
    15. return data;
    16. }

    2.5 效果

     三、注意事项

    3.1 图片导出问题

    对于图片的导出,其字段可以有多种数据类型,官网就介绍了5种(File、InputStream、String、byte[]、URL、WriteCellData)。这里简要介绍一下String 和 URL。

    1、String 类型

    1. /**
    2. *  如果图片地址通过String类型保存,则需要加一个自带的类型转换器(StringImageConverter)
    3. */
    4. @ExcelProperty(converter = StringImageConverter.class, value = {"本地图片"})
    5. private String localPic;

    2、URL类型 

    1. @ExcelProperty({"网络图片"})
    2. private URL picture;

            经过测试发现,String类型只能保存本地图片地址,如果保存网络图片地址,则会导致图片无法下载。原因则是EasyExcel会把“//” 转换成 “\”,导致地址错误。

            因此,可以约定String类型用于保存本地图片地址,URL类型用于保存网络图片地址。

    3.2 图片单元格合并问题

            图片类型单元格无法做到相同的图片合并单元格,主要是因为无法通过单元格对象拿到图片的序列化值。

    3.3 表格样式

            表格的样式既可以表格样式类(例如:WriteCellStyle)来设置,也可以通过注解(例如:@HeadStyle)来设置,两者互补,不冲突。

  • 相关阅读:
    python 执行cmd命令,输出日志的几种写法
    服务器部署 CentOS、VeraCrypt、Docker、主从MySQL、Redis、备份等
    【信号隐藏-数字水印】基于小波变换算法DWT结合离散余弦变换DCT实现音频数字水印嵌入提取附matlab代码
    前端求职难?那是你没看见这个
    成都理工大学_Python程序设计_第8章
    【eslint 插件开发】禁用 location 跳转外部链接
    Java18知多少
    【Redis7】--3.Redis持久化
    Vue+ECharts实现可视化地图
    基于一种交互式的光伏组件特性曲线算法(Matlab代码实现)
  • 原文地址:https://blog.csdn.net/qq_41057885/article/details/126411957