最近阿里开源的excel读写项目EasyExcel又火了起来 . 原来是项目又开始维护了, 从1.x 更新到2.x 了 , 而且迭代迅速 , 目前已经更新到 2.1.0-beta3 版本.
关于easyexcel , 其主要目的为降低读取excel时的内存损耗 , 简化读写excel的api .
同时2.x版本提供了很多新功能 , 具体大家可以直接参考官方说明吧 , github文档上写一清二楚 , 这里就不传播一些没啥必要的二三手知识了 , 而且目前该项目还在不停地迭代 , 给contributor一个star也是很有必要的 . 😄
官方地址 : easyexcel仓库地址 , easyexcel官网
这里我基于easyexcel 2.0.5版本简单封装了一个web读写excel的工具类 , 主要封装了如下功能 :
-
通过注解自定义
LocalDateTime的读写格式 -
通过注解自定义枚举类型的读写格式
-
自定义
BaseExcelListener抽象类封装了常用的数据处理逻辑 , 以及补充读取excel过程中读取发生错误被跳过的行号记录 . -
封装了web的读写excel操作
-
...
下面列举主要功能以及相关示例 , 可以直接看源码 , 每个方法都有写完整的注释 , 如果觉得写得还凑合能看的话 , 给我这个刚毕业没多久的小菜鸡点个star呗 😄
附上源码地址 : https://github.com/aStudyMachine/easyexcel-utils
easyexcel读写excel可以基于java 模型的方式 , 也可以使用List<List<String>> 的方式读写excel , 这里我读写操作使用基于java模型的方式 , 通过java类的属性与excel每一列的数据进行对应
关键注解 : @ExcelProperty
具体如何使用注解建立java模型与Excel表数据的映射可以参考 com.luwei.module.easyexcel.pojo下的两个java模型类Order 类与User类
/** * @author WuKun * @since 2019年10月09日 */ @Data @AllArgsConstructor @NoArgsConstructor //必须要保证无参构造方法存在,否则会报初始化对象失败 // @Accessors(chain = true) 使用lombok该注解会导致无法正常读取到该数据 public class User { /** * {@code @ExcelIgnore} 用于标识该字段不用做excel读写过程中的数据转换 */ @ExcelIgnore private Integer userId; /** * <pre> * {@code @ExcelIgnore} 中的属性 不建议 index 和 name 同时用 * * 要么一个对象统一只用index表示列号, * 例如 : {@code @ExcelProperty(index = 0)} * * 要么一个对象统一只用value去匹配列名 * 例如 : {@code @ExcelProperty("姓名")} * * 用名字去匹配,这里需要注意,如果名字重复,会导致只有一个字段读取到数据 * </pre> */ @ExcelProperty("姓名") private String name; @ExcelProperty("年龄") private Integer age; @ExcelProperty("地址") private String address; /** * <pre> * {@code @EnumFormat} 注解 : * 作用 : 与 {@code @ExcelProperty(converter = EnumExcelConverter.class)} 搭配使用 * 转换java枚举与excel中指定的内容 * 属性 : * - value : 要转换的枚举类class对象 * - fromExcel : 指定excel中用户输入的枚举值的名字的字符串形式,与toJavaEnum中指定的枚举值一一对应 * 以下面的示例来说,fromExcel指定的 "男" 对应 toJavaEnum中的 "MAN" , * 当excel中该列读取到"男" 这个字符串时,会自动转化为枚举{@code GenderEnum.MAN}, * 同理在写excel时,如果该字段为{@code GenderEnum.MAN} 时, 写到excel时则转化为 "男" * - toJavaEnum : 如上所述 * * 注意 : fromExcel 与 toJavaEnum 这两个属性必须同时使用, 而且两个属性的字符串的数组长度必须相同, * 若两个属性都不指定 , 则默认 枚举值名字符串转化为对应的枚举 例如: "MAN" <--> {@code GenderEnum.MAN} * </pre> */ @EnumFormat(value = GenderEnum.class, fromExcel = {"男", "女"}, toJavaEnum = {"MAN", "WOMAN"}) // "男" <--> GenderEnum.MAN ; "女" <--> GenderEnum.WOMAN @ExcelProperty(value = "性别", converter = EnumExcelConverter.class) private GenderEnum gender; /** * <pre> * {@code @LocalDateTimeFormat} 注解 * 作用: 与 {@code @ExcelProperty(converter = LocalDateTimeExcelConverter.class)} 搭配使用, * 指定导入导出的时间格式. * 属性 : * - value : 日期格式字符串 * </pre> */ @ExcelProperty(value = "生日", converter = LocalDateTimeExcelConverter.class) @LocalDateTimeFormat("yyyy-MM-dd HH:mm:ss") private LocalDateTime birthday; }
web导出excel 根据03 / 07版本分为两个不同的方法 ,分别为EasyExcelUtil类中以下两个方法 :
-
导出03版本 :
exportExcel2003Format(EasyExcelParams excelParams) -
导出07版本 :
exportExcel2007Format(EasyExcelParams excelParams)
EasyExcelParams是使用EasyExcel导出excel需要设置的相关参数 , 包括需要导出的List<T>数据以及对应的Java模型 , 使用时根据实际情况设置相应的参数即可.
/** * @author WuKun * @since 2019年10月14日 */ @Data @EqualsAndHashCode(callSuper = false) @Accessors(chain = true) public class EasyExcelParams implements Serializable { /** * excel文件名(不带拓展名) */ private String excelNameWithoutExt; /** * sheet名称 */ private String sheetName; /** * 数据 */ private List data; /** * 数据模型类型 */ private Class dataModelClazz; /** * 响应 */ private HttpServletResponse response; public EasyExcelParams() { } /** * 检查不允许为空的属性 * * @return this */ public EasyExcelParams checkValid() { Assert.isTrue(ObjectUtils.allNotNull(excelNameWithoutExt, data, dataModelClazz, response), "导出excel参数不合法!"); return this; } }
/** * 使用EasyExcelUtils 导出Excel 2007 * * @param response HttpServletResponse * @throws Exception exception */ @GetMapping("/easy2007") public void easy2007(HttpServletResponse response) throws Exception { initData(); //设置参数 EasyExcelParams params = new EasyExcelParams().setResponse(response) .setExcelNameWithoutExt("Order(xlsx)") .setSheetName("第一张sheet") .setData(data) .setDataModelClazz(Order.class) .checkValid(); long begin = System.currentTimeMillis(); EasyExcelUtil.exportExcel2007(params); long end = System.currentTimeMillis(); log.info("-----EasyExcelUtils : 导出成功,导出excel花费时间为 : " + ((end - begin) / 1000) + "秒"); } private void initData() { if (CollectionUtils.isEmpty(data)) { for (int i = 0; i < 60000; i++) { Order order = new Order(); order.setPrice(BigDecimal.valueOf(11.11)); order.setCreateTime(LocalDateTime.now()); order.setGoodsName("香蕉"); order.setOrderId(i); order.setNum(11); order.setOrderStatus(OrderStatusEnum.PAYED); data.add(order); } } }
- 读取excel时用到的是
EasyExcelUtils的readExcel方法 ;
/** * 读取 Excel(支持单个model的多个sheet) * * @param excel 文件 * @param rowModel 实体类映射 * @param listener 用于读取excel的listener */ public static void readExcel(MultipartFile excel, Class rowModel, BaseExcelListener listener) { ExcelReader reader = getReader(excel, rowModel, listener); try { Assert.notNull(reader, "导入Excel失败!"); Integer totalSheetCount = reader.getSheets().size(); for (Integer i = 0; i < totalSheetCount; i++) { reader.read(EasyExcel.readSheet(i).build()); } } finally { // 这里千万别忘记关闭,读的时候会创建临时文件,到时磁盘会崩的 Optional.ofNullable(reader).ifPresent(ExcelReader::finish); } }
- easyexcel的读取操作需要自建一个类继承
AnalysisEventListener抽象类 , 这里我创建BaseExcelListener类继承并重写读取excel的相关方法 , 每个方法的具体作用可直接查看方法头部注释 , 使用时直接创建一个listener类继承BaseExcelListener即可 , 如果默认的BaseExcelListener不满足需求 , 也可以直接自定义一个Listener 类继承BaseExcelListener并重写相应方法.
package com.luwei.module.easyexcel.listener; import com.alibaba.excel.context.AnalysisContext; import com.alibaba.excel.event.AnalysisEventListener; import com.alibaba.excel.exception.ExcelAnalysisException; import com.alibaba.fastjson.JSON; import com.luwei.module.easyexcel.pojo.ErrRows; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import java.util.*; /** * @author WuKun * @since 2019年10月10日 * <p> * 由于在实际中可能会根据不同的业务场景需要的读取到的不同的excel表的数据进行不同操作, * 所以这里将ExcelListener作为所有listener的基类,根据读取不同的java模型自定义一个listener类继承ExcelListener, * 根据不同的业务场景选择性对以下方法进行重写,具体如com.luwei.listener.OrderListener所示 * </p> * * <p>如果默认实现的方法不满足业务,则直接自定义一个listener实现AnalysisEventListener,重写一遍方法即可.</p> */ @Slf4j public abstract class BaseExcelListener<Model> extends AnalysisEventListener<Model> { /** * 自定义用于暂时存储data。 * 可以通过实例获取该值 * 可以指定AnalysisEventListener的泛型来确定List的存储类型 */ @Getter private List<Model> data = new ArrayList<>(); /** * 每隔N条存执行一次{@link BaseExcelListener#doService()}方法, * 如果是入库操作,可使用默认的3000条,然后清理list,方便内存回收 */ private int batchCount = 3000; /** * @param batchCount see batchCount * @return this * @see BaseExcelListener#batchCount */ public BaseExcelListener batchCount(int batchCount) { this.batchCount = batchCount; return this; } /** * <p>读取时抛出异常是否继续读取.</p> * <p>true:跳过继续读取 , false:停止读取 , 默认true .</p> */ private boolean continueAfterThrowing = true; /** * 设置抛出解析过程中抛出异常后是否跳过继续读取下一行 * * @param continueAfterThrowing 解析过程中抛出异常后是否跳过继续读取下一行 * @return this */ public BaseExcelListener continueAfterThrowing(boolean continueAfterThrowing) { this.continueAfterThrowing = continueAfterThrowing; return this; } /** * 读取过程中发生异常被跳过的行数记录 * String 为 sheetNo * List<Integer> 为 错误的行数列表 */ // TODO: 2019年10月28日 改为不需要通过Map进行转换 private Map<String, List<Integer>> errRowsMap = new HashMap<>(); /** * 错误行号的pojo形式 */ private List<ErrRows> errRowsList = new ArrayList<>(); /** * 获取错误的行号,以pojo的形式返回 * * @return 错误的行号 */ public List<ErrRows> getErrRowsList() { errRowsMap.forEach((sheetNo, rows) -> errRowsList.add(new ErrRows().setSheetNo(sheetNo).setErrRows(rows))); return errRowsList; } /** * 每解析一行会回调invoke()方法。 * 如果当前行无数据,该方法不会执行, * 也就是说如果导入的的excel表无数据,该方法不会执行, * 不需要对上传的Excel表进行数据非空判断 * * @param object 当前读取到的行数据对应的java模型对象 * @param context 定义了获取读取excel相关属性的方法 */ @Override public void invoke(Model object, AnalysisContext context) { log.info("解析到一条数据:{}", object); if (!validateBeforeAddData(object)) { throw new ExcelAnalysisException("数据校验不合法!"); } // 数据存储到list,供批量处理,或后续自己业务逻辑处理。 data.add(object); //如果continueAfterThrowing 为false 时保证数据插入的原子性 if (data.size() >= batchCount && continueAfterThrowing) { doService(); data.clear(); } } /** * 该方法用于对读取excel过程中对每一行的数据进行校验操作, * 如果不需要对每行数据进行校验,则直接返回true即可. * * @param object 读取到的数据对象 * @return 校验是否通过 true:通过 ; false:不通过 */ public abstract boolean validateBeforeAddData(Model object); /** * 对暂存数据的业务逻辑方法 . * 相关逻辑可以在该方法体内编写, 例如入库. */ public abstract void doService(); // { // log.info("模拟写入数据库"); // log.info("/*------- {} -------*/", JSON.toJSONString(data)); // data.clear(); // } /** * 解析监听器 * 每个sheet解析结束会执行该方法 * * @param context 定义了获取读取excel相关属性的方法 */ @Override public void doAfterAllAnalysed(AnalysisContext context) { doService(); log.info("/*------- 当前sheet读取完毕,sheetNo : {} , 读取错误的行号列表 : {} -------*/", getCurrentSheetNo(context), JSON.toJSONString(errRowsMap)); data.clear();//解析结束销毁不用的资源 } /** * 在转换异常 获取其他异常下会调用本接口。抛出异常则停止读取。如果这里不抛出异常则继续读取下一行。 * 如果不重写该方法,默认抛出异常,停止读取 * * @param exception exception * @param context context */ @Override public void onException(Exception exception, AnalysisContext context) throws Exception { // 如果continueAfterThrowing为false,则直接将异常抛出 if (!continueAfterThrowing) { throw exception; } Integer sheetNo = getCurrentSheetNo(context); Integer rowIndex = context.readRowHolder().getRowIndex(); log.error("/*------- 读取发生错误! 错误SheetNo:{},错误行号:{} -------*/ ", sheetNo, rowIndex, exception); List<Integer> errRowNumList = errRowsMap.get(String.valueOf(sheetNo)); if (Objects.isNull(errRowNumList)) { errRowNumList = new ArrayList<>(); errRowNumList.add(rowIndex); errRowsMap.put(String.valueOf(sheetNo), errRowNumList); } else { errRowNumList.add(rowIndex); } } /** * 获取当前读取的sheet no * * @param context 定义了获取读取excel相关属性的方法 * @return current sheet no */ private Integer getCurrentSheetNo(AnalysisContext context) { return context.readSheetHolder().getSheetNo(); } }
- 读取时不区分03或07版本 , 底层会自动判断 ;
- 自定义一个listener类继承
BaseExcelListener
package com.luwei.module.easyexcel.listener; import com.luwei.module.easyexcel.pojo.User; import lombok.extern.slf4j.Slf4j; /** * @author WuKun * @since 2019年10月10日 */ @Slf4j public class UserListener extends BaseExcelListener<User> { /** * 这里需要注意入库使用到的Service或者DAO层需要使用到的相关方法时, * 不要通过Spring 使用{@code @Autowired}注入,同时该Listener也不要交由Spring IOC进行管理 * 直接通过构造方法传入相关`xxxService` 或者 `xxxMapper` */ private UserService userService; public UserListener(UserService userService) { this.userService = userService; } @Override void saveData() { // 批量插入数据 userService.saveBatchUsers(this.getData()) log.info("/*------- 写入数据 -------*/"); } }
- 调用工具方法
@Autowired private UserService userService; /** * 读取测试 * * @param excel excel文件 */ @PostMapping("/readExcel") public void readExcel(@RequestParam MultipartFile excel) { List<ErrRows> errRows = EasyExcelUtil.readExcel(excel, User.class, new UserListener(userService)); log.info("/*------- 错误的行号数为 : {}-------*/", JSON.toJSONString(errRows)); }
-
java模型必须要保证无参构造方法存在 , 否则会在读写excel时报无法初始化java模型对象的异常
-
使用java模型读取excel时不能对Java模型使用
@Accessors(chain = true)注解, 会导致数据无法转换 -
sheetNo 从 0开始 , 行号不包括表头 , 例如log中打印的是第9行, 实际在excel中对应的是第10行
2019-10-20 15:34:57.236 INFO 38012 --- [nio-8081-exec-8] c.l.e.listener.BaseExcelListener : /*------- 当前sheet读取完毕,sheetNo : 1 , 读取错误的行号列表 : {"1":[9]} -------*/