Alibaba EasyExcel 导入导出全家桶

悠扬随风  金牌会员 | 2024-12-10 00:40:32 | 来自手机 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 988|帖子 988|积分 2964

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

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

x


一、阿里巴巴EasyExcel的上风

        首先说下EasyExcel相对 Apache poi的上风:
        EasyExcel也是阿里研发在poi基础上做了封装,改进产物。它替开发者做了注解列表分析,表格添补等一系列代码编写工作,并将此抽象成通用和可扩展的框架。相对poi,在数据量比较大的时候,它有着更精良的性能表现。导出的时候,easyexcel使用优化的反射技术,制止poi频仍的去创建cell和row对象;导入的时候,它的分析器AnalysisEventListener,可设置批量阈值 BATCH_COUNT,达到阈值就往数据库插入数据,然后清空分析器内部缓存,相同的表格,easyexcel导入所占用的内存要比poi节流90%,制止了大数据量导入的时候,造成的内存占用井喷(这使得stop the world的时间可能会被集中,而体系可能会出现短暂的停摆。),而GC不能均衡变动垃圾回收。同时也制止堆积数据后,sql的巨量数据的批量插入,导致超出mybatis批量插入语句能蒙受的最大长度限制。

二、EasyExcel核心util类


  1. @Slf4j
  2. public class EasyExcels {
  3.     public static final String EXT_NAME_XLSX = "xlsx";
  4.     public static final String EXT_NAME_XLS = "xls";
  5.     /**
  6.      *
  7.      * @param response
  8.      * @param data
  9.      * @param filename
  10.      * @param sheetName
  11.      * @param selectMap  自定义下拉列,但是既然数据都导出了,下拉用处何在?这个需求比较少
  12.      * @param <T>
  13.      * @throws IOException
  14.      */
  15.     public static <T> void write(HttpServletResponse response, List<T> data, String filename, String sheetName,
  16.                                  List<KeyValue<ExcelColumn, List<String>>> selectMap) throws IOException {
  17.         setResponse(response, filename);
  18.         if (StringUtils.isBlank(sheetName)) {
  19.             sheetName = filename;
  20.         }
  21.         // 输出 Excel
  22.         try {
  23.             EasyExcel.write(response.getOutputStream(), data != null && !data.isEmpty() ? data.get(0).getClass() : null)
  24.                     .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度
  25.                     .registerWriteHandler(new CustomCellWriteWeightConfig()) // Excel 列宽自适应
  26.                     .registerWriteHandler(EasyExcelStyle.horizontalCellStyleStrategy) //内容样式
  27.                     .registerWriteHandler(new SelectWriteHandler(selectMap)) // 基于固定 sheet 实现下拉框
  28.                     .sheet(sheetName).doWrite(data);
  29.         } catch (Exception e) {
  30.             e.printStackTrace();
  31.         } finally {
  32.             response.getOutputStream().close();
  33.         }
  34.     }
  35.     // 简单导入读取,不做解析,不做校验
  36.     public static <T> List<T> read(MultipartFile file, Class<T> head) throws IOException {
  37.         return EasyExcel.read(file.getInputStream(), head, null)
  38.                 .autoCloseStream(false)  // 不要自动关闭,交给 Servlet 自己处理
  39.                 .doReadAllSync();
  40.     }
  41.     // 需要配合监听器解析数据
  42.     public static <T> void read(MultipartFile file, Class<T> head, ReadListener<T> listener) throws IOException {
  43.         EasyExcel.read(file.getInputStream(), head, listener)
  44.                 .sheet()
  45.                 .doRead();
  46.     }
  47.     // 不带下拉列的导出,用的比较多
  48.     public static <T> void export(HttpServletResponse response, List<T> data, String filename, String sheetName) throws IOException {
  49.         setResponse(response, filename);
  50.         if (StringUtils.isBlank(sheetName)) {
  51.             sheetName = filename;
  52.         }
  53.         EasyExcel.write(response.getOutputStream(), data != null && !data.isEmpty() ? data.get(0).getClass() : null)
  54.                 .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
  55.                 .registerWriteHandler(new CustomCellWriteWeightConfig())
  56.                 .registerWriteHandler(EasyExcelStyle.horizontalCellStyleStrategy)
  57.                 .sheet(sheetName).doWrite(data);
  58.     }
  59.    // 用于合并单元格列的导出
  60.     public static <T> void export(HttpServletResponse response, List<T> data, String filename, String sheetName, RowWriteHandler handler) throws IOException {
  61.         setResponse(response, filename);
  62.         EasyExcel.write(response.getOutputStream(), data != null && !data.isEmpty() ? data.get(0).getClass() : null)
  63.                 .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
  64.                 .registerWriteHandler(new CustomCellWriteWeightConfig())
  65.                 .registerWriteHandler(EasyExcelStyle.horizontalCellStyleStrategy)
  66.                 .registerWriteHandler(handler)
  67.                 .sheet(sheetName).doWrite(data);
  68.     }
  69.    // 用于导出表头模板,填充导入数据用的excel模板,因为是模板,所以肯定会有下拉列的需求
  70.     public static <T> void export(HttpServletResponse response, Class<T> clazz, String filename) throws IOException {
  71.         setResponse(response, filename);
  72.         Map<Integer, ExcelSelectedResolve> selectedMap = resolveSelectedAnnotation(clazz);
  73.         EasyExcel.write(response.getOutputStream(), clazz)
  74.                 .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
  75. //                .registerWriteHandler(new CustomCellWriteHeightConfig())
  76.                 .registerWriteHandler(new CustomCellWriteWeightConfig())
  77.                 .registerWriteHandler(EasyExcelStyle.horizontalCellStyleStrategy)
  78.                 .registerWriteHandler(new SelectSheetWriteHandler(selectedMap))
  79.                 .sheet(filename).doWrite(Collections.emptyList());
  80.     }
  81.     /**
  82.      * 解析表头类中的下拉注解
  83.      * @param head 表头类
  84.      * @param <T> 泛型
  85.      * @return Map<下拉框列索引, 下拉框内容> map
  86.      */
  87.     private static <T> Map<Integer, ExcelSelectedResolve> resolveSelectedAnnotation(Class<T> head) {
  88.         Map<Integer, ExcelSelectedResolve> selectedMap = new HashMap<>();
  89.         // getDeclaredFields(): 返回全部声明的属性;getFields(): 返回public类型的属性
  90.         Field[] fields = head.getDeclaredFields();
  91.         for (int i = 0; i < fields.length; i++){
  92.             Field field = fields[i];
  93.             // 解析注解信息
  94.             ExcelSelected selected = field.getAnnotation(ExcelSelected.class);
  95.             ExcelProperty property = field.getAnnotation(ExcelProperty.class);
  96.             if (selected != null) {
  97.                 ExcelSelectedResolve excelSelectedResolve = new ExcelSelectedResolve();
  98.                 String[] source = excelSelectedResolve.resolveSelectedSource(selected);
  99.                 if (source != null && source.length > 0){
  100.                     excelSelectedResolve.setSource(source);
  101.                     excelSelectedResolve.setFirstRow(selected.firstRow());
  102.                     excelSelectedResolve.setLastRow(selected.lastRow());
  103.                     if (property != null && property.index() >= 0){
  104.                         selectedMap.put(property.index(), excelSelectedResolve);
  105.                     } else {
  106.                         selectedMap.put(i, excelSelectedResolve);
  107.                     }
  108.                 }
  109.             }
  110.         }
  111.         return selectedMap;
  112.     }
  113.     public static void setResponse(HttpServletResponse response, String filename) throws IOException {
  114.         setResponse(response, filename, EXT_NAME_XLSX);
  115.     }
  116.     public static void setResponse(HttpServletResponse response, String filename, String extName) throws IOException {
  117.         String exportFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8.name());
  118.         response.setContentType("application/vnd.ms-excel");
  119.         response.setCharacterEncoding("utf-8");
  120.         response.setHeader("Access-Control-Expose-Headers", "token,Content-Type,Content-disposition");
  121.         response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Connection, User-Agent, Cookie, token,Content-Type,Content-disposition");
  122.         response.setHeader("Content-disposition", exportFilename + "." + extName);
  123.     }
  124. }
复制代码

三、导入分析监听器

那要使用easyexcel,首先要解决分析器抽象类的实现:
        当时第一次使用easyexcel的时候,对这个工具框架不熟悉,项目时间被催的紧,没时间去做设计,当时修改每个类的字段注解index属性,每个字段单独写校验语句,简直苦不堪言。我只想说,磨刀不误砍柴工,不注重设计的公司,只会被延误更多的时间。

  1. /**
  2. * @Title: ExcelImportReadListener
  3. * @Description: 其他人如果觉得invoke()方法不满足其需求,可以自己实现一下
  4. * @Author: wenrong
  5. * @Date: 2024/4/25 17:08
  6. * @Version:1.0
  7. */
  8. @Data
  9. public abstract class ExcelImportReadListener<T extends ValidateBaseBo> extends AnalysisEventListener<T> {
  10.     private static final Logger log = LoggerFactory.getLogger("excelReadListener");
  11.     public static int BATCH_COUNT = 1000;
  12.     private AtomicLong successNum = new AtomicLong();
  13.     private final Class<T> clazz;
  14.     private Validator<T> validator;
  15.     private List<T> successData = new ArrayList<>();
  16.     private List<T> failureData = new ArrayList<>();
  17.     public ExcelImportReadListener(Class<T> clazz) {
  18.         this.clazz = clazz;
  19.     }
  20.     @Override
  21.     public void invoke(T data, AnalysisContext context) {
  22.         log.info("解析到一条数据:{}", JSONObject.toJSON(data));
  23.         StringBuilder errMsg = new StringBuilder();
  24.         try {
  25.             //根据excel数据实体中的javax.validation + 正则表达式来校验excel数据
  26.             errMsg.append(EasyExcelValidateHandler.validateEntity(data));
  27.             // 额外自定义校验,以及设置数据属性的逻辑
  28.             if (validator != null) {
  29.                 errMsg.append(validator.validate(data));
  30.             }
  31.         } catch (NoSuchFieldException e) {
  32.             log.error(e.getMessage());
  33.         }
  34.         if (StringUtils.isNotEmpty(errMsg.toString())) {
  35.             data.setErrMsg(errMsg.toString());
  36.             failureData.add(data);
  37.         } else {
  38.             successData.add(data);
  39.             successNum.incrementAndGet();
  40.         }
  41.         if (BATCH_COUNT != 0 && successData.size() >= BATCH_COUNT) {
  42.             try {
  43.                 saveData();
  44.             } catch (Exception e) {
  45.                 log.error(e.getMessage(), e);
  46.             }
  47.             successData.clear();
  48.         }
  49.     }
  50.     @Override
  51.     public final void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
  52.         // 验证表头
  53.         if (headMap.isEmpty()) {
  54.             throw new ServiceException("无表头");
  55.         }
  56.         if (clazz != null) {
  57.             try {
  58.                 Map<Integer, String> indexNameMap = getIndexNameMap(clazz);
  59.                 for (Integer index : indexNameMap.keySet()) {
  60.                     log.info("表头字段:{}", headMap.get(index));
  61.                     if (StringUtils.isEmpty(headMap.get(index))) {
  62.                         throw new ServiceException("未设置index");
  63.                     }
  64.                     // 对比excel表头和解析数据的java实体类的,看是否匹配
  65.                     if (!headMap.get(index).equals(indexNameMap.get(index))) {
  66.                         throw new ServiceException("导入模板错误");
  67.                     }
  68.                 }
  69.             } catch (NoSuchFieldException e) {
  70.                 log.error(e.getMessage(), e);
  71.             }
  72.         }
  73.     }
  74.     @Override
  75.     public final void doAfterAllAnalysed(AnalysisContext context) {
  76.         log.info("所有数据解析完成!共校验成功{}条数据,校验失败{}条数据", successNum.get(), failureData.size());
  77.         try {
  78.             saveData();
  79.         } catch (Exception e) {
  80.             log.error(e.getMessage(), e);
  81.         }
  82.     }
  83.     /**
  84.      * 将该类做成抽象类,在各service中实现saveDate方法,
  85.      * 不侵入业务,同时不会让解析占用内存
  86.      */
  87.     public void saveData() throws Exception {
  88.         log.info("开始往数据库插入数据");
  89.     }
  90.     private Map<Integer, String> getIndexNameMap(Class<T> clazz) throws NoSuchFieldException {
  91.         Map<Integer, String> excelPropertyMap = new HashMap<>();
  92.         Field field;
  93.         Field[] fields = clazz.getDeclaredFields();
  94.         int sequence = 0;
  95.         for (Field item : fields) {
  96.             field = clazz.getDeclaredField(item.getName());
  97.             field.setAccessible(true);
  98.             ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
  99.             if (excelProperty != null) {
  100.                 // 避免每个列都要写index,插入或删除一个字段,所有的index都需要修改。默认为java实体类中字段的顺序。
  101.                 int index = excelProperty.index() == -1 ? sequence : excelProperty.index();
  102.                 String[] values = excelProperty.value();
  103.                 StringBuilder value = new StringBuilder();
  104.                 for (String v : values) {
  105.                     value.append(v);
  106.                 }
  107.                 excelPropertyMap.put(index, value.toString());
  108.                 sequence++;
  109.             }
  110.         }
  111.         return excelPropertyMap;
  112.     }
  113. }
复制代码
3.1、分析成功的数据直接落库,错误数据导出

        ValidateBaseBo:用在导入的时候,将校验的错误保留下来,然后再把有问题的数据过滤出来,再导出,或者表现在前端的导入效果里,操纵者可以按照错误信息把表格里的数据修改好后,再次导入,而且只将导入失败的数据导出,不用去原表中大片的数据中去找有错误信息的数据,目标是方便操纵者快速定位表格里的问题数据。
  1. /**
  2. * @Title: ValidateBaseBo
  3. * @Description:
  4. * @Author: wenrong
  5. * @Date: 2024/10/17 上午11:02
  6. * @Version:1.0
  7. */
  8. @Data
  9. public abstract class ValidateBaseBo {
  10.     @ExcelProperty(value = "错误信息")
  11.     @TableField(exist = false)
  12.     @ApiModelProperty(hidden = true)
  13.     private String errMsg;
  14. }
复制代码
3.2、分析过程中校验数据精确性 

        除了javax.validation,基础的注解校验之外,假如还必要额外的校验,就自界说校验器作增补。
  1. /**
  2. * @Title: ValidData
  3. * @Description: javax.validation 以外校验函数
  4. * @Author: wenrong
  5. * @Date: 2024/4/26 19:51
  6. * @Version:1.0
  7. */
  8. public interface Validator<T> {
  9.     /**
  10.      * 这里的实现方法,最后返回的如果为null,一定要返回"",否则会被转化为"null"
  11.      *
  12.      * @param T t
  13.      * @return ""
  14.      */
  15.     String validate(T t);
  16. }
复制代码


3.3、导入/导出 Convertor 

        excel导入数据对应的实体类:要注意表格中的汉字和现实存入到数据库中数值的转换:Convertor
  1. /**
  2. * @author wenrong
  3. * @date 2024-11-25 17:38:26
  4. */
  5. @Data
  6. @Builder
  7. @NoArgsConstructor
  8. @AllArgsConstructor
  9. @TableName("····")
  10. public class YourClass extends ValidateBaseBo implements Serializable {
  11.     private static final long serialVersionUID = 1L;
  12.     public static Map<String, DyzProvinceSchool> provinceMap = new HashMap<>();
  13.     public static Map<String, DyzProvinceSchool> schoolMap = new HashMap<>();
  14.     @ExcelIgnore
  15.     @TableId(type = IdType.AUTO)
  16.     private Long id;
  17.     @ApiModelProperty(value = "省份ID")
  18.     @ExcelProperty(value = "省份", converter = ProvinceConvertor.class)
  19.     @NotNull(message = "不能为空")
  20.     private Integer provinceId;
  21.     @ApiModelProperty(value = "学校ID")
  22.     @ExcelProperty(value = "学校", converter = SchoolConvertor.class)
  23.     @NotNull(message = "不能为空")
  24.     private Integer schoolId;
  25.     @ApiModelProperty(value = "节目代码")
  26.     @ExcelProperty(value = "节目代码")
  27.     @NotNull(message = "不能为空")
  28.     private String worksNumber;
  29.     @ApiModelProperty(value = "节目/项目名称")
  30.     @ExcelProperty(value = "节目/项目名称")
  31.     @NotNull(message = "不能为空")
  32.     private String ``````;
  33.     public String validate(Map<Integer, DyzProvinceSchool> provinceMap, Map<Integer, DyzProvinceSchool> schoolMap) {
  34.         StringBuilder sb = new StringBuilder(this.getErrMsg() == null ? "" : this.getErrMsg());
  35.         if (provinceMap.get(this.provinceId) == null) {
  36.             sb.append("省份不存在: ").append(provinceId);
  37.         }
  38.         if (schoolMap.get(this.schoolId) == null) {
  39.             sb.append("学校不存在: ").append(schoolId);
  40.         }
  41.         return sb.toString();
  42.     }
  43.     public static class GroupTypeConvertor implements Converter<Integer> {
  44.         //导入的时候,将表格的汉字转换成java对应数据库的字段
  45.         @Override
  46.         public Integer convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
  47.                                          GlobalConfiguration globalConfiguration) {
  48.             switch (cellData.getStringValue()) {
  49.                 default:
  50.                     return 2;
  51.                 case "小学组":
  52.                     return 0;
  53.                 case "中学组":
  54.                     return 1;
  55.             }
  56.         }
  57.         
  58.         // 导出的时候,将数据库中存储的值,转换为用户能看懂的汉字
  59.         @Override
  60.         public WriteCellData<?> convertToExcelData(Integer value,
  61.                                                    ExcelContentProperty excelContentProperty,
  62.                                                    GlobalConfiguration globalConfiguration) {
  63.             switch (value) {
  64.                 default:
  65.                     return new WriteCellData<>("其他组");
  66.                 case 0:
  67.                     return new WriteCellData<>("小学组");
  68.                 case 1:
  69.                     return new WriteCellData<>("中学组");
  70.             }
  71.         }
  72.     }
  73.     public static class PresentConvertor implements Converter<Integer> {
  74.         @Override
  75.         public Integer convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
  76.                                          GlobalConfiguration globalConfiguration) {
  77.             if (cellData.getStringValue().equals("否")) {
  78.                 return 0;
  79.             } else {
  80.                 return 1;
  81.             }
  82.         }
  83.         
  84.         @Override
  85.         public WriteCellData<?> convertToExcelData(Integer value,
  86.                                                    ExcelContentProperty excelContentProperty,
  87.                                                    GlobalConfiguration globalConfiguration) {
  88.             switch (value) {
  89.                 default:
  90.                     return new WriteCellData<>("-");
  91.                 case 0:
  92.                     return new WriteCellData<>("否");
  93.                 case 1:
  94.                     return new WriteCellData<>("是");
  95.             }
  96.         }
  97.     }
  98.     public static class ProvinceConvertor implements Converter<Integer> {
  99.         @Override
  100.         public Integer convertToJavaData(ReadCellData<?> cellData,
  101.                                          ExcelContentProperty contentProperty,
  102.                                          GlobalConfiguration globalConfiguration) throws Exception {
  103.             if (provinceMap.isEmpty()) {
  104.                 throw new Exception("省份配置数据为空");
  105.             }
  106.             return StringUtils.isBlank(cellData.getStringValue()) ? null : provinceMap.get(cellData.getStringValue()).getId();
  107.         }
  108.     }
  109.     public static class SchoolConvertor implements Converter<Integer> {
  110.         @Override
  111.         public Integer convertToJavaData(ReadCellData<?> cellData,
  112.                                          ExcelContentProperty contentProperty,
  113.                                          GlobalConfiguration globalConfiguration) throws Exception {
  114.             if (schoolMap.isEmpty()) {
  115.                 throw new Exception("学校配置数据为空");
  116.             }
  117.             return StringUtils.isBlank(cellData.getStringValue()) ? null : schoolMap.get(cellData.getStringValue()).getId();
  118.         }
  119.         
  120.         // 导出转换省略掉
  121.     }
  122. }
复制代码

3.4、实现导入分析监听器    

        上面的分析监听器是个抽象类,是一种模板模式的设计头脑应用,我们根据不同的业务,自己扩展invoke方法和saveData方法,但其实saveData也可以做成模板方法,只是必要依靠内部绑定一个数据层dao接口,Mapper,对于有的人来说,会耦合dao层,但我以为假如dao层取一个接口,那么也没什么耦合的问题。节流不必要的重复代码,还是值得的。
那么上述的谁人模板抽象分析监听器可以改为:
  1. /**
  2. * @Title: ExcelImportReadListener
  3. * @Description: 其他人如果觉得invoke()方法不满足其需求,可以自己实现一下
  4. * @Author: wenrong
  5. * @Date: 2024/4/25 17:08
  6. * @Version:1.0
  7. */
  8. @Data
  9. public abstract class ExcelImportReadListener<T extends ValidateBaseBo, S extends IService<T>> extends AnalysisEventListener<T> {
  10.     private static final Logger log = LoggerFactory.getLogger("excelReadListener");
  11.     public static int BATCH_COUNT = 1000;
  12.     private AtomicLong successNum = new AtomicLong();
  13.     private final Class<T> clazz;
  14.     private S service;
  15.     private Validator<T> validator;
  16.     private List<T> successData = new ArrayList<>();
  17.     private List<T> failureData = new ArrayList<>();
  18.     public ExcelImportReadListener(Class<T> clazz, S service) {
  19.         this.clazz = clazz;
  20.         this.service = service;
  21.     }
  22.     @Override
  23.     public void invoke(T data, AnalysisContext context) {
  24.         log.info("解析到一条数据:{}", JSONObject.toJSON(data));
  25.         StringBuilder errMsg = new StringBuilder();
  26.         try {
  27.             //根据excel数据实体中的javax.validation + 正则表达式来校验excel数据
  28.             errMsg.append(EasyExcelValidateHandler.validateEntity(data));
  29.             // 额外自定义校验,以及设置数据属性的逻辑
  30.             if (validator != null) {
  31.                 errMsg.append(validator.validate(data));
  32.             }
  33.         } catch (NoSuchFieldException e) {
  34.             log.error(e.getMessage());
  35.         }
  36.         if (StringUtils.isNotEmpty(errMsg.toString())) {
  37.             data.setErrMsg(errMsg.toString());
  38.             failureData.add(data);
  39.         } else {
  40.             successData.add(data);
  41.             successNum.incrementAndGet();
  42.         }
  43.         if (BATCH_COUNT != 0 && successData.size() >= BATCH_COUNT) {
  44.             try {
  45.                 saveData();
  46.             } catch (Exception e) {
  47.                 log.error(e.getMessage(), e);
  48.             }
  49.             successData.clear();
  50.         }
  51.     }
  52.     @Override
  53.     public final void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
  54.         // 验证表头
  55.         if (headMap.isEmpty()) {
  56.             throw new ServiceException("无表头");
  57.         }
  58.         if (clazz != null) {
  59.             try {
  60.                 Map<Integer, String> indexNameMap = getIndexNameMap(clazz);
  61.                 for (Integer index : indexNameMap.keySet()) {
  62.                     log.info("表头字段:{}", headMap.get(index));
  63.                     if (StringUtils.isEmpty(headMap.get(index))) {
  64.                         throw new ServiceException("未设置index");
  65.                     }
  66.                     if (!headMap.get(index).equals(indexNameMap.get(index))) {
  67.                         throw new ServiceException("导入模板错误");
  68.                     }
  69.                 }
  70.             } catch (NoSuchFieldException e) {
  71.                 log.error(e.getMessage(), e);
  72.             }
  73.         }
  74.     }
  75.     @Override
  76.     public final void doAfterAllAnalysed(AnalysisContext context) {
  77.         log.info("所有数据解析完成!共校验成功{}条数据,校验失败{}条数据", successNum.get(), failureData.size());
  78.         try {
  79.             saveData();
  80.         } catch (Exception e) {
  81.             log.error(e.getMessage(), e);
  82.         }
  83.     }
  84.     /**
  85.      * 将该类做成抽象类,在各service中实现saveDate方法,
  86.      * 不侵入业务,同时不会让解析占用内存
  87.      */
  88.     public void saveData() throws Exception {
  89.         log.info("开始往数据库插入数据");
  90.         List<T> successData = this.getSuccessData();
  91.         List<T> failureData = this.getFailureData();
  92.         boolean saved = service.saveBatch(successData);
  93.         if (!saved) {
  94.             successData.forEach(work -> work.setErrMsg("保存失败"));
  95.             failureData.addAll(successData);
  96.         } else {
  97.             this.setSuccessData(successData);
  98.         }
  99.     }
  100.     private Map<Integer, String> getIndexNameMap(Class<T> clazz) throws NoSuchFieldException {
  101.         Map<Integer, String> excelPropertyMap = new HashMap<>();
  102.         Field field;
  103.         Field[] fields = clazz.getDeclaredFields();
  104.         int sequence = 0;
  105.         for (Field item : fields) {
  106.             field = clazz.getDeclaredField(item.getName());
  107.             field.setAccessible(true);
  108.             ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
  109.             if (excelProperty != null) {
  110.                 int index = excelProperty.index() == -1 ? sequence : excelProperty.index();
  111.                 String[] values = excelProperty.value();
  112.                 StringBuilder value = new StringBuilder();
  113.                 for (String v : values) {
  114.                     value.append(v);
  115.                 }
  116.                 excelPropertyMap.put(index, value.toString());
  117.                 sequence++;
  118.             }
  119.         }
  120.         return excelPropertyMap;
  121.     }
  122. }
复制代码
业务代码中实现模板分析监听器的代码示例:
  1.     @Override
  2.     @Transactional
  3.     public ExcelImportReadListener<BasicWorks> importExcel(MultipartFile file) throws IOException {
  4.         List<DyzProvinceSchool> schoolList = dyzProvinceSchoolService.getSchoolList();
  5.         List<DyzProvinceSchool> provinceList = dyzProvinceSchoolService.getProvinceList();
  6.         Map<String, DyzProvinceSchool> schoolMap = schoolList.stream().collect(Collectors.toMap(DyzProvinceSchool::getSchoolName, s -> s));
  7.         Map<Integer, DyzProvinceSchool> schoolMap1 = schoolList.stream().collect(Collectors.toMap(DyzProvinceSchool::getId, s -> s));
  8.         Map<String, DyzProvinceSchool> provinceMap = provinceList.stream().collect(Collectors.toMap(DyzProvinceSchool::getProvinceName, s -> s));
  9.         Map<Integer, DyzProvinceSchool> provinceMap1 = provinceList.stream().collect(Collectors.toMap(DyzProvinceSchool::getId, s -> s));
  10.         BasicWorks.schoolMap = schoolMap;
  11.         BasicWorks.provinceMap = provinceMap;
  12.         // 匿名内部类扩展模板监听器
  13.         ExcelImportReadListener<BasicWorks> readListener = new ExcelImportReadListener<BasicWorks>(BasicWorks.class) {
  14.             @Override
  15.             public void invoke(BasicWorks data, AnalysisContext context) {
  16.                 Set<String> allDataExistInExcel = new HashSet<>();
  17.                 Set<String> allDataExistInDataSource = list().stream().map(BasicWorks::getWorksNumber).collect(Collectors.toSet());
  18.                 List<BasicWorks> failureData = this.getFailureData();
  19.                 StringBuilder errMsg = new StringBuilder(data.getErrMsg() == null ? "" : data.getErrMsg());
  20.                 if (StringUtils.isBlank(data.getWorksNumber())) {
  21.                     errMsg.append("节目代码不能为空,");
  22.                     data.setErrMsg(errMsg.toString());
  23.                     failureData.add(data);
  24.                 } else {
  25.                     if (allDataExistInExcel.contains(data.getWorksNumber())) {
  26.                         errMsg.append("Excel表格中存在重复的数据,")
  27.                                 .append("节目代码:").append(data.getWorksNumber());
  28.                         data.setErrMsg(errMsg.toString());
  29.                         failureData.add(data);
  30.                         allDataExistInExcel.add(data.getWorksNumber());
  31.                     }
  32.                     if (allDataExistInDataSource.contains(data.getWorksNumber())) {
  33.                         errMsg.append("数据库中存在重复的数据,")
  34.                                 .append("节目代码:").append(data.getWorksNumber());
  35.                         data.setErrMsg(errMsg.toString());
  36.                         failureData.add(data);
  37.                         allDataExistInExcel.add(data.getWorksNumber());
  38.                     }
  39.                 }
  40.                 allDataExistInExcel.add(data.getWorksNumber());
  41.                 super.invoke(data, context);
  42.             }
  43.         
  44.         // 设置javax.validation以外校验器,将会在invoke方法里执行校验
  45.         readListener.setValidator(work -> work.validate(provinceMap1, schoolMap1));
  46.         // 导入 Excel
  47.         EasyExcels.read(file, BasicWorks.class, readListener);
  48.         return readListener;
  49.     }
复制代码

另外还有必要将表格中图片导入后主动上传到文件服务,然后将url生存在数据库的需求:
  1. public ExcelImportReadListener<BasicHotel> importExcel(MultipartFile file) throws IOException {
  2.     //获取图片,联合Apache 的ExcelUtil,ExcelPicUtil工具类,获取图片数据对象PictureData
  3.     ExcelReader reader = ExcelUtil.getReader(file.getInputStream());
  4.     Map<String, PictureData> picMap = ExcelPicUtil.getPicMap(reader.getWorkbook(), 0);
  5.     ExcelImportReadListener<BasicHotel> readListener = new ExcelImportReadListener<BasicHotel>(BasicHotel.class) {
  6.         @Override
  7.         public void invoke(BasicHotel data, AnalysisContext context) {
  8.             Set<String> allDataExistInDataSource = list().stream().map(BasicHotel::getHotelName).collect(Collectors.toSet());
  9.             Set<String> allDataExistInExcel = new HashSet<>();
  10.             List<BasicHotel> failureData = this.getFailureData();
  11.             StringBuilder errMsg = new StringBuilder(data.getErrMsg() == null ? "" : data.getErrMsg());
  12.             if (StringUtils.isEmpty(data.getErrMsg())) {
  13.                 errMsg.append("酒店名称不能为空,");
  14.                 data.setErrMsg(errMsg.toString());
  15.                 failureData.add(data);
  16.             } else {
  17.                 if (allDataExistInExcel.contains(data.getHotelName())) {
  18.                     errMsg.append("Excel表格中存在重复的数据,")
  19.                             .append("酒店名称:").append(data.getHotelName());
  20.                     data.setErrMsg(errMsg.toString());
  21.                     failureData.add(data);
  22.                     allDataExistInExcel.add(data.getHotelName());
  23.                 }
  24.                 if (allDataExistInDataSource.contains(data.getHotelName())) {
  25.                     errMsg.append("数据库中存在重复的数据,")
  26.                             .append("酒店名称:").append(data.getHotelName());
  27.                     data.setErrMsg(errMsg.toString());
  28.                     failureData.add(data);
  29.                     allDataExistInExcel.add(data.getHotelName());
  30.                 }
  31.             }
  32.             allDataExistInExcel.add(data.getHotelName());
  33.             String err = "";
  34.             int rowIndex = context.readRowHolder().getRowIndex() + 1;
  35.             PictureData pictureData = picMap.get(rowIndex + "_0");
  36.             if (pictureData == null) {
  37.                 err = String.format(data.getErrMsg() + "第%s行,%s", rowIndex, "酒店照片为空");
  38.             }
  39.             try {
  40.                 // 上传图片
  41.                 String fileUrl = ossFileController.ftpUploadFile(pictureData.getData(), "", data.getHotelName());
  42.                 data.setPicture(fileUrl);
  43.             } catch (IOException ex) {
  44.                 err = String.format(data.getErrMsg() + "第%s行,%s", rowIndex, "酒店照片为空上传失败");
  45.             }
  46.             data.setErrMsg(err);
  47.             super.invoke(data, context);
  48.         }
  49.     };
  50.     // 导入 Excel
  51.     EasyExcels.read(file, BasicHotel.class, readListener);
  52.     return readListener;
  53. }
复制代码
        这是导入部分,导出部分,五花八门的需求就比较多了。

四、导出


4.1、导出 数据导入模板

模板一样平常会有下拉选项列的需求,下拉列一样平常用注解枚举几个就行了:
  1. import java.lang.annotation.*;
  2. /**
  3. * 标注导出的列为下拉框类型,并为下拉框设置内容
  4. */
  5. @Documented
  6. @Retention(RetentionPolicy.RUNTIME)
  7. @Target(ElementType.FIELD)
  8. public @interface ExcelSelected {
  9.     /**
  10.      * 固定下拉内容
  11.      */
  12.     String[] source() default {};
  13.     /**
  14.      * 设置下拉框的起始行,默认为第二行
  15.      */
  16.     int firstRow() default 1;
  17.     /**
  18.      * 设置下拉框的结束行,默认为最后一行
  19.      */
  20.     int lastRow() default 0x10000;
  21. }
复制代码
下拉注解分析器:
  1. @Data
  2. @Slf4j
  3. public class ExcelSelectedResolve {
  4.     /**
  5.      * 下拉内容
  6.      */
  7.     private String[] source;
  8.     /**
  9.      * 设置下拉框的起始行,默认为第二行
  10.      */
  11.     private int firstRow;
  12.     /**
  13.      * 设置下拉框的结束行,默认为最后一行
  14.      */
  15.     private int lastRow;
  16.     public String[] resolveSelectedSource(ExcelSelected excelSelected) {
  17.         if (excelSelected == null) {
  18.             return null;
  19.         }
  20.         // 获取固定下拉框的内容
  21.         String[] source = excelSelected.source();
  22.         if (source.length > 0) {
  23.             return source;
  24.         }
  25. //        // 获取动态下拉框的内容
  26. //        Class<? extends ExcelDynamicSelect>[] classes = excelSelected.sourceClass();
  27. //        if (classes.length > 0) {
  28. //            try {
  29. //                ExcelDynamicSelect excelDynamicSelect = classes[0].newInstance();
  30. //                String[] dynamicSelectSource = excelDynamicSelect.getSource();
  31. //                if (dynamicSelectSource != null && dynamicSelectSource.length > 0) {
  32. //                    return dynamicSelectSource;
  33. //                }
  34. //            } catch (InstantiationException | IllegalAccessException e) {
  35. //                log.error("解析动态下拉框数据异常", e);
  36. //            }
  37. //        }
  38.         return null;
  39.     }
  40. }
复制代码
下拉handler:
  1. public class SelectSheetWriteHandler implements SheetWriteHandler {
  2.     private final Map<Integer, ExcelSelectedResolve> selectedMap;
  3.     public SelectSheetWriteHandler(Map<Integer, ExcelSelectedResolve> selectedMap) {
  4.         this.selectedMap = selectedMap;
  5.     }
  6.     /**
  7.      * Called before create the sheet
  8.      */
  9.     @Override
  10.     public void beforeSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
  11.     }
  12.     /**
  13.      * Called after the sheet is created
  14.      */
  15.     @Override
  16.     public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
  17.         // 这里可以对cell进行任何操作
  18.         Sheet sheet = writeSheetHolder.getSheet();
  19.         DataValidationHelper helper = sheet.getDataValidationHelper();
  20.         selectedMap.forEach((k, v) -> {
  21.             // 设置下拉列表的行: 首行,末行,首列,末列
  22.             CellRangeAddressList rangeList = new CellRangeAddressList(v.getFirstRow(), v.getLastRow(), k, k);
  23.             // 设置下拉列表的值
  24.             DataValidationConstraint constraint = helper.createExplicitListConstraint(v.getSource());
  25.             // 设置约束
  26.             DataValidation validation = helper.createValidation(constraint, rangeList);
  27.             // 阻止输入非下拉选项的值
  28.             validation.setErrorStyle(DataValidation.ErrorStyle.STOP);
  29.             validation.setShowErrorBox(true);
  30.             validation.setSuppressDropDownArrow(true);
  31.             validation.createErrorBox("提示", "请输入下拉选项中的内容");
  32.             sheet.addValidationData(validation);
  33.         });
  34.     }
  35. }
复制代码
        其实假如是动态的下拉列表,不能固定枚举的话,直接从配置数据表中拉出业务配置列表,将列表作为传参,使用util类EasyExcels第一个方法就好。
  1.     @ExcelProperty(value = "组别")
  2.     @ExcelSelected(source = {"小学组", "初中组"})
  3.     @ApiModelProperty(value = "组别:0-小学组,1-初中组,2-其他组")
  4.     @NotNull(message = "不能为空")
  5.     private String groupType;
  6.     @ExcelProperty(value = "是否出席")
  7.     @ExcelSelected(source = {"是", "否"})
  8.     @ApiModelProperty(value = "是否出席:0-否,1是")
  9.     @NotNull(message = "不能为空")
  10.     private String present;
复制代码


4.2、图片导出convertor:

  1. public class UrlPictureConverter implements Converter<String> {
  2.     public static int urlConnectTimeout = 2000;
  3.     public static int urlReadTimeout = 6000;
  4.     @Override
  5.     public Class<?> supportJavaTypeKey() {
  6.         return String.class;
  7.     }
  8.     @Override
  9.     public WriteCellData<?> convertToExcelData(String url, ExcelContentProperty contentProperty,
  10.                                                GlobalConfiguration globalConfiguration) throws IOException {
  11.         InputStream inputStream = null;
  12.         try {
  13.             URL value = new URL(url);
  14.             if (ObjectUtils.isEmpty(value)) {
  15.                 return new WriteCellData<>("");
  16.             }
  17.             URLConnection urlConnection = value.openConnection();
  18.             urlConnection.setConnectTimeout(urlConnectTimeout);
  19.             urlConnection.setReadTimeout(urlReadTimeout);
  20.             inputStream = urlConnection.getInputStream();
  21.             byte[] bytes = IoUtils.toByteArray(inputStream);
  22.             return new WriteCellData<>(bytes);
  23.         } catch (Exception e) {
  24.             log.info("图片获取异常", e);
  25.             return new WriteCellData<>("图片获取异常");
  26.         } finally {
  27.             if (inputStream != null) {
  28.                 inputStream.close();
  29.             }
  30.         }
  31.     }
  32. }
复制代码
4.3、有归并单元格导出:

  1. @Data
  2. @Builder
  3. @AllArgsConstructor
  4. @NoArgsConstructor
  5. @ExcelIgnoreUnannotated
  6. @Slf4j
  7. public class WorkJudgesStatisticsVo implements Serializable {
  8.     private static final long serialVersionUID = 1L;
  9.     @ExcelProperty(value = "序号", index = 0)
  10.     private String sequence;
  11.     public static class MergeStrategy implements RowWriteHandler {
  12.         private int totalRowNum;
  13.         public MergeStrategy(int totalRowNum) {
  14.             this.totalRowNum = totalRowNum;
  15.         }
  16.         public static MergeStrategy build(int totalRowNum) {
  17.             return new MergeStrategy(totalRowNum);
  18.         }
  19.         @Override
  20.         public void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) {
  21.             if (isHead) {
  22.                 // 处理表头
  23.                 return;
  24.             }
  25.             log.info("当前行号:{}", row.getRowNum());
  26.             log.info("总行数:{}", totalRowNum);
  27.             // 合并
  28.             if (row.getRowNum() != totalRowNum + 1) {
  29.                 return;
  30.             }
  31.             writeSheetHolder.getSheet().addMergedRegion(new CellRangeAddress(writeSheetHolder.getLastRowIndex(), writeSheetHolder.getLastRowIndex(), 5, 6));
  32.         }
  33.     }
  34. }
复制代码


4.4、行转列,并使用模板的方式导出:

  1. @Override
  2. public void selectWorksJudgesResultReview(HttpServletResponse response) throws IOException {
  3.     List<Map<String, Object>> views = scoreReviewWorksJudgesMapper.selectWorksJudgesResultReview();
  4.     List<DyzScoreReviewWorksJudges> reviewWorksJudges = scoreReviewWorksJudgesMapper.selectList();
  5.     List<DyzScoreWorksFiles> scoreWorksFiles = worksFilesMapper.selectList();
  6.     HashMap<Long, List<DyzScoreWorksFiles>> fileMap = new HashMap<>();
  7.     scoreWorksFiles.forEach(f -> {
  8.         List<DyzScoreWorksFiles> files = fileMap.computeIfAbsent(f.getWorksId(), k -> new ArrayList<>());
  9.         files.add(f);
  10.     });
  11.     HashMap<Long, List<DyzScoreReviewWorksJudges>> scoreMap = new HashMap<>();
  12.     reviewWorksJudges.forEach(judge -> {
  13.         List<DyzScoreReviewWorksJudges> judges = scoreMap.computeIfAbsent(judge.getWorksId(), k -> new ArrayList<>());
  14.         judges.add(judge);
  15.     });
  16.     AtomicInteger sequence = new AtomicInteger(0);
  17.     views.forEach(map -> {
  18.         map.put("sequence", String.valueOf(sequence.incrementAndGet()));
  19.         Long workId = Long.valueOf(map.get("workId").toString());
  20.         List<DyzScoreReviewWorksJudges> judges = scoreMap.get(workId);
  21.         for (int i = 0; i < 15; i++) {
  22.             map.put("score" + (i + 1), "");
  23.             map.put("correctness" + (i + 1), "");
  24.         }
  25.         map.put("avgScore", "");
  26.         map.put("avgScore1", "");
  27.         if (judges != null && judges.size() > 0) {
  28.             AtomicInteger serialNo = new AtomicInteger(0);
  29.             AtomicInteger serialNo1 = new AtomicInteger(0);
  30.             judges.forEach(j -> {
  31.                 map.put("score" + serialNo.incrementAndGet(), j.getScore());
  32.                 map.put("correctness" + serialNo1.incrementAndGet(), j.getRemark());
  33.             });
  34.             judges.sort(Comparator.comparing(DyzScoreReviewWorksJudges::getScore));
  35.             BigDecimal sum = judges.stream().map(DyzScoreReviewWorksJudges::getScore).reduce(BigDecimal.ZERO, BigDecimal::add);
  36.             BigDecimal avg = sum.divide(BigDecimal.valueOf(judges.size()), 2, BigDecimal.ROUND_HALF_UP);
  37.             map.put("avgScore", avg);
  38.             if (judges.size() > 3) {
  39.                 judges.remove(0);
  40.                 judges.remove(judges.size() - 1);
  41.                 BigDecimal sum1 = judges.stream().map(DyzScoreReviewWorksJudges::getScore).reduce(BigDecimal.ZERO, BigDecimal::add);
  42.                 BigDecimal avg1 = sum1.divide(BigDecimal.valueOf(judges.size()), 2, BigDecimal.ROUND_HALF_UP);
  43.                 map.put("avgScore1", avg1);
  44.             }
  45.         }
  46.         for (int i = 0; i < 4; i++) {
  47.             map.put("fileName" + (i + 1), "");
  48.         }
  49.         List<DyzScoreWorksFiles> files = fileMap.get(workId);
  50.         if (files != null && files.size() > 0) {
  51.             AtomicInteger serialNo = new AtomicInteger(0);
  52.             files.forEach(f -> map.put("fileName" + serialNo.incrementAndGet(), f.getUrl()));
  53.         }
  54.     });
  55.     ConcurrentHashSet<String> columns = views.stream().flatMap(map -> map.keySet().stream()).collect(Collectors.toCollection(ConcurrentHashSet::new));
  56.     List<String> scoreColumns = columns.stream().filter(c -> c.contains("score") || c.contains("avgScore")).collect(Collectors.toList());
  57.     List<String> correctnessColumns = columns.stream().filter(c -> c.contains("correctness")).collect(Collectors.toList());
  58.     //输入流
  59.     InputStream inputStream = null;
  60.     ServletOutputStream outputStream = null;
  61.     ExcelWriter excelWriter = null;
  62.     try {
  63.         org.springframework.core.io.Resource templateFile = resourceLoader.getResource("classpath:templates\\XXXX报表.xlsx");
  64.         inputStream = templateFile.getInputStream();
  65.         // 获取文件名并转码
  66.         response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
  67.         response.setCharacterEncoding("utf-8");
  68.         outputStream = response.getOutputStream();
  69.         // 创建填充配置
  70.         FillConfig fillConfig = FillConfig.builder().forceNewRow(true).build();
  71.         // 创建写对象
  72.         excelWriter = EasyExcel.write(outputStream)
  73.                 .withTemplate(inputStream).build();
  74.         // 创建Sheet对象
  75.         WriteSheet sheet = EasyExcel.writerSheet(0, "报名数量统计").build();
  76.         excelWriter.fill(views, fillConfig, sheet);
  77.         excelWriter.fill(new FillWrapper("scoreColumns", scoreColumns), sheet);
  78.         excelWriter.fill(new FillWrapper("correctnessColumns", correctnessColumns), sheet);
  79.     } catch (Exception e) {
  80.         log.error("导出失败={}", e.getMessage());
  81.     } finally {
  82.         if (excelWriter != null) {
  83.             excelWriter.finish();
  84.         }
  85.         //关闭流
  86.         if (outputStream != null) {
  87.             try {
  88.                 outputStream.close();
  89.             } catch (IOException e) {
  90.                 log.error("关闭输出流失败", e);
  91.             }
  92.         }
  93.         if (inputStream != null) {
  94.             try {
  95.                 inputStream.close();
  96.             } catch (IOException e) {
  97.                 log.error("关闭输入流失败", e);
  98.             }
  99.         }
  100.     }
  101. }
复制代码
模板内里的取值占位符写法

        还有一些表格宽度,高度自顺应策略,美化风格的代码就不贴了,必要的话到我的资源中去下载。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

悠扬随风

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表