Java 后端程序员应该会遇到读取 Excel 信息到 DB 等相关需求,脑海中可能忽然间想起 Apache POI 这个技术解决方案,可是当 Excel 的数据量很是大的时候,你也许发现,POI 是将整个 Excel 的内容所有读出来放入到内存中,因此内存消耗很是严重,若是同时进行包含大数据量的 Excel 读操做,很容易形成内存溢出问题html
但 EasyExcel 的出现很好的解决了 POI 相关问题,本来一个 3M 的 Excel 用 POI 须要100M左右内存, 而 EasyExcel 能够将其下降到几 M,同时再大的 Excel 都不会出现内存溢出的状况,由于是逐行读取 Excel 的内容 (老规矩,这里不用过度关心下图,脑海中有个印象便可,看完下面的用例再回看这个图,就很简单了)java
另外 EasyExcel 在上层作了模型转换的封装,不须要 cell 等相关操做,让使用者更加简单和方便,且看git
假设咱们 excel 中有如下内容:程序员
咱们须要新建 User 实体,同时为其添加成员变量github
@Data
public class User {
/** * 姓名 */
@ExcelProperty(index = 0)
private String name;
/** * 年龄 */
@ExcelProperty(index = 1)
private Integer age;
}
复制代码
你也许关注到了 @ExcelProperty
注解,同时使用了 index 属性 (0 表明第一列,以此类推),该注解同时支持以「列名」name 的方式匹配,好比:web
@ExcelProperty("姓名")
private String name;
复制代码
按照 github 文档的说明:面试
不建议 index 和 name 同时用,要么一个对象只用index,要么一个对象只用name去匹配数据库
因此你们能够根据本身的状况自行选择编程
编写测试用例 后端
EasyExcel 类中重载了不少个 read 方法,这里不一一列举说明,请你们自行查看;同时 sheet 方法也能够指定 sheetNo,默认是第一个 sheet 的信息
上面代码的 new UserExcelListener()
异常醒目,这也是 EasyExcel 逐行读取 Excel 内容的关键所在,自定义 UserExcelListener
继承 AnalysisEventListener
@Slf4j
public class UserExcelListener extends AnalysisEventListener<User> {
/** * 批处理阈值 */
private static final int BATCH_COUNT = 2;
List<User> list = new ArrayList<User>(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 的信息呢?
很简单,只是将测试用例的关键代码移动到 Controller 中便可,咱们新建一个 UserController
,在其添加 upload
方法
@RestController
@RequestMapping("/users")
@Slf4j
public 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<User> users);
}
复制代码
新建 IUser 接口实现类 UserServiceImpl:
@Service
@Slf4j
public class UserServiceImpl implements IUser {
@Override
public boolean saveData(List<User> users) {
log.info("UserService {}条数据,开始存储数据库!", users.size());
log.info(JSON.toJSONString(users));
log.info("UserService 存储数据库成功!");
return true;
}
}
复制代码
接下来,在 Controller 中注入 IUser:
@Autowired
private IUser iUser;
复制代码
修改 upload 方法,以匿名内部类重写 listener 方法的形式来实现:
@PostMapping("/uploadWithAnonyInnerClass")
public String uploadWithAnonyInnerClass(MultipartFile file) throws IOException {
EasyExcel.read(file.getInputStream(), User.class, new AnalysisEventListener<User>(){
/** * 批处理阈值 */
private static final int BATCH_COUNT = 2;
List<User> list = new ArrayList<User>();
@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 :
@Slf4j
public 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)).sheet().doRead();
return "success";
}
复制代码
运行结果: 同上
这样更改后,controller 代码看着很清晰,但若是后续业务还有别的 Service 须要注入,咱们难道要一直添加有参构造器吗?很明显,这种方式一样不是很灵活。
其实在使用匿名内部类的时候,你也许会想到,咱们能够经过 Java8 lambda 的方式来解决这个问题
为了解决构造器传参的痛点,同时咱们又但愿 listener 更具备通用性,不必为每一个 Excel 业务都新建一个 listener,由于 listener 都是逐行读取 Excel 数据,只须要将咱们的业务逻辑代码传入给 listener 便可,因此咱们需用到 Consumer<T>
,将其做为构造 listener 的参数。
新建一个工具类 ExcelDemoUtils,用来构造 listener:
咱们看到,getListener 方法接收一个 Consumer<List<T>>
的参数,这样下面代码被调用时,咱们的业务逻辑也就会被相应的执行了:
consumer.accept(linkedList);
复制代码
继续改造 Controller 方法:
运行结果: 同上
到这里,咱们只须要将业务逻辑定制在 batchInsert
方法中:
到这里,关于如何使用 EasyExcel 读取 Excel 信息的基本使用方式已经介绍完了,还有不少细节内容没有讲,你们能够自行查阅 EasyExcel Github 文档去发现更多内容。灵活使用 Java 8 的函数式接口,更容易让你提升代码的复用性,同时看起来更简洁规范
除了读取 Excel 的读取,还有 Excel 的写入,若是须要将其写入到指定位置,配合 HuTool 的工具类 FileWriter 的使用是很是方便的,针对 EasyExcel 的使用,若是你们有什么问题,也欢迎到博客下方探讨
完整代码请在公众号回复「demo」,点开连接,查看「easy-excel-demo」文件夹的内容便可,另外我的博客因为特殊缘由暂时关闭首页,其余目录访问一切正常,更多文章能够从 dayarch.top/archives 入口查看
很是感谢 EasyExcel 的做者 🌹🌹,让 Excel 的读写更加方便
欢迎持续关注公众号:「日拱一兵」
- 前沿 Java 技术干货分享
- 高效工具汇总 | 回复「工具」
- 面试问题分析与解答
- 技术资料领取 | 回复「资料」
以读侦探小说思惟轻松趣味学习 Java 技术栈相关知识,本着将复杂问题简单化,抽象问题具体化和图形化原则逐步分解技术问题,技术持续更新,请持续关注......
2019.10.24 补充说明
看到评论区有童鞋针对Lambda方式有一些困惑,因而单独补充说明于此
很是坦诚的说,这种书写方式一开始我也不会,多亏了朋友的点播,在此先感谢。
Lambda的诞生大大方便了咱们集合的处理,咱们天天处理集合,也就容易让咱们忽略Lambda 函数式接口出发点是解决了匿名内部类的代码臃肿问题,好比教程中常被说起的() -> System.out.println("hello world");
public void testRunnable(Runnable runnable){
runnable.run();
}
@Test
public void callTestRunnable1(){
testRunnable(new Runnable() {
@Override
public void run() {
System.out.println("hello Runnable");
}
});
}
@Test
public void callTestRunnable(){
testRunnable(() -> System.out.println("hello Runnable"));
}
复制代码
到这里有朋友可能会说怎么将 AnalysisEventListener
匿名内部类转换为 Lambda 方式呢?答案是不能, 这违背了函数式接口的定义(只有一个抽象方法,其余都为 default),回看上面的代码,我只是把匿名内部类的方式放到了工具类 ExcelDemoUtils 里面,这种方式较传统方式有了进步,咱们使用了 Java 泛型,除了 User 实体,咱们能够处理任何实体,请看代码:
public static <T> AnalysisEventListener<T> getListener() {
return new AnalysisEventListener<T>() {
private static final int BATCH_COUNT = 2;
List<T> list = new ArrayList<T>();
@Override
public void invoke(T 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("存储数据库成功!");
}
};
}
复制代码
这里写的工具类不具有业务落地的可能性,一般咱们要将读取到的 Excel 内容 (list) 执行特定业务逻辑最终持久化到 DB,这句话按照 Lambda 翻译过来就是(list) -> 特定业务逻辑的执行
这正好匹配 Consumer 函数式接口的 accept(T t) 方法,因此咱们为上面的 getListener 方法添加 Consumer 类型的参数就行了,这样调用 getListener 方法咱们须要传递 Consumer 类型的参数,当执行 consumer.accept 方法时,就会执行咱们写的 batchInsert 方法里面的业务逻辑
因此说,当基本执行逻辑固定,只有局部须要特定业务处理的,咱们均可以使用函数式接口来传参数,记住这个思惟定式就好
欢迎关注上面二维码公众号,将技术落地,趣谈Coding那些事,最后1024节日快乐