专栏名称: 武哥聊编程
这里有技术,有段子,有生活,也有资源,要不然怎么叫 “私房菜” 呢?
目录
相关文章推荐
国家外汇管理局  ·  习近平对四川宜宾市筠连县山体滑坡作出重要指示 ·  昨天  
手游那点事  ·  全球手游收入Top20:《王者荣耀》空降第一 ... ·  3 天前  
手游那点事  ·  又是腾讯?一款收入超10亿美金的二游出现了 ·  3 天前  
国家外汇管理局  ·  习近平和彭丽媛为出席哈尔滨第九届亚洲冬季运动 ... ·  2 天前  
国家外汇管理局  ·  国家外汇管理局公布2025年主要统计数据发布时间表 ·  3 天前  
51好读  ›  专栏  ›  武哥聊编程

读取 Excel 还用 POI?试试这款开源工具

武哥聊编程  · 公众号  ·  · 2020-03-03 08:45

正文

写在前面

Java 后端程序员应该会遇到读取 Excel 信息到 DB 等相关需求,脑海中可能突然间想起 Apache POI 这个技术解决方案,但是当 Excel 的数据量非常大的时候,你也许发现,POI 是将整个 Excel 的内容全部读出来放入到内存中,所以内存消耗非常严重,如果同时进行包含大数据量的 Excel 读操作,很容易造成内存溢出问题

但 EasyExcel 的出现很好的解决了 POI 相关问题,原本一个 3M 的 Excel 用 POI 需要100M左右内存, 而 EasyExcel 可以将其降低到几 M,同时再大的 Excel 都不会出现内存溢出的情况,因为是逐行读取 Excel 的内容 (老规矩,这里不用过分关心下图,脑海中有个印象即可,看完下面的用例再回看这个图,就很简单了)

另外 EasyExcel 在上层做了模型转换的封装,不需要 cell 等相关操作,让使用者更加简单和方便,且看

简单读

假设我们 excel 中有以下内容:

我们需要新建 User 实体,同时为其添加成员变量

@Datapublic class User {
/** * 姓名 */ @ExcelProperty(index = 0) private String name;
/** * 年龄 */ @ExcelProperty(index = 1) private Integer age;}

你也许关注到了 @ExcelProperty 注解,同时使用了 index 属性 (0 代表第一列,以此类推),该注解同时支持以「列名」name 的方式匹配,比如:

@ExcelProperty("姓名")private String name;

按照 github 文档的说明:

不建议 index 和 name 同时用,要么一个对象只用index,要么一个对象只用name去匹配

  1. 如果读取的 Excel 模板信息列固定,这里建议以 index 的形式使用,因为如果用名字去匹配,名字重复,会导致只有一个字段读取到数据,所以 index 是更稳妥的方式

  2. 如果 Excel 模板的列 index 经常有变化,那还是选择 name 方式比较好,不用经常性修改实体的注解 index 数值

所以大家可以根据自己的情况自行选择

编写测试用例

EasyExcel 类中重载了很多个 read 方法,这里不一一列举说明,请大家自行查看;同时 sheet 方法也可以指定 sheetNo,默认是第一个 sheet 的信息

上面代码的 new UserExcelListener() 异常醒目,这也是 EasyExcel 逐行读取 Excel 内容的关键所在,自定义 UserExcelListener 继承 AnalysisEventListener

@Slf4jpublic class UserExcelListener extends AnalysisEventListener {
/** * 批处理阈值 */ private static final int BATCH_COUNT = 2; List list = new ArrayList(BATCH_COUNT);
@Override public void invoke(User user, AnalysisContext analysisContext) { log.info("解析到一条数据:{}", JSON.toJSONString(user)); list.add(user); if (list.size() >= BATCH_COUNT) { saveData(); list.clear(); } }
@Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { saveData(); log.info("所有数据解析完成!"); }
private void saveData(){ log.info("{}条数据,开始存储数据库!", list.size()); log.info("存储数据库成功!"); }}
到这里请回看文章开头的 EasyExcel 原理图,invoke 方法逐行读取数据,对应的就是订阅者 1;doAfterAllAnalysed 方法对应的就是订阅者 2,这样你理解了吗?


打印结果:

从这里可以看出,虽然是逐行解析数据,但我们可以自定义阈值,完成数据的批处理操作,可见 EasyExcel 操作的灵活性

自定义转换器

这是最基本的数据读写,我们的业务数据通常不可能这么简单,有时甚至需要将其转换为程序可读的数据

性别信息转换

比如 Excel 中新增「性别」列,其性别为男/女,我们需要将 Excel 中的性别信息转换成程序信息: 「1: 男;2:女」

首先在 User 实体中添加成员变量 gender:

@ExcelProperty(index = 2)private Integer gender;

EasyExcel 支持我们自定义 converter,将 excel 的内容转换为我们程序需要的信息,这里新建 GenderConverter,用来转换性别信息

public class GenderConverter implements Converter<Integer> {
public static final String MALE = "男"; public static final String FEMALE = "女";
@Override public Class supportJavaTypeKey() { return Integer.class; }
@Override public CellDataTypeEnum supportExcelTypeKey() { return CellDataTypeEnum.STRING; }
@Override public Integer convertToJavaData(CellData cellData, ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception { String stringValue = cellData.getStringValue(); if (MALE.equals(stringValue)){ return 1; }else { return 2; } }
@Override public CellData convertToExcelData(Integer integer, ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception { return null; }}

上面程序的 Converter 接口的泛型是指要转换的 Java 数据类型,与 supportJavaTypeKey 方法中的返回值类型一致

打开注解 @ExcelProperty 查看,该注解是支持自定义 Converter 的,所以我们为 User 实体添加 gender 成员变量,并指定 converter

/** * 性别 1:男;2:女 */@ExcelProperty(index = 2, converter = GenderConverter.class)private Integer gender;

来看运行结果:

数据按照我们预期做出了转换,从这里也可以看出,Converter 可以一次定义到处是用的便利性

日期信息转换

日期信息也是我们常见的转换数据,比如 Excel 中新增「出生年月」列,我们要解析成 yyyy-MM-dd 格式,我们需要将其进行格式化,EasyExcel 通过 @DateTimeFormat 注解进行格式化

在 User 实体中添加成员变量 birth ,同时应用 @DateTimeFormat 注解,按照要求做格式化

/** * 出生日期 */@ExcelProperty(index = 3)@DateTimeFormat("yyyy-MM-dd HH:mm:ss")private String birth;

来看运行结果:

如果这里你指定 birth 的类型为 Date,试试看,你得到的结果是什么?

到这里都是以测试的方式来编写程序代码,作为 Java Web 开发人员,尤其在目前主流 Spring Boot 的架构下,所以如何实现 Web 方式读取 Excel 的信息呢?

web 读

简单 Web

很简单,只是将测试用例的关键代码移动到 Controller 中即可,我们新建一个 UserController ,在其添加 upload 方法

@RestController@RequestMapping("/users")@Slf4jpublic class UserController {    @PostMapping("/upload")    public String upload(MultipartFile file) throws IOException {        EasyExcel.read(file.getInputStream(), User.class, new UserExcelListener()).sheet().doRead();        return "success";    }}


其实在写测试用例的时候你也许已经发现,listener 是以 new 的形式作为参数传入到 EasyExcel.read 方法中的,这是不符合 Spring IoC 的规则的,我们通常读取 Excel 数据之后都要针对读取的数据编写一些业务逻辑的,而业务逻辑通常又会写在 Service 层中,我们如何在 listener 中调用到我们的 service 代码呢?

先不要向下看,你脑海中有哪些方案呢?

匿名内部类方式

匿名内部类是最简单的方式,我们需要先新建 Service 层的信息: 新建 IUser 接口:

public interface IUser {    public boolean saveData(List users);}

新建 IUser 接口实现类 UserServiceImpl:

@Service@Slf4jpublic class UserServiceImpl implements IUser {    @Override    public boolean saveData(List users) {        log.info("UserService {}条数据,开始存储数据库!", users.size());        log.info(JSON.toJSONString(users));        log.info("UserService 存储数据库成功!");        return true;    }}

接下来,在 Controller 中注入 IUser:

@Autowiredprivate IUser iUser;

修改 upload 方法,以匿名内部类重写 listener 方法的形式来实现:

@PostMapping("/uploadWithAnonyInnerClass")    public String uploadWithAnonyInnerClass(MultipartFile file) throws IOException {        EasyExcel.read(file.getInputStream(), User.class, new AnalysisEventListener(){            /**             * 批处理阈值             */            private static final int BATCH_COUNT = 2;            List list = new ArrayList();
@Override public void invoke(User user, AnalysisContext analysisContext) { log.info("解析到一条数据:{}", JSON.toJSONString(user)); list.add(user); if (list.size() >= BATCH_COUNT) { saveData(); list.clear(); } }
@Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { saveData(); log.info("所有数据解析完成!"); }
private void saveData(){ iUser.saveData(list); } }).sheet().doRead(); return "success"; }

查看结果:

这种实现方式,其实这只是将 listener 中的内容全部重写,并在 controller 中展现出来,当你看着这么臃肿的 controller 是不是非常难受?很显然这种方式不是我们的最佳编码实现

构造器传参

在之前分析 SpringBoot 统一返回源码时,不知道你是否发现,Spring 底层源码多数以构造器的形式传参,所以我们可以将为 listener 添加有参构造器,将 Controller 中依赖注入的 IUser 以构造器的形式传入到 listener :

@Slf4jpublic class UserExcelListener extends AnalysisEventListener<User> {
private IUser iUser;
public UserExcelListener(IUser iUser){ this.iUser = iUser; }
// 省略相应代码...
private void saveData(){ iUser.saveData(list); //调用 userService 中的 saveData 方法 }

更改 Controller 方法:

@PostMapping("/uploadWithConstructor")public String uploadWithConstructor(MultipartFile file) throws IOException {    EasyExcel.read(file.getInputStream(), User.class, new UserExcelListener(iUser






请到「今天看啥」查看全文