开源轮子 - EasyExcel02(深入实践)

[复制链接]
发表于 2025-11-18 16:48:08 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
EasyExcel02 - 深入实践

   本文整理自掘金大佬 - 竹子爱熊猫
https://juejin.cn/post/7405158771017220131
  

一:通用监听器

利用EasyExcel分析excel文件时,针对差别的业务场景,通常必要界说差别的监听器,如:


  • 现在必要导入商品数据,就必要界说一个ProductListener;
  • 现在必要导入员工数据,就必要界说一个StaffListener;
  • ……
很显然,这一步会造成大量性子类似的class被界说出来,以是,能否封装一个通用监听器,以此来镌汰这步工作量与重复类呢
1:通用监听器

不管是什么业务场景下的Excel导入,都会颠末“分析文件、提取数据、举行业务处理处罚”这三步
除开第三步外,前两步逻辑是类似的,既然逻辑类似,那么天然可以抽象成通用监听器
  1. package com.example.bootrocketmq.study.wheel.easyexcel;
  2. import com.alibaba.excel.context.AnalysisContext;
  3. import com.alibaba.excel.event.AnalysisEventListener;
  4. import java.lang.reflect.Field;
  5. import java.util.ArrayList;
  6. import java.util.List;
  7. import java.util.Map;
  8. /**
  9. * @author cui haida
  10. * 2024/12/22
  11. */
  12. public class CommonListener<T> extends AnalysisEventListener<T> {
  13.     //创建list集合封装最终的数据
  14.     private final List<T> data;
  15.     // 字段列表
  16.     private final Field[] fields;
  17.     // 类
  18.     private final Class<T> clazz;
  19.     // 是否对excel的有效性进行校验,如果设置为true, 将会检验excel模板的头和数据模型类字段是否匹配
  20.     private boolean validateSwitch = true;
  21.     public CommonListener(Class<T> clazz) {
  22.         fields = clazz.getDeclaredFields();
  23.         this.clazz = clazz;
  24.         this.data = new ArrayList<T>();
  25.     }
  26.     /*
  27.      * 每解析到一行数据都会触发
  28.      * */
  29.     @Override
  30.     public void invoke(T row, AnalysisContext analysisContext) {
  31.         data.add(row);
  32.     }
  33.     /*
  34.      * 读取到excel头信息时触发,会将表头数据转为Map集合
  35.      * */
  36.     @Override
  37.     public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
  38.         // 校验读到的excel表头是否与数据模型类匹配
  39.         if (validateSwitch) {
  40.             // 如果需要进行正确性校验,将会进行工具方法进行excel模板校验 -> header和数据模型类字段关系校验
  41.             ExcelUtil.validateExcelTemplate(headMap, clazz, fields);
  42.         }
  43.     }
  44.     /*
  45.      * 所有数据解析完之后触发
  46.      * */
  47.     @Override
  48.     public void doAfterAllAnalysed(AnalysisContext analysisContext) {}
  49.     /*
  50.      * 关闭excel表头验证
  51.      * */
  52.     public void offValidate() {
  53.         this.validateSwitch = false;
  54.     }
  55.     /*
  56.      * 返回解析到的所有数据
  57.      * */
  58.     public List<T> getData() {
  59.         return data;
  60.     }
  61. }
复制代码
上述界说了一个通用监听器,实在跟EasyExcel里提供的SyncReadListener非常类似,只不外我们这里做了两点优化


  • 利用泛型来取代Object,使其变得更加的机动,获取数据的时间无需举行逼迫转换
  • 增长模板校验机制,查抄excel的头信息和数据模子字段的匹配关系
为什么要有第二个呢?
全部excel导入的业务场景,都是先下载模板,再根据模板指引填写数据,末了才上传填写好的excel文件,这是导入场景的业务流程
如果你不对传入的excel文件举行模板校验,这时就算任意传个excel文件,EasyExcel也照旧不会报错,而是分析出多行全部字段为空的数据,从而触发你背面的业务逻辑引发未知Bug
  1. package com.example.bootrocketmq.study.wheel.easyexcel;
  2. import com.alibaba.excel.annotation.ExcelIgnore;
  3. import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
  4. import com.alibaba.excel.annotation.ExcelProperty;
  5. import com.alibaba.excel.exception.ExcelAnalysisException;
  6. import lombok.extern.slf4j.Slf4j;
  7. import java.lang.reflect.Field;
  8. import java.util.Collection;
  9. import java.util.Map;
  10. /**
  11. * @author cui haida
  12. * 2024/12/22
  13. */
  14. @Slf4j
  15. public class ExcelUtil {
  16.     /*
  17.      * 校验excel文件的表头,与数据模型类的映射关系是否匹配
  18.      * */
  19.     public static void validateExcelTemplate(Map<Integer, String> headMap, Class<?> clazz, Field[] fields) {
  20.         // 拿到头部名称
  21.         Collection<String> headNames = headMap.values();
  22.         // 类上是否存在忽略excel字段的注解
  23.         boolean classIgnore = clazz.isAnnotationPresent(ExcelIgnoreUnannotated.class);
  24.         int count = 0;
  25.         for (Field field : fields) {
  26.             // 如果字段上存在忽略注解,则跳过当前字段
  27.             if (field.isAnnotationPresent(ExcelIgnore.class)) {
  28.                 continue;
  29.             }
  30.             ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
  31.             if (null == excelProperty) {
  32.                 // 如果类上也存在忽略注解,则跳过所有未使用ExcelProperty注解的字段
  33.                 if (classIgnore) {
  34.                     continue;
  35.                 }
  36.                 // 如果检测到既未忽略、又未映射excel列的字段,则抛出异常提示模板不正确
  37.                 throw new ExcelAnalysisException("请检查导入的excel文件是否按模板填写!");
  38.             }
  39.             // 校验数据模型类上绑定的名称,是否与excel列名匹配
  40.             String[] value = excelProperty.value();
  41.             String name = value[0];
  42.             if (name != null && !name.isEmpty() && !headNames.contains(name)) {
  43.                 throw new ExcelAnalysisException("请检查导入的excel文件是否按模板填写!");
  44.             }
  45.             // 更新有效字段的数量
  46.             count++;
  47.         }
  48.         // 最后校验数据模型类的有效字段数量,与读到的excel列数量是否匹配
  49.         if (headMap.size() != count) {
  50.             throw new ExcelAnalysisException("请检查导入的excel文件是否按模板填写!");
  51.         }
  52.     }
  53. }
复制代码
先来看三个入参,分别是当前读到的表头聚集,以及数据模子类的class对象和字段数组,这三个参数会用来校验模板的准确性。


  • 注意点一:ExcelProperty注解提供了列名、权重、索引三种列匹配模式,可现在只是基于列名实现了校验逻辑,以是界说的数据模子类上,不支持利用index、order来指定字段与excel列的匹配关系(也可以自行改造上面的第四步校验逻辑)
  • 注意点二:出于校验机制的存在,以是这个通用监听器无法正常读取多行头的excel文件,如果你想要正常分析多行头文件,则可以在创建监听器之后,手动调用offValidate()方法来关闭模板校验机制,这时就能制止校验机制干扰多行头文件的读取
  • 注意点三:如果你利用的EasyExcel版本较低,分析excel数据时不会主动忽略已利用过的空行,即填写过内容又删除的数据行,在分析时仍旧会被辨认成一条数据,这种环境必要在监听器的invoke()方法中主动过滤
2:分批处理处罚监听器

前面封装的通用监听器,现实上只能满足数据量不大的业务场景,当数据量到达几万行、数十万、百万行时,如果再利用这个通用监听器就会存在OOM风险
由于分析到的全部数据,都会暂存到内部的data聚集里,直至整个文件全部数据行读取竣事
对于大文件而言,这种模式会给内存造成较大的负担,一旦同时导入的excel文件数量过多,内存资源耗尽就会引发内存溢出标题,怎么办?可以选择分批处理处罚读取到的数据
  1. package com.example.bootrocketmq.study.wheel.easyexcel;
  2. import com.alibaba.excel.context.AnalysisContext;
  3. import com.alibaba.excel.event.AnalysisEventListener;
  4. import java.lang.reflect.Field;
  5. import java.util.ArrayList;
  6. import java.util.List;
  7. import java.util.Map;
  8. import java.util.function.Consumer;
  9. /**
  10. * @author cui haida
  11. * 2024/12/22
  12. */
  13. public class BatchHandleListener<T> extends AnalysisEventListener<T> {
  14.     // 每次处理的行数(每次处理1000行 - 批次大小)
  15.     private static int BATCH_NUMBER = 1000;
  16.     // 临时存储读取到的excel数据
  17.     private List<T> data;
  18.     // 行数和批次编号
  19.     private int rows, batchNo;
  20.     // 是否要进行excel模板校验,如果需要校验,将validateSwitch设置为true
  21.     private boolean validateSwitch = true;
  22.     /*
  23.      * 每批数据的业务逻辑处理器
  24.      * 说明:如果业务方法会返回结果,可以将换成Function接口,同时类上新增一个类型参数
  25.      **/
  26.     private final Consumer<List<T>> businessHandler;
  27.     /*
  28.      * 用于校验excel模板正确性的字段
  29.      **/
  30.     private final Field[] fields;
  31.     private final Class<T> clazz;
  32.     /**
  33.      * @author cui haida
  34.      * 方法编写日期: 2024/12/22
  35.      * desc -> 构造方法1 - 使用的批次大小为默认的1000条
  36.      */
  37.     public BatchHandleListener(Class<T> clazz, Consumer<List<T>> handle) {
  38.         // 通过构造器为字段赋值,用于校验excel文件与模板十分匹配
  39.         this(clazz, handle, BATCH_NUMBER);
  40.     }
  41.     /**
  42.      * @author cui haida
  43.      * 方法编写日期: 2024/12/22
  44.      * desc ->  构造方法2 - 传入具体使用的批次大小
  45.      */
  46.     public BatchHandleListener(Class<T> clazz, Consumer<List<T>> handle, int batchNumber) {
  47.         // 通过构造器为字段赋值,用于校验excel文件与模板十分匹配
  48.         this.clazz = clazz;
  49.         this.fields = clazz.getDeclaredFields();
  50.         // 初始化临时存储数据的集合,及外部传入的业务方法
  51.         this.businessHandler = handle;
  52.         BATCH_NUMBER = batchNumber;
  53.         this.data = new ArrayList<>(BATCH_NUMBER);
  54.     }
  55.     /*
  56.      * 读取到excel头信息时触发,会将表头数据转为Map集合(用于校验导入的excel文件与模板是否匹配)
  57.      *   注意点1:当前校验逻辑不适用于多行头模板(如果是多行头的文件,请关闭表头验证);
  58.      *   注意点2:使用当前监听器的导入场景,模型类不允许出现既未忽略、又未使用ExcelProperty注解的字段;
  59.      **/
  60.     @Override
  61.     public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
  62.         if (validateSwitch) {
  63.             ExcelUtil.validateExcelTemplate(headMap, clazz, fields);
  64.         }
  65.     }
  66.     /*
  67.      * 每成功解析一条excel数据行时触发
  68.      **/
  69.     @Override
  70.     public void invoke(T row, AnalysisContext analysisContext) {
  71.         data.add(row);
  72.         // 判断当前已解析的数据是否达到本批上限,是则执行对应的业务处理
  73.         if (data.size() >= BATCH_NUMBER) {
  74.             // 更新读取到的总行数、批次数
  75.             rows += data.size();
  76.             batchNo++;
  77.             // 触发业务逻辑处理
  78.             this.businessHandler.accept(data);
  79.             // 处理完本批数据后,使用新List替换旧List,旧List失去引用后会很快被GC
  80.             data = new ArrayList<>(BATCH_NUMBER);
  81.         }
  82.     }
  83.     /*
  84.      * 所有数据解析完之后触发
  85.      **/
  86.     @Override
  87.     public void doAfterAllAnalysed(AnalysisContext analysisContext) {
  88.         // 因为最后一批可能达不到指定的上限,所以解析完之后要做一次收尾处理
  89.         if (!data.isEmpty()) {
  90.             this.businessHandler.accept(data);
  91.             // 更新读取到的总行数、批次数,以及清理集合辅助GC
  92.             rows += data.size();
  93.             batchNo++;
  94.             data.clear();
  95.         }
  96.     }
  97.     /*
  98.      * 关闭excel表头验证
  99.      **/
  100.     public void offValidate() {
  101.         this.validateSwitch = false;
  102.     }
  103.     public int getRows() {
  104.         return rows;
  105.     }
  106.     public int getBatchNo() {
  107.         return batchNo;
  108.     }
  109. }
复制代码
这个批量处理处罚监听器比上一个通用监听器多了些逻辑


  • 起首多了一个批次的概念,当分析的数据量到达给定量级时,就会触发businessHandler业务处理处罚器,而businessHandler则是在监听器初始化时传入的Consumer对象。
  • 当实行业务逻辑竣过后,会重新new一个新的聚集吸收数据,而旧聚集被断开引用关系后,会在短时间内被GC,这里不利用clear()方法的缘故起因,是由于clear()内部会去断开与每个元素的引用,这种方式会影响整个文件的读取性能。其次,当分析到的行数还未到达给定阈值时,会继续分析背面的数据,直到抵达阈值后重复前面的流程。
  • 这个分批处理处罚监听器,还重写了doAfterAllAnalysed()方法,这个方法会在整个文件分析完成后触发,内里实现的逻辑是为了做好收尾工作,由于末了一批大概达不到指定的上限,以是分析完之后还要视环境做一次处理处罚。不外这里用了clear()方法,毕竟这是末了一次收尾工作,后续也不必要新聚集来吸收数据了,直接整理现有聚集最符合。
该监听器还提供了两个API:


  • getRows():获取当前监听器分析的总行数;
  • getBatchNo():获取当前正在处理处罚的批次数(批次号)。
这两个方法返回的值,会随着excel文件不绝分析而不绝变革,当文件彻底分析完成后,调用这两个方法可以获取到总批次数以及数据总行数。
关于这个监听器,会在百万级大文件分析的实战中才会用到,先暂时相识即可。
二:封装excel导出工具类

上面listener是导入的时间用到的,那么如果是导出的时间呢?
  1. package com.example.bootrocketmq.study.wheel.easyexcel;
  2. import com.alibaba.excel.EasyExcelFactory;
  3. import com.alibaba.excel.support.ExcelTypeEnum;
  4. import com.alibaba.excel.write.builder.ExcelWriterBuilder;
  5. import com.alibaba.excel.write.metadata.style.WriteCellStyle;
  6. import com.alibaba.excel.write.metadata.style.WriteFont;
  7. import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
  8. import org.apache.poi.ss.usermodel.HorizontalAlignment;
  9. import org.apache.poi.ss.usermodel.IndexedColors;
  10. import javax.servlet.http.HttpServletResponse;
  11. import java.io.IOException;
  12. import java.net.URLEncoder;
  13. import java.util.List;
  14. /**
  15. * @author cui haida
  16. * 2024/12/22
  17. * 导出工具类
  18. */
  19. public class ExcelExportUtil {
  20.     /*
  21.      * 三种excel文件类型分别对应的响应头格式
  22.      **/
  23.     private static final String XLS_CONTENT_TYPE = "application/vnd.ms-excel";
  24.     private static final String XLSX_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
  25.     private static final String CSV_CONTENT_TYPE = "text/csv";
  26.     /*
  27.      * 导出excel的通用方法
  28.      * clazz -> 导出excel所需的数据模型类
  29.      * execlData -> 需要导出的数据列表
  30.      * filenName -> 当前导出的文件名称(不带文件后缀)
  31.      * execlType -> 导出的文件类型(xlsx, xls, csv)
  32.      * response -> 网络相应对象
  33.      **/
  34.     public static void exportExcel(Class<?> clazz, List<?> excelData, String fileName, ExcelTypeEnum excelType, HttpServletResponse response) throws IOException {
  35.         HorizontalCellStyleStrategy styleStrategy = setCellStyle();
  36.         // 对文件名进行UTF-8编码、拼接文件后缀名
  37.         fileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20") + excelType.getValue();
  38.         switch (excelType) {
  39.             case XLS:
  40.                 response.setContentType(XLS_CONTENT_TYPE);
  41.                 break;
  42.             case XLSX:
  43.                 response.setContentType(XLSX_CONTENT_TYPE);
  44.                 break;
  45.             case CSV:
  46.                 response.setContentType(CSV_CONTENT_TYPE);
  47.                 break;
  48.             default:
  49.         }
  50.         response.setCharacterEncoding("utf-8");
  51.         response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + fileName);
  52.         ExcelWriterBuilder writeWork = EasyExcelFactory.write(response.getOutputStream(), clazz);
  53.         writeWork.registerWriteHandler(styleStrategy).excelType(excelType).sheet().doWrite(excelData);
  54.     }
  55.     /*
  56.      * 设置单元格风格
  57.      **/
  58.     public static HorizontalCellStyleStrategy setCellStyle(){
  59.         // 设置表头的样式(背景颜色、字体、居中显示)
  60.         WriteCellStyle headStyle = new WriteCellStyle();
  61.         //设置表头的背景颜色
  62.         headStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
  63.         WriteFont headFont = new WriteFont();
  64.         headFont.setFontHeightInPoints((short)12);
  65.         headFont.setBold(true);
  66.         headStyle.setWriteFont(headFont);
  67.         headStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
  68.         // 设置Excel内容策略(水平居中)
  69.         WriteCellStyle cellStyle = new WriteCellStyle();
  70.         cellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
  71.         // 创建并返回样式策略对象
  72.         return new HorizontalCellStyleStrategy(headStyle, cellStyle);
  73.     }
  74. }
复制代码
接着基于response对象,设置了相应的数据范例、编码格式、文件名称等,末了就基于相应对象的输出流写出了天生的excel文件。
固然,通常大文件导出并不会实时返回流给调用方,而是返回OSS地点给前端去下载
  1. /**
  2. * @author cui haida
  3. * 方法编写日期: 2024/12/22
  4. * desc ->  上传文件到oss
  5. * @param clazz -> 导出excel所需的数据模型类
  6. * @param excelData -> 需要导出的数据列表
  7. * @param excelType -> excel的类型
  8. * @param fileName -> excel文件名称
  9. */
  10. public static String exportExcelToOSS(Class<?> clazz, List<?> excelData, String fileName, ExcelTypeEnum excelType) throws IOException {
  11.     HorizontalCellStyleStrategy styleStrategy = setCellStyle();
  12.     fileName = fileName + excelType.getValue();
  13.     File excelFile = File.createTempFile(fileName, excelType.getValue());
  14.     EasyExcelFactory.write(excelFile, clazz).registerWriteHandler(styleStrategy).sheet().doWrite(excelData);
  15.     String url = uploadFileToOss(excelFile);
  16.     if (excelFile.exists()) {
  17.         boolean flag = excelFile.delete();
  18.         log.info("删除临时文件是否成功:{}", flag);
  19.     }
  20.     return url;
  21. }
  22. /*
  23. * 模拟将上传OSS文件的代码(实际使用请替换为真实上传)
  24. * @param file 文件对象
  25. **/
  26. public static String uploadFileToOss(File file) {
  27.     String resultUrl = "";
  28.     // 省略上传至OSS的代码……
  29.     return resultUrl;
  30. }
复制代码
三:项目实践

1: excel导入并举行批量落库

为了更加的贴合业务环境,创建对应的数据库表举行测试
  1. CREATE TABLE `panda` (
  2.   `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  3.   `name` varchar(80) DEFAULT NULL COMMENT '名称',
  4.   `nickname` varchar(80) DEFAULT NULL COMMENT '外号',
  5.   `unique_code` varchar(20) DEFAULT NULL COMMENT '唯一编码',
  6.   `sex` tinyint(1) DEFAULT '2' COMMENT '性别,0:男,1:女,2:未知',
  7.   `height` decimal(6,2) DEFAULT NULL COMMENT '身高',
  8.   `birthday` date DEFAULT NULL COMMENT '出生日期',
  9.   `pic` varchar(255) DEFAULT NULL COMMENT '头像地址',
  10.   `level` varchar(50) DEFAULT '0' COMMENT '等级',
  11.   `motto` varchar(255) DEFAULT NULL COMMENT '座右铭',
  12.   `address` varchar(255) DEFAULT NULL COMMENT '所在地址',
  13.   `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  14.   `is_deleted` tinyint(1) DEFAULT '0' COMMENT '删除标识,0:正常,1:删除',
  15.   PRIMARY KEY (`id`) USING BTREE
  16. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='熊猫表';
复制代码
这是一张拥有十三个字段的熊猫表,下面初始化一些数据:
  1. insert into panda(name, nickname, unique_code, sex, height, birthday, pic, level, motto, address, create_time) values
  2. ("竹子", "小竹", "P888888", 0, '179.99', '2017-07-07', NULL, "高级", "十年磨一剑,五年磨半边!", "太阳省银河市地球村888号", now()),
  3. ("花花", "阿花", "P666666", 1, '155.00', '2016-06-07', NULL, "顶级", "我爱睡觉爱我!", "太阳省银河市地球村666号", now()),
  4. ("甜甜", "肥肥", "P999999", 0, '163.43', '2020-02-02', NULL, "特级", "今天的事能拖就拖,明天的事明天再说!", "太阳省银河市地球村999号", now()),
  5. ("子竹", "小子", "P555555", 1, '188.88', '2021-08-08', NULL, "初级", "你小子!", "太阳省银河市地球村555号", now());
复制代码
前面封装了通用监听器,下面就来试试结果,先来写一个最根本的excel导入接口,对应的导入模板如下:

起首来界说一个与excel模板匹配的数据模子类:
  1. package org.example.myeasyexcel.model.excel;
  2. import com.alibaba.excel.annotation.ExcelProperty;
  3. import com.alibaba.excel.annotation.format.DateTimeFormat;
  4. import lombok.Data;
  5. import java.io.Serializable;
  6. import java.math.BigDecimal;
  7. import java.util.Date;
  8. /**
  9. * @author cui haida
  10. * 2024/12/23
  11. * 熊猫导出类
  12. */
  13. @Data
  14. public class PandaReadModel implements Serializable {
  15.     private static final long serialVersionUID = 1L;
  16.     @ExcelProperty("名称")
  17.     private String name;
  18.     @ExcelProperty("外号")
  19.     private String nickname;
  20.     @ExcelProperty("唯一编码")
  21.     private String uniqueCode;
  22.     @ExcelProperty("性别")
  23.     private String sex;
  24.     @ExcelProperty("身高")
  25.     private BigDecimal height;
  26.     @ExcelProperty("出生日期")
  27.     @DateTimeFormat("yyyy-MM-dd")
  28.     private Date birthday;
  29.     @ExcelProperty("等级")
  30.     private String level;
  31.     @ExcelProperty("座右铭")
  32.     private String motto;
  33.     @ExcelProperty("所在地址")
  34.     private String address;
  35. }
复制代码
这是一个尺度的Java类,没有任何特别的地方,下面再来编写service层的实现逻辑:
  1. @Override
  2. @Transactional(rollbackFor = Exception.class)
  3. public void importExcelV1(MultipartFile file) {
  4.     // 创建通用监听器来解析excel文件
  5.     CommonListener<PandaReadModel> listener = new CommonListener<>(PandaReadModel.class);
  6.     try {
  7.         // 从excel模板中读取数据
  8.         EasyExcelFactory.read(file.getInputStream(), PandaReadModel.class, listener).sheet().doRead();
  9.     } catch (IOException e) {
  10.         log.error("导入熊猫数据出错:{}: {}", e, e.getMessage());
  11.         throw new BusinessException(ResponseCode.ANALYSIS_EXCEL_ERROR, "网络繁忙,请稍后重试!");
  12.     }
  13.     // 对读取到的数据进行批量保存
  14.     List<PandaReadModel> excelData = listener.getData();
  15.     if (excelData.isEmpty()) {
  16.         throw new BusinessException(ResponseCode.ANALYSIS_EXCEL_ERROR, "请检查您上传的excel文件是否为空!");
  17.     }
  18.     batchSaveExcelData(excelData);
  19. }
  20. @Override
  21. @Transactional(rollbackFor = Exception.class)
  22. public void batchSaveExcelData(List<PandaReadModel> excelData) {
  23.     List<Panda> pandas = excelData.stream().map(model -> {
  24.         Panda panda = new Panda();
  25.         BeanUtils.copyProperties(model, panda, "sex");
  26.         panda.setSex(Sex.codeOfValue(model.getSex()));
  27.         panda.setCreateTime(new Date());
  28.         return panda;
  29.     }).collect(Collectors.toList());
  30.     saveBatch(pandas);
  31. }
复制代码
接着界说controller层
  1. @RestController
  2. @RequestMapping("/panda")
  3. public class PandaController {
  4.   @Resource
  5.   private PandaService pandaService;
  6.   @PostMapping("/import/v1")
  7.   public ServerResponse<Void> importExcelV1(MultipartFile file) {
  8.     if (null == file) {
  9.       throw new BusinessException(ResponseCode.FILE_IS_NULL);
  10.     }
  11.     pandaService.importExcelV1(file);
  12.     return ServerResponse.success();
  13.   }
  14. }
复制代码
调用外部接口就可以发现已经乐成的读取了数据并

2:根据检索条件导出excel

导出也非常的容易,先界说导出模子类:
  1. package org.example.myeasyexcel.model.vo;
  2. import com.alibaba.excel.annotation.ExcelIgnore;
  3. import com.alibaba.excel.annotation.ExcelProperty;
  4. import com.alibaba.excel.annotation.format.DateTimeFormat;
  5. import com.alibaba.excel.annotation.write.style.ColumnWidth;
  6. import lombok.Data;
  7. import org.example.myeasyexcel.converter.SexConverter;
  8. import java.io.Serializable;
  9. import java.math.BigDecimal;
  10. import java.util.Date;
  11. /**
  12. * @author cui haida
  13. * 2024/12/22
  14. */
  15. @Data
  16. @ColumnWidth(10)
  17. public class PandaExportVO implements Serializable {
  18.     private static final long serialVersionUID = 1L;
  19.     @ExcelIgnore
  20.     private Long id;
  21.     @ExcelProperty(value = "熊猫昵称", index = 0)
  22.     private String nickname;
  23.     @ExcelProperty(value = "性别", index = 1, converter = SexConverter.class)
  24.     private Integer sex;
  25.     @ExcelProperty(value = "唯一编码", index = 2)
  26.     private String uniqueCode;
  27.     @ExcelProperty(value = "身高", index = 3)
  28.     private BigDecimal height;
  29.     @ExcelProperty(value = "出生日期", index = 4)
  30.     @DateTimeFormat("yyyy-MM-dd")
  31.     @ColumnWidth(15)
  32.     private Date birthday;
  33.     @ExcelProperty(value = "创建时间", index = 5)
  34.     @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
  35.     @ColumnWidth(20)
  36.     private Date createTime;
  37. }
复制代码
固然,如果导入、导出的字段类似,你可以利用同一个类,不外个人发起是最好分开
下面再来界说一个查询条件参数类:
  1. package org.example.myeasyexcel.model.dto;
  2. import lombok.Data;
  3. import java.io.Serializable;
  4. /**
  5. * @author cui haida
  6. * 2024/12/23
  7. */
  8. @Data
  9. public class PandaQueryDTO implements Serializable {
  10.     private static final long serialVersionUID = 1L;
  11.     /*
  12.      * 搜索关键字(兼容名称、外号、编码三个条件)
  13.      */
  14.     private String keyword;
  15.     /*
  16.      * 生日开始时间(yyyy-MM-dd格式)
  17.      */
  18.     private String startTime;
  19.     /*
  20.      * 生日结束时间(yyyy-MM-dd格式)
  21.      */
  22.     private String endTime;
  23. }
复制代码
这个DTO类中,统共有三个字段,支持名称、外号、编码的暗昧搜刮,以及基于生日按范围查询熊猫数据,接着来看service层:
  1. /**
  2. * 根据条件导出熊猫信息到Excel
  3. *
  4. * @param queryDTO 包含查询条件的PandaQueryDTO对象,用于筛选要导出的熊猫信息
  5. * @param response HttpServletResponse对象,用于将Excel文件作为HTTP响应返回
  6. *
  7. * 本方法首先根据查询条件从数据库中筛选出相应的熊猫信息,然后利用ExcelUtil工具类
  8. * 将这些信息导出为Excel文件,并通过HTTP响应返回给客户端如果导出过程中发生IO异常,
  9. * 则记录错误日志日志并抛出业务异常,提示导出失败
  10. */
  11. @Override
  12. public void exportExcelByCondition(PandaQueryDTO queryDTO, HttpServletResponse response) {
  13.     // 通过查询条件选择出对应的熊猫
  14.     List<PandaExportVO> pandas = baseMapper.selectPandas(queryDTO);
  15.     // 生成唯一文件名,防止重复
  16.     String fileName = "熊猫基本信息集合-" + System.currentTimeMillis();
  17.     try {
  18.         // 通过工具方法进行导入
  19.         ExcelUtil.exportExcel(PandaExportVO.class, pandas, fileName, ExcelTypeEnum.XLSX, response);
  20.     } catch (IOException e) {
  21.         // 记录异常日志日志
  22.         log.error("熊猫数据导出失败,{}:{}", e, e.getMessage());
  23.         // 抛出业务异常,提示用户导出失败
  24.         throw new BusinessException("熊猫基本信息导出失败,请稍后再试!");
  25.     }
  26. }
复制代码
对应的工具方法在上面已经分析,这里在形貌一遍
  1. package org.example.myeasyexcel.util;
  2. import com.alibaba.excel.EasyExcelFactory;
  3. import com.alibaba.excel.ExcelWriter;
  4. import com.alibaba.excel.annotation.ExcelIgnore;
  5. import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
  6. import com.alibaba.excel.annotation.ExcelProperty;
  7. import com.alibaba.excel.exception.ExcelAnalysisException;
  8. import com.alibaba.excel.support.ExcelTypeEnum;
  9. import com.alibaba.excel.write.builder.ExcelWriterBuilder;
  10. import com.alibaba.excel.write.metadata.style.WriteCellStyle;
  11. import com.alibaba.excel.write.metadata.style.WriteFont;
  12. import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
  13. import lombok.extern.slf4j.Slf4j;
  14. import org.apache.poi.ss.usermodel.HorizontalAlignment;
  15. import org.apache.poi.ss.usermodel.IndexedColors;
  16. import org.example.myeasyexcel.enums.ExcelTemplate;
  17. import javax.servlet.http.HttpServletResponse;
  18. import java.io.File;
  19. import java.io.IOException;
  20. import java.io.UnsupportedEncodingException;
  21. import java.lang.reflect.Field;
  22. import java.net.URLEncoder;
  23. import java.util.*;
  24. /**
  25. * @author cui haida
  26. * 2024/12/23
  27. */
  28. @Slf4j
  29. public class ExcelUtil {
  30.     /*
  31.      * 三种excel文件类型分别对应的响应头格式
  32.      * */
  33.     private static final String XLS_CONTENT_TYPE = "application/vnd.ms-excel";
  34.     private static final String XLSX_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
  35.     private static final String CSV_CONTENT_TYPE = "text/csv";
  36.     /*
  37.      * 导出excel的通用方法
  38.      *   clazz:传入excel导出的VO类
  39.      *   data:传入需要导出的数据列表
  40.      *   fileName:当前导出的excel文件名称(不带文件后缀)
  41.      *   excelType:导出的文件类型
  42.      *   response:网络响应对象
  43.      * */
  44.     public static void exportExcel(Class<?> clazz, List<?> excelData, String fileName, ExcelTypeEnum excelType, HttpServletResponse response) throws IOException {
  45.         HorizontalCellStyleStrategy styleStrategy = setCellStyle();
  46.         setResponse(fileName, excelType, response);
  47.         ExcelWriterBuilder writeWork = EasyExcelFactory.write(response.getOutputStream(), clazz);
  48.         writeWork.registerWriteHandler(styleStrategy).excelType(excelType).sheet().doWrite(excelData);
  49.     }
  50.     /*
  51.      * 初始化模板填充导出场景的写对象
  52.      * */
  53.     public static ExcelWriter initExportFillWriter(String fileName, ExcelTypeEnum excelType, ExcelTemplate template, HttpServletResponse response) throws IOException {
  54.         setResponse(fileName, excelType, response);
  55.         return EasyExcelFactory.write(response.getOutputStream())
  56.                 .excelType(excelType)
  57.                 .withTemplate(template.getPath()).build();
  58.     }
  59.     /*
  60.      * 设置通用的响应头信息
  61.      * */
  62.     public static void setResponse(String fileName, ExcelTypeEnum excelType, HttpServletResponse response) {
  63.         // 对文件名进行UTF-8编码、拼接文件后缀名
  64.         try {
  65.             fileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20") + excelType.getValue();
  66.         } catch (UnsupportedEncodingException e) {
  67.             throw new RuntimeException(e);
  68.         }
  69.         switch (excelType) {
  70.             case XLS:
  71.                 response.setContentType(XLS_CONTENT_TYPE);
  72.                 break;
  73.             case XLSX:
  74.                 response.setContentType(XLSX_CONTENT_TYPE);
  75.                 break;
  76.             case CSV:
  77.                 response.setContentType(CSV_CONTENT_TYPE);
  78.                 break;
  79.         }
  80.         response.setCharacterEncoding("utf-8");
  81.         response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + fileName);
  82.     }
  83.     /*
  84.      * 设置单元格风格
  85.      * */
  86.     public static HorizontalCellStyleStrategy setCellStyle(){
  87.         // 设置表头的样式(背景颜色、字体、居中显示)
  88.         WriteCellStyle headStyle = new WriteCellStyle();
  89.         //设置表头的背景颜色
  90.         headStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
  91.         WriteFont headFont = new WriteFont();
  92.         headFont.setFontHeightInPoints((short)12);
  93.         headFont.setBold(true);
  94.         headStyle.setWriteFont(headFont);
  95.         headStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
  96.         // 设置Excel内容策略(水平居中)
  97.         WriteCellStyle cellStyle = new WriteCellStyle();
  98.         cellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
  99.         return new HorizontalCellStyleStrategy(headStyle, cellStyle);
  100.     }
  101.     /*
  102.      * 上传错误的Excel文件到OSS
  103.      * */
  104.     public static String exportExcelToOSS(Class<?> clazz, List<?> excelData, String fileName, ExcelTypeEnum excelType) throws IOException {
  105.         HorizontalCellStyleStrategy styleStrategy = ExcelUtil.setCellStyle();
  106.         fileName = fileName + excelType.getValue();
  107.         File excelFile = File.createTempFile(fileName, excelType.getValue());
  108.         EasyExcelFactory.write(excelFile, clazz).registerWriteHandler(styleStrategy).sheet().doWrite(excelData);
  109.         String url = uploadFileToOss(excelFile);
  110.         if (excelFile.exists()) {
  111.             boolean flag = excelFile.delete();
  112.             log.info("删除临时文件是否成功:{}", flag);
  113.         }
  114.         return url;
  115.     }
  116.     /*
  117.      * 模拟将上传OSS文件的代码(实际使用请替换为真实上传)
  118.      * */
  119.     public static String uploadFileToOss(File file) {
  120.         String resultUrl = "https://juejin.cn/user/862486453028888/posts";
  121.         // 省略上传至OSS的代码……
  122.         return resultUrl;
  123.     }
  124.     /*
  125.      * 校验excel文件的表头,与数据模型类的映射关系是否匹配
  126.      * */
  127.     public static void validateExcelTemplate(Map<Integer, String> headMap, Class<?> clazz, Field[] fields) {
  128.         Collection<String> headNames = headMap.values();
  129.         // 类上是否存在忽略excel字段的注解
  130.         boolean classIgnore = clazz.isAnnotationPresent(ExcelIgnoreUnannotated.class);
  131.         int count = 0;
  132.         // 如果EasyExcel框架版本较低,请把这行代码放开,并将有效的字段列表返回,用于空值校验
  133.         // List<Field> validFields = new ArrayList<>(fields.length);
  134.         for (Field field : fields) {
  135.             // 忽略序列化ID字段
  136.             if ("serialVersionUID".equals(field.getName())) {
  137.                 continue;
  138.             }
  139.             // 如果字段上存在忽略注解,则跳过当前字段
  140.             if (field.isAnnotationPresent(ExcelIgnore.class)) {
  141.                 continue;
  142.             }
  143.             ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
  144.             if (null == excelProperty) {
  145.                 // 如果类上也存在忽略注解,则跳过所有未使用ExcelProperty注解的字段
  146.                 if (classIgnore) {
  147.                     continue;
  148.                 }
  149.                 // 如果检测到既未忽略、又未映射excel列的字段,则抛出异常提示模板不正确
  150.                 throw new ExcelAnalysisException("请检查导入的excel文件是否按模板填写!");
  151.             }
  152.             // 校验数据模型类上绑定的名称,是否与excel列名匹配
  153.             String[] value = excelProperty.value();
  154.             String name = value[0];
  155.             if (name != null && 0 != name.length() && !headNames.contains(name)) {
  156.                 throw new ExcelAnalysisException("请检查导入的excel文件是否按模板填写!");
  157.             }
  158.             // 更新有效字段的数量
  159.             count++;
  160.             // validFields.add(field);
  161.         }
  162.         // 最后校验数据模型类的有效字段数量,与读到的excel列数量是否匹配
  163.         if (headMap.size() != count) {
  164.             throw new ExcelAnalysisException("请检查导入的excel文件是否按模板填写!");
  165.         }
  166.     }
  167.     /**
  168.      * 判断整行单元格数据是否均为空(依赖于validateExcelTemplate()方法返回的有效字段列表)
  169.      * 说明:该方法适用于低版本的EasyExcel读取数据时校验,因为低版本的不会自动忽略空行
  170.      */
  171.     public static <T> boolean rowIsNull(T data, List<Field> validFields) {
  172.         if (data instanceof String) {
  173.             return "".equals(data);
  174.         }
  175.         try {
  176.             List<Boolean> fieldNulls = new ArrayList<>(validFields.size());
  177.             for (Field field : validFields) {
  178.                 field.setAccessible(true);
  179.                 Object value = field.get(data);
  180.                 if (Objects.isNull(value)) {
  181.                     fieldNulls.add(Boolean.TRUE);
  182.                 } else {
  183.                     fieldNulls.add(Boolean.FALSE);
  184.                 }
  185.             }
  186.             return fieldNulls.stream().allMatch(Boolean.TRUE::equals);
  187.         } catch (Exception e) {
  188.             log.error("读取数据行[{}]解析失败: {}", data, e.getMessage());
  189.         }
  190.         return true;
  191.     }
  192. }
复制代码
末了编写controller层,就是简朴的对service层举行调用
  1. @PostMapping("/export/v1")
  2. public void exportExcelV1(@RequestBody PandaQueryDTO queryDTO, HttpServletResponse response) {
  3.     pandaService.exportExcelByCondition(queryDTO, response);
  4. }
复制代码

3:校验写入的excel的数据


在有些场景中,我们必要对导入的excel数据举行校验洗濯后才气利用
如果上传的excel文件存在校验堕落的信息,则需像上图一样指明错误缘故起因,末了回传给调用方下载检察,这该怎么实现呢?如下:
  1. @Override
  2. @Transactional(rollbackFor = Exception.class)
  3. public BaseImportExcelVO<String> importExcelV2(MultipartFile file) {
  4.     CommonListener<PandaReadModel> listener = new CommonListener<>(PandaReadModel.class);
  5.     try {
  6.         EasyExcelFactory.read(file.getInputStream(), PandaReadModel.class, listener).sheet().doRead();
  7.     } catch (IOException e) {
  8.         log.error("导入熊猫数据出错:{}: {}", e, e.getMessage());
  9.         throw new BusinessException(ResponseCode.ANALYSIS_EXCEL_ERROR, "网络繁忙,请稍后重试!");
  10.     }
  11.     List<PandaReadModel> excelData = listener.getData();
  12.     if (excelData.isEmpty()) {
  13.         throw new BusinessException(ResponseCode.ANALYSIS_EXCEL_ERROR, "请检查您上传的excel文件是否为空!");
  14.     }
  15.     // 校验excel数据,如果校验未通过直接阻断执行,将错误信息返回给调用方
  16.     BaseImportExcelVO<String> result = validateExcelData(excelData);
  17.     if (result.getErrorFlag()) {
  18.         return result;
  19.     }
  20.     // 对校验通过的数据进行批量落库
  21.     batchSaveExcelData(excelData);
  22.     return result;
  23. }
  24. /*
  25. * 校验导入的excel数据
  26. * */
  27. private BaseImportExcelVO<String> validateExcelData(List<PandaReadModel> excelData) {
  28.     BaseImportExcelVO<String> result = new BaseImportExcelVO<>();
  29.     boolean errorFlag = false;
  30.     List<PandaReadErrorModel> validatePandas = new ArrayList<>();
  31.     String birthdayErrorMsg = "生日不能为空;", uniCodeErrorMsg = "唯一编码重复;";
  32.     // 根据唯一编码查询库内的熊猫数
  33.     List<String> uniCodes = excelData.stream().map(PandaReadModel::getUniqueCode).collect(Collectors.toList());
  34.     List<CountVO<String, Integer>> counts = baseMapper.selectCountByUniCodes(uniCodes);
  35.     Map<String, Integer> countMap = counts.stream().collect(Collectors.toMap(CountVO::getKey, CountVO::getValue));
  36.     // 循环对excel所有数据行进行校验
  37.     for (PandaReadModel excelRow : excelData) {
  38.         String errorMsg = "";
  39.         PandaReadErrorModel errorModel = new PandaReadErrorModel();
  40.         BeanUtils.copyProperties(excelRow, errorModel);
  41.         // 如果库里对应的唯一编码能查到熊猫,说明UniqueCode重复
  42.         if (countMap.containsKey(excelRow.getUniqueCode())) {
  43.             errorMsg += uniCodeErrorMsg;
  44.             errorFlag = true;
  45.         }
  46.         // 如果导入的生日字段为空,说明对应的excel行没填写出生日期
  47.         if (null == excelRow.getBirthday()) {
  48.             errorMsg += birthdayErrorMsg;
  49.             errorFlag = true;
  50.         }
  51.         errorModel.setErrorMsg(errorMsg);
  52.         validatePandas.add(errorModel);
  53.     }
  54.     // 如果存在校验未通过的记录,则导出校验出错的数据为excel文件
  55.     if (errorFlag) {
  56.         String url, fileName = "熊猫信息导入-校验出错文件-" + System.currentTimeMillis();
  57.         try {
  58.             url = ExcelUtil.exportExcelToOSS(PandaReadErrorModel.class, validatePandas, fileName, ExcelTypeEnum.XLSX);
  59. //                ExcelUtil.exportExcel(PandaReadErrorModel.class, validatePandas, fileName, ExcelTypeEnum.XLSX, response);
  60.         } catch (IOException e) {
  61.             log.error("生成熊猫导入校验出错文件失败:{}: {}", e, e.getMessage());
  62.             throw new BusinessException(ResponseCode.ANALYSIS_EXCEL_ERROR, "网络繁忙,请稍后重试!");
  63.         }
  64.         result.setResult(url);
  65.         result.setCheckMsg("文件校验未通过!");
  66.     }
  67.     result.setErrorFlag(errorFlag);
  68.     return result;
  69. }
复制代码
4:部分字段差别的导出场景


来看这个场景,正如上图所示,现在有两个导出需求,可仅仅只有第一列字段差别罢了,这时我们必要界说两个接口吗?答案是不必要,面临这种大多数字段类似、部分字段差别的场景,可以基于多态的特性来实现,起首界说一个父类来声明公共字段:
  1. @Data
  2. public class PandaStatisticsExportVO implements Serializable {
  3.     private static final long serialVersionUID = 1L;
  4.     @ExcelProperty(index = 1, value = "熊数")
  5.     private Integer counting;
  6.     @ExcelProperty(index = 2, value = "身高>=170cm熊数")
  7.     private Integer heightGte170cm;
  8. }
复制代码
接着分别为两个差别的场景界说子类,即:
  1. /**
  2. * 熊猫统计导出VO类(根据年龄分组)
  3. */
  4. @Data
  5. @EqualsAndHashCode(callSuper = true)
  6. public class SexStatisticsExportVO extends PandaStatisticsExportVO implements Serializable {
  7.     private static final long serialVersionUID = 1L;
  8.     @ExcelProperty(index = 0, value = "性别分组", converter = SexConverter.class)
  9.     private Integer sex;
  10. }
  11. /**
  12. * 熊猫统计导出VO类(根据等级分组)
  13. */
  14. @Data
  15. @EqualsAndHashCode(callSuper = true)
  16. public class LevelStatisticsExportVO extends PandaStatisticsExportVO implements Serializable {
  17.   private static final long serialVersionUID = 1L;
  18.   @ExcelProperty(index = 0, value = "等级分组")
  19.   private String level;
  20. }
复制代码
这是面向对象语言的尺度写法,值得一提的是:这里公道的利用了ExcelProperty注解的index属性,由于两个表格中,存在差别的字段就是第一列,以是两个子类的差别字段的列索引写成0即可。
下面来看详细的service实现:
  1. @Override
  2. public void exportStatisticsData(PandaStatisticsDTO statisticsDTO, HttpServletResponse response) {
  3.     List<PandaStatisticsBO> pandaStatisticsBOs = baseMapper.selectPandaStatistics(statisticsDTO);
  4.     List<PandaStatisticsExportVO> exportData;
  5.     Class<?> clazz;
  6.     // 如果是按性别分组统计,则使用性别的数据模型类
  7.     if (0 == statisticsDTO.getStatisticsType()) {
  8.         clazz = SexStatisticsExportVO.class;
  9.         exportData = pandaStatisticsBOs.stream().map(bo -> {
  10.             SexStatisticsExportVO sexVO = new SexStatisticsExportVO();
  11.             BeanUtils.copyProperties(bo, sexVO);
  12.             return sexVO;
  13.         }).collect(Collectors.toList());
  14.     }
  15.     // 如果是按等级分组统计,则使用等级的数据模型类
  16.     else if (1 == statisticsDTO.getStatisticsType()) {
  17.         clazz = LevelStatisticsExportVO.class;
  18.         exportData = pandaStatisticsBOs.stream().map(bo -> {
  19.             LevelStatisticsExportVO levelVO = new LevelStatisticsExportVO();
  20.             BeanUtils.copyProperties(bo, levelVO);
  21.             return levelVO;
  22.         }).collect(Collectors.toList());
  23.     } else {
  24.         throw new BusinessException("暂不支持这种统计方式哦~");
  25.     }
  26.     // 导出对应的excel数据
  27.     String fileName = "熊猫统计数据-" + System.currentTimeMillis();
  28.     try {
  29.         ExcelUtil.exportExcel(clazz, exportData, fileName, ExcelTypeEnum.XLSX, response);
  30.     } catch (IOException e) {
  31.         log.error("熊猫统计数据导出失败,{}:{}", e, e.getMessage());
  32.         throw new BusinessException("统计数据导出失败,请稍后再试!");
  33.     }
  34. }
复制代码
代码仍旧非常简朴,起首会根据参数里传入的导出范例,来选择要利用的数据模子类:


  • 0:代表按性别统计数据,利用SexStatisticsExportVO模子类;
  • 1:代表按品级统计数据,利用LevelStatisticsExportVO模子类。
5:多行头的excel导出


有些业务场景下,我们必要导出像上图如许的归并表头文件,即一个大列下面有多个子列,整个文件由多个大列+N多个子列构成,这种需求该怎么导出呢?
实在非常简朴,只靠ExcelProperty注解就能实现,如下:
  1. @Data
  2. public class MultiLineHeadExportVO implements Serializable {
  3.     private static final long serialVersionUID = 1L;
  4.     @ExcelProperty(value = {"基本信息", "昵称"})
  5.     private String nickname;
  6.     @ExcelProperty(value = {"基本信息", "编码"})
  7.     private String uniqueCode;
  8.     @ExcelProperty(value = {"基本信息","性别"}, converter = SexConverter.class)
  9.     private Integer sex;
  10.     @ExcelProperty(value = {"基本信息","身高"})
  11.     private BigDecimal height;
  12.     @ExcelProperty(value = {"基本信息","出生日期"})
  13.     @DateTimeFormat("yyyy-MM-dd")
  14.     @ColumnWidth(15)
  15.     private Date birthday;
  16.     @ExcelProperty(value = {"其他信息","等级"})
  17.     private String level;
  18.     @ExcelProperty(value = {"其他信息","座右铭"})
  19.     @ColumnWidth(30)
  20.     private String motto;
  21.     @ExcelProperty(value = {"其他信息", "创建时间"})
  22.     @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
  23.     @ColumnWidth(20)
  24.     private Date createTime;
  25. }
复制代码
正如代码所示,如果你想要将某几列归并成一个大列,只需在注解的value属性中,以数组情势通报类似的大列名即可。
而EasyExcel发现列名是数组情势时,就会实行主动归并类似列表的字段,而导出代码无需做任何改造:
  1. @Override
  2. public void exportMultiLineHeadExcel(HttpServletResponse response) {
  3.     List<MultiLineHeadExportVO> pandas = baseMapper.selectAllPandas();
  4.     String fileName = "多行表头熊猫数据-" + System.currentTimeMillis();
  5.     try {
  6.         ExcelUtil.exportExcel(MultiLineHeadExportVO.class, pandas, fileName, ExcelTypeEnum.XLSX, response);
  7.     } catch (IOException e) {
  8.         log.error("多行表头熊猫数据,{}:{}", e, e.getMessage());
  9.         throw new BusinessException("数据导出失败,请稍后再试!");
  10.     }
  11. }
复制代码
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表