封面来源:碧蓝航线 蝶海梦花 活动CG
本文参考:【编程不良人 EasyPOI】 与 【黑马程序员 EasyExcel】
1. Easy POI 简介
在说 Easy POI 之前,咱们先说说 Apache POI:
Apache POI 是基于 Office Open XML 标准(OOXML)和 Microsoft 的 OLE 2 复合文档格式(OLE2)处理各种文件格式的开源项目。 简而言之,我们可以使用 Java 读写 MS Excel 文件,可以使用 Java 读写 MS Word 和 MS PowerPoint 文件。
Apache POI 功能很强大,但是它代码写起来很头疼,😵 于是国内的“大牛” 🐮 就对 Apache POI 进行了二次封装,Easy POI 也就应运而生了。
Easy POI 功能如同名字 easy,主打的功能就是容易,让一个没见接触过 POI 的人员就可以方便的写出 Excel 导出、Excel 模板导出、Excel 导入、Word 模板导出,通过简单的注解和模板语言(熟悉的表达式语法),完成以前复杂的写法。👍
那 EasyPOI 为谁而开发:🤔
不太熟悉 POI 的
不想写太多重复代码的
只是简单进行导入导出的
喜欢使用模板的
Easy POI Gitee 仓库
不得不说,官网这个语言描述、文章排版和中文用英文符号宛如一坨狗屎,SHIT!💩 🤢
2. Easy POI 快速开始
2.1 相关注解说明
先导入依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <dependency > <groupId > cn.afterturn</groupId > <artifactId > easypoi-base</artifactId > <version > 3.2.0</version > </dependency > <dependency > <groupId > cn.afterturn</groupId > <artifactId > easypoi-web</artifactId > <version > 3.2.0</version > </dependency > <dependency > <groupId > cn.afterturn</groupId > <artifactId > easypoi-annotation</artifactId > <version > 3.2.0</version > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > 1.18.12</version > <scope > provided</scope > </dependency >
注意: 如果导入实体的某一时间类型属性是 Java8 的时间类型,需要使用 4.1 以上的版本!
Easy POI 最初就是为了 Excel 的导入导出,最初的模板是实体和 Excel 的对应,model 对应 row,filed 对应 col。利用注解我们可以很容易做到 Excel 的导入导出,现在有 5 个注解,分别是:
@Excel:作用于某一字段上,是对 Excel 一列的描述
@ExcelCollection:表示一个集合,针对一对多的导出。比如一个老师对应多个科目,科目就可以用集合表示
@ExcelEntity:表示一个继续深入导出的实体,但它没有太多的实际意义,只是告诉系统这个对象里有需要导出的字段
@ExcelIgnore:忽略作用字段的导入导出,类似于 Jackson 的 @JsonIgnore
@ExcelTarget:作用于最外层的对象,描述这个对象的 id(唯一),以便支持一个对象可以针对不同导出做出不同处理
@ExcelTarget
作用在实体类上,表示当前实体类时一个可以通过 Easy POI 导入导出的实体类。
常用属性:
value
属性:【String】定义 id 的唯一标识,不能重复
height
属性:【Double】定义单元格高度
fontSize
属性:【short】定义单元格字体大小
@Excel
作用于实体类的属性上,是对 Excel 一列的描述。
常用属性:
name
:【String】生成Excel表格中列名
needMerge
:【boolean】是否需要纵向合并单元格(用于含有 List 中,合并由 List 创建的多个 row)
orderNum
:【String】指定生成 Excel 中列的顺序,按照数字自然顺序排序
savePath
:【String】指定导出 Excel 中图片的保存路径
type
:【String】导出类型。1 是文本,2 是图片,3 是函数,10 是数字,默认是文本
width
:【Double】指定导出 Excel 时列的宽度
isImportField
:【boolean】是否是导入字段,如果没有说明是错误的 Excel
exportFormat
:【String】导出 Excel 的时间格式
importFormat
:【String】导入 Excel 的时间格式
format
:【String】相当于同时设置了 exportFormat
和 importFormat
imageType
:【int】导出类型。1 是从 file 读取,2 是从数据库中读取,默认是文件,同样导入也是
suffix
:【String】文字后缀,如 90 变成 90%
replace
:【String[]】值的替换。例如将 0 替换为锁定,那么就该写成 replace = {"锁定_0"}
,由于其类型是一个属性,因此可以书写多个替换。
@ExcelCollection
一对多的集合注释,用以标记集合是否被数据以及集合的整体排序。
常用属性:
name
:【String】定义集合列名
orderNum
:【int】用来指定导出 Excel 集合内列的顺序
type
:【Class】用来指定导出创建对象的类型
2.2 导出基本数据
注意: 导出 Excel 的对象必须实现对象序列化接口。
一个实体类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Getter @Setter @Accessors(chain = true) @ExcelTarget("user") public class User implements Serializable { @Excel(name = "编号", orderNum = "0") private String id; @Excel(name = "姓名", orderNum = "1") private String name; @Excel(name = "年龄", orderNum = "3", suffix = " &", replace = {"10岁_10", "11岁_11"}) private Integer age; @Excel(name = "生日", width = 15.0, format = "yyyy-MM-dd", orderNum = "2") private Date birth; @Excel(name = "状态", replace = {"激活_1", "锁定_0"}) private int status; }
测试类,导出数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public class POITest { private int USER_NUM = 5 ; private List<User> getUser () { List<User> users = new ArrayList <>(); for (int i = 0 ; i < USER_NUM; i++) { User user = new User (); user.setId(String.valueOf(i)).setName("默烦_" + i) .setAge(10 + i).setBirth(new Date ()); if (i % 2 == 0 ) { user.setStatus(0 ); } else { user.setStatus(1 ); } users.add(user); } return users; } @Test public void testExport () { List<User> users = getUser(); try ( Workbook workbook = ExcelExportUtil.exportExcel(new ExportParams ("用户信息列表" , "用户信息" ), User.class, users); FileOutputStream fileOutputStream = new FileOutputStream ("D:\\test.xls" )) { workbook.write(fileOutputStream); } catch (IOException e) { e.printStackTrace(); } } }
导出的 Excel 样式:
2.3 导出集合与对象
导出集合
给 User
实体类添加一个集合字段 hobbies
,表示用户的爱好。
1 2 3 4 @Excel(name = "状态", replace = {"激活_1", "锁定_0"}, orderNum = "4") private int status; @Excel(name = "爱好", width = 20.0, orderNum = "5") private List<String> hobbies;
修改 getUsers()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 private List<User> getUser () { List<User> users = new ArrayList <>(); for (int i = 0 ; i < USER_NUM; i++) { User user = new User (); user.setId(String.valueOf(i)).setName("默烦_" + i) .setAge(10 + i).setBirth(new Date ()); if (i % 2 == 0 ) { user.setStatus(0 ); user.setHobbies(Arrays.asList("dance" , "rap" , "basketball" )); } else { user.setStatus(1 ); user.setHobbies(Arrays.asList("read" , "code" , "write" )); } users.add(user); } return users; }
运行程序,查看 Excel:
从上面的运行结果可以看出,导出的格式默认是上图中的样式,如果想要自定义导出样式应该怎么办呢?🤔
自定义导出格式可以在对应的 GET 方法中进行处理。
修改 User
实体类,增加一个表示爱好的字符串属性,并修改它的默认 GET 方法:
1 2 3 4 5 6 7 8 9 10 @ExcelIgnore private List<String> hobbies;@Excel(name = "爱好", width = 20.0, orderNum = "5") private String hobbyStr;public String getHobbyStr () { StringBuilder sb = new StringBuilder (); hobbies.forEach(s -> sb.append(s).append("-" )); return sb.toString(); }
运行程序,查看 Excel:
导出对象
假设每个用户有自己的身份证,身份证上有身份证号码和籍贯,那怎么样在导出数据时一并导出身份证号码和籍贯?
创建 IdentityCard
实体,别忘记实现序列化接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 @ExcelTarget("card") @Getter @Setter @Accessors(chain = true) public class IdentityCard implements Serializable { @Excel(name = "身份证号码", width = 20.0, orderNum = "6") private String num; @Excel(name = "籍贯", width = 40.0, orderNum = "7") private String address; }
然后给 User
实体添加一个属性:
1 2 @ExcelEntity private IdentityCard identityCard;
再修改 getUsers()
方法,添加身份证信息的获取:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private List<User> getUser () { List<User> users = new ArrayList <>(); for (int i = 0 ; i < USER_NUM; i++) { User user = new User (); IdentityCard identityCard = new IdentityCard (); identityCard.setNum("123456789987654321" ); identityCard.setAddress("四川省成都市" ); user.setId(String.valueOf(i)).setName("默烦_" + i) .setAge(10 + i).setBirth(new Date ()) .setIdentityCard(identityCard); if (i % 2 == 0 ) { user.setStatus(0 ); user.setHobbies(Arrays.asList("dance" , "rap" , "basketball" )); } else { user.setStatus(1 ); user.setHobbies(Arrays.asList("read" , "code" , "write" )); } users.add(user); } return users; }
运行程序,查看 Excel:
导出集合对象
我们可以导出对象,可以导出集合,那可以导出集合对象吗?😏
创建一个 Order
表示订单信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Getter @Setter @AllArgsConstructor @NoArgsConstructor @ExcelTarget("orders") public class Order implements Serializable { @Excel(name = "订单编号", orderNum = "8", width = 20.0) private String no; @Excel(name = "订单名称", orderNum = "9", width = 15.0) private String orderName; }
每位用户用多个订单信息:
1 2 @ExcelCollection(name = "订单列表", orderNum = "8") private List<Order> orders;
为初始数据设置订单信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 private List<User> getUser () { List<User> users = new ArrayList <>(); for (int i = 0 ; i < USER_NUM; i++) { User user = new User (); IdentityCard identityCard = new IdentityCard (); identityCard.setNum("123456789987654321" ); identityCard.setAddress("四川省成都市" ); List<Order> orders = new ArrayList <>(); orders.add(new Order ("12" , "衣服" )); orders.add(new Order ("13" , "裤子" )); orders.add(new Order ("14" , "帽子" )); user.setId(String.valueOf(i)).setName("默烦_" + i) .setAge(10 + i).setBirth(new Date ()) .setIdentityCard(identityCard) .setOrders(orders); if (i % 2 == 0 ) { user.setStatus(0 ); user.setHobbies(Arrays.asList("dance" , "rap" , "basketball" )); } else { user.setStatus(1 ); user.setHobbies(Arrays.asList("read" , "code" , "write" )); } users.add(user); } return users; }
运行程序,查看 Excel:
虽然可以成功导出,但是样式不是很美观,我们想要的是一行对应多列,这个时候可以使用 @Excel
的 needMerge
属性,将其设置为 true
即可。比如合并一下编号列:
2.4 导出图片与大数据量
图片的导出
往往随着业务的不断变化,可能需要在导出 Excel 时将图片信息一并导出,比如商品图标、用户头像等,这个时候 Easy POI 应该如何处理呢?😶
比如,在实体类中有一个表示图片路径的属性,就应该这样使用注解:
1 2 @Excel(name = "公司LOGO", type = 2, width = 40, height = 20, imageType = 1) private String companyLogo;
其中 type =2
表示该字段类型为图片,imageType=1
(默认可以不填)表示从 file 中读取,该字段类型是个字符串类型,表示图片的路径。可以用相对路径也可以用绝对路径,会先使用绝对路径依次获取。
那我数据库中是直接存储的图片(比方说使用了 Base64 编码)应该怎么导出呢?
那就需要将属性类型修改为 byte[]
,注解的一些信息也需要修改:😉
1 2 @Excel(name = "公司LOGO", type = 2, width = 40, height = 20, imageType = 2) private byte [] companyLogo;
将 imageType
设置为 2,表示从数据库读取。
为 User
实体类添加一个属性,表示用户头像信息:
1 2 @Excel(name = "头像信息", width = 20.0, height = 40.0,type = 2) private String avatar;
为初始数据设置头像信息:
1 2 3 4 5 6 private List<User> getUser () { user.setAvatar("D:\\Code\\IdeaCode\\Java\\easypoi\\1.jpg" ); }
测试类不变,运行查看导出的数据:
大数据量的导出
大数据导出是当我们导出的数据在几万,甚至几百万时,一次性从数据库中查询这么多数据并加载到内存然后写入,这会对我们的内存和 CPU 都造成巨大的压力,这个时候需要我们像分页一样处理数据的导出,分段写入 Excel 以缓解 Excel 的压力。
注意: 最好在大数据量时进行分页处理,每次导出的数据最好不要超过 1W 条数据。
具体操作就是从数据库中查询数据时使用分页查询,然后一页一页得导入,比如我有 10W 条数据,那么我一次查询 1W 条,使用一个循环导出到 Excel 中。
同时 Easy POI 还为大数据的导出提供了一个新 API,名为 ExcelExportUtil.exportBigExcel()
,但是它与 ExcelExportUtil.exportExcel()
的使用方式没啥区别,只不过导出大数据的使用前者就行了。
2.5 导入基本数据
假设我们现在有这样一份数据:
它对应的实体是 Emp,我们计划将其导入到某处,比如打印到控制台或导入进数据库。
Emp
实体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Setter @Getter @ToString @ExcelTarget("emps") public class Emp implements Serializable { @Excel(name = "编号") private String id; @Excel(name = "姓名") private String name; @Excel(name = "年龄") private Integer age; @Excel(name = "生日", format = "yyyy-MM-dd") private Date birth; @Excel(name = "状态", replace = {"激活_1", "锁定_0"}) private String status; }
在导入 Excel 数据时,需要 @Excel
注解 name 属性值与 Excel 的列名相同,否则无法找到需要导入的列。
导入和导出是十分类似的,属性对应列,如果有其他的设置那么进行设置就行。😮
我们计划将这份数据打印在控制台:🧐
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void testImport () throws Exception{ ImportParams params = new ImportParams (); params.setTitleRows(1 ); params.setHeadRows(2 ); List<Emp> emps = ExcelImportUtil.importExcel(new FileInputStream ("D:\\test1.xls" ), Emp.class, params); emps.forEach(System.out::println); }
为了让框架知道哪些行是标题、哪些行是 Header,记得指定它俩所占的行数。
运行后,控制台打印结果:
Emp(id=0, name=默烦_0, age=10, birth=Wed Jan 20 00:00:00 CST 2021, status=0)
Emp(id=1, name=默烦_1, age=11, birth=Wed Jan 20 00:00:00 CST 2021, status=1)
Emp(id=2, name=默烦_2, age=12, birth=Wed Jan 20 00:00:00 CST 2021, status=0)
Emp(id=3, name=默烦_3, age=13, birth=Wed Jan 20 00:00:00 CST 2021, status=1)
Emp(id=4, name=默烦_4, age=14, birth=Wed Jan 20 00:00:00 CST 2021, status=0)
导入的小技巧
1、读取指定的 sheet:比如要读取 Excel 中第二个 sheet,只需设置 startSheetIndex = 1
即可
2、读取几个 sheet 的内容:比如读取前 2 个 sheet,设置 sheetNum = 2
即可
3、读取第二个到第五个 sheet:设置 startSheetIndex = 1
和 sheetNum = 4
即可
4、判断一个 Excel 是不是合法的 Excel:设置 importFields
的值,表示表头必须至少包含的字段,如果缺一个就是非法的 Excel,那么将不会导入
图片的导入
图片的导入和图片的导出也是类似的,因为需要导入图片(就是将 Excel 的图片保存到本地),所以需要设置图片的保存路径。
使用 saveUrl
属性设置图片保存路径,默认路径为“upload/excelUpload”。
3. Easy Excel 简介
能够“优雅”地操作 Excel 并不只有 Easy POI。众所周知,阿里巴巴总会出各种各样的小工具,或者说类库,比如说操作 JSON 的 FastJSON、为监控而生的数据库连接池 Druid,在操作 Excel 方面,阿里巴巴也出品了一个工具 —— Easy Excel。
先来个 GayHub 地址:EasyExcel 😸
再来个操作手册:EasyExcel 操作手册 📚
吐槽一下:其实我并不想学习 Easy Excel,并不是它不好,而是没有第一个接触到它,首先接触的是 Easy POI,但是 Easy POI 的官网描述是真的蠢 🤦♂,感觉让我小学三年级的表弟来写都比那狗屎写的好,再多看一眼那官网我就难受 🤮,没有办法,只能从零开始学一下 Easy Excel 了。😤
阿里出品的,质量也是有一定的保证,咱们再介绍一下 Apache POI 的特点:
功能强大
代码书写冗余繁杂
读写大文件耗费内存较大,容易 OOM
那 Easy Excel 有什么特点呢?
在数据模型层面进行了封装,使用简单
重写了 07 版本 Excel 的解析代码,降低内存消耗,能够有效避免 OOM
只能操作 Excel(见名识意)
本文也只做简单的介绍,具体使用可以参考 GitHub 的源码。建议将源码 clone 下来,然后用 IDE 打开,找到测试包,关于 Easy Excel 具体的操作里面都有。 🖖
4. Easy Excel 快速开始
4.1 环境准备
创建项目,导入以下依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > 1.18.12</version > <scope > provided</scope > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > easyexcel</artifactId > <version > 2.2.6</version > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-core</artifactId > <version > 5.3.3</version > </dependency > <dependency > <groupId > junit</groupId > <artifactId > junit</artifactId > <version > 4.12</version > <scope > test</scope > </dependency >
创建 Student
实体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Getter @Setter @NoArgsConstructor @AllArgsConstructor @ToString public class Student { private String name; private String gender; private Date birthday; private String id; }
注意: 当使用了 Lombok 后,不能在实体类上添加 @Accessors(chain = true)
注解,否则会出现数据读取为空的现象。
4.2 读数据
我们介绍一下 Excel 的基本知识:
1、工作簿:一个 Excel 文件就是一个工作簿
2、工作表:一个工作簿中可以有多个工作表(sheet)
准备读取的 Excel 文件内容:
创建一个 StudentListener
监听器类,读取时有用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class StudentListener extends AnalysisEventListener <Student> { @Override public void invoke (Student data, AnalysisContext context) { System.out.println("student = " + data); } @Override public void doAfterAllAnalysed (AnalysisContext context) { } }
下面是读取文件的测试类,提供了两种数据读取的方式,一种需要关闭流,另一种不需要:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public class ExcelTest { @Test public void testReadExcel1 () throws IOException { File file = new ClassPathResource ("/excel/readExcel.xlsx" ).getFile(); ExcelReaderBuilder readWorkBook = EasyExcel.read(file, Student.class, new StudentListener ()); ExcelReaderSheetBuilder sheet = readWorkBook.sheet(); sheet.doRead(); } @Test public void testReadExcel2 () throws IOException { File file = new ClassPathResource ("/excel/readExcel.xlsx" ).getFile(); ExcelReader excelReader = EasyExcel.read(file, Student.class, new StudentListener ()).build(); ReadSheet readSheet = EasyExcel.readSheet().build(); excelReader.read(readSheet); excelReader.finish(); } }
运行后控制台打印结果:
student = Student(name=默烦1, gender=男, birthday=Fri Jan 01 00:00:00 CST 2021, id=null)
student = Student(name=默烦2, gender=男, birthday=Sat Jan 02 00:00:00 CST 2021, id=null)
student = Student(name=默烦3, gender=男, birthday=Sun Jan 03 00:00:00 CST 2021, id=null)
student = Student(name=默烦4, gender=男, birthday=Mon Jan 04 00:00:00 CST 2021, id=null)
student = Student(name=默烦5, gender=男, birthday=Tue Jan 05 00:00:00 CST 2021, id=null)
student = Student(name=默烦6, gender=女, birthday=Wed Jan 06 00:00:00 CST 2021, id=null)
student = Student(name=默烦7, gender=女, birthday=Thu Jan 07 00:00:00 CST 2021, id=null)
student = Student(name=默烦8, gender=女, birthday=Fri Jan 08 00:00:00 CST 2021, id=null)
student = Student(name=默烦9, gender=女, birthday=Sat Jan 09 00:00:00 CST 2021, id=null)
student = Student(name=默烦10, gender=女, birthday=Sun Jan 10 00:00:00 CST 2021, id=null)
可以看到数据读取成功,由于准备的数据中并没有 id 信息,因此 id 信息为 null
。
或许你的读出来的数据全为 null
,检查一下 Java 实体类中属性的顺序 是否 和 Excel 中的列头的顺序是相同的。
读取指定 Sheet 的数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Test public void testReadSheet () throws IOException { File file = new ClassPathResource ("/excel/readExcel.xlsx" ).getFile(); EasyExcel.read(file, Student.class, new StudentListener ()).doReadAll(); System.out.println("=================================================" ); ExcelReader excelReader = EasyExcel.read(file).build(); ReadSheet sheet = EasyExcel.readSheet(0 ).head(Student.class) .registerReadListener(new StudentListener ()).build(); excelReader.read(sheet); excelReader.finish(); }
上述代码中有读取所有 Sheet 和读取某个 Sheet,在读取所有 Sheet 时,要保证这个 Sheet 的数据结构都一样。也可以读取某些 Sheet,但是这些 Sheet 的数据格式可能不一样,因此要根据数据格式创建不同的 ReadSheet
对象,然后调用 ExcelReader
的 read()
方法时将这些 ReadSheet
对象全部传入进去就行,因为 read()
方法的参数是可变参数列表。
4.3 写数据
咱们试试将一份数据写入到当前 Module 下,提供了两种数据写入的方式,一种需要关闭流,另一种不需要:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 @Test public void testWriteExcel1 () { String path = "writeExcel_1.xlsx" ; ExcelWriterBuilder writeWorkBook = EasyExcel.write(path, Student.class); ExcelWriterSheetBuilder sheet = writeWorkBook.sheet(); sheet.doWrite(getStudentData()); } @Test public void testWriteExcel2 () { String path = "writeExcel_2.xlsx" ; ExcelWriter excelWriter = EasyExcel.write(path, Student.class).build(); WriteSheet sheet = EasyExcel.writerSheet("学生信息" ).build(); excelWriter.write(getStudentData(), sheet); excelWriter.finish(); } private List<Student> getStudentData () { List<Student> students = new ArrayList <>(); for (int i = 0 ; i < 10 ; i++) { Student student = new Student (); student.setName("mofan" + i); if (i % 2 == 0 ) { student.setGender("boy" ); } else { student.setGender("girl" ); } student.setBirthday(new Date ()); student.setId(i + "" ); students.add(student); } return students; }
运行后,最终生成文件:
那如果我不想让 id 字段的数据写入 Excel,可以这样写:
1 2 3 4 Set<String> set = new HashSet <>(); set.add("id" ); ExcelWriterBuilder writeWorkBook = EasyExcel.write(path, Student.class) .excludeColumnFiledNames(set);
但是还发现列宽不合适,列名也怪怪的,和我们实体的属性名一致,那这可以优化吗?🤔
那肯定是可以的,只需要在实体类的属性或类名上添加一些注解就行了:🥳
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Getter @Setter @NoArgsConstructor @AllArgsConstructor @ToString @ContentRowHeight(20) @HeadRowHeight(40) public class Student { @ExcelProperty("ID") @ExcelIgnore private String id; @ExcelProperty("姓名") @ColumnWidth(10) private String name; @ExcelProperty(value = "性别", index = 2) private String gender; @ExcelProperty(value = "出生日期", index = 1) @ColumnWidth(20) @DateTimeFormat("yyyy-MM-dd") private Date birthday; }
再次运行测试类后,可生成如下格式的 Excel:
那这些注解有什么用?又怎么使用呢?🤨
5. Easy Excel 常见 API 与注解
5.1 常用类
EasyExcel
入口类,用于构建各种对象、开始各种操作。
ExcelReaderBuilder
构建出一个 ReadWorkbook 对象,即一个(读)工作簿对象,对应的是一个 Excel 文件。
ExcelWriterBuilder
构建出一个 WriteWorkbook 对象,即一个(写)工作簿对象,对应的是一个 Excel 文件。
ExcelReaderSheetBuilder
构建出一个 ReadSheet 对象,即一个工作表的对象,对应的 Excel 中的每个 Sheet,一个工作簿可以有多个工作表。
ExcelWriterSheetBuilder
构建出一个 WriteSheet 对象,即一个工作表的对象,对应的 Excel 中的每个 Sheet,一个工作簿可以有多个工作表。
ReadListener
在每一行读取完毕后都会调用 ReadListener 来处理数据, 我们可以把调用 service 的代码可以写在其 invoke()
方法内部。
WriteHandler
在每一个操作包括创建单元格、创建表格等都会调用 WriteHandler 来处理数据,对使用者透明不可见。
需要注意的是: 所有的配置都是继承的,WorkBook 的配置会被 Sheet 继承,所以在用 EasyExcel 设置参数的时候,在 EasyExcel.....sheet()
方法之前的作用域都是整个 WorkBook 的所有 Sheet,之后的配置才针对单个 Sheet。
5.2 常用注解
与 Easy POI 一样,这些注解都是 读写通用 的。
1、@ContentRowHeight
用于设置内容的高度,而 @HeadRowHeight
可以设置头部的高度。
2、@ColumnWidth
用于设置列宽,可作用与属性字段给其在 Excel 中的列设置列宽,也可作用于类,给其下所有字段设置在 Excel 中的列宽。
3、@ExcelProperty
可用于设置写入数据时的列名或列的顺序。列名使用 value
进行设置,它是数组类型的属性,因此如果要 设置复杂的头 ,可以为 value 指定多个值。顺序使用 index
属性进行设置,默认值是 -1,设置时建议从 0 开始,设置为几,对应的列就会出现在第几列。该注解还有一个名为 converter
的属性,它是成员变量转换器。如果不使用 @ExcelProperty
注解,成员变量从上到下的顺序,对应表格中从左到右的顺序。使用这个注解的属性时,要么都使用 index
,要么都使用 value
,不建议在同一个实体类中混合使用这两个属性 。
4、@ExcelIgnore
标注在成员变量上,默认所有字段都会和 Excel 匹配,但会忽略加了该注解的字段。
5、还有一个名为 @ExcelIgnoreUnannotated
的注解,这个注解作用于类上。不标注该注解时,默认类中所有成员变量都会参与读写,无论是否在成员变量上添加了 @ExcelProperty
注解。但在使用了该注解后,类中的成员变量如果没有被标记 @ExcelProperty
,那么它们将不会参与读写。
6、@DateTimeFormat
用于时间格式化,用 String
去接收 Excel 日期格式的数据会调用这个注解。参考 SimpleDateFormat
类,比如:
1 2 3 @NumberFormat("#.##") @NumberFormat("#.##%")
7、还有一个名为 @NumberFormat
的注解,可用于数字格式化,用 String
去接收 Excel 数字格式的数据会调用这个注解。参考 DecimalFormat
类。
5.3 读取 API
读取时通用参数
以下参数是 ReadWorkbook
,ReadSheet
都会有的参数,如果为空,默认使用上级。
converter
转换器,默认加载了很多转换器。也可以自定义。
readListener
监听器,在读取数据的过程中会不断的调用监听器。
headRowNumber
指定需要读表格的列头行数。默认有一行头,也就是认为从第二行开始为数据。
head
与 c1azz
二选一。读取文件头对应的列表,会根据列表匹配数据。建议使用 class
,就是文件中每一行数据对应的代码中的实体类型。
clazz
与 head
二选一。读取文件的头对应的class
,也可以使用注解。如果两个都不指定,则会读取全部数据。
autoTrim
字符串、表头等数据自动移除空格。
password
读的时候是否需要使用密码。
ReadWorkbook(工作簿对象)参数
1、excelType
当前 Excel 的类型,读取时会自动判断,无需设置。
2、inputStream
与 fi1e
二选一。前者表示读取文件的流,建议使用 file
,表示读取的文件。
3、autoClosestream
自动关闭流。
4、readCache
默认小于 5M 用内存,超过 5M 会使用 EhCache,不建议使用这个参数。
5、useDefaultListener
@since 2.1.4
默认会加入 ModelBuildEventListener
来帮忙转换成传入 class
的对象,设置成 false
后将不会协助转换对象,自定义的监听器会接收到 Map<Integer, CellData>
对象,如果还想继续接听到 class
对象,就需要调用 readListener
方法,加入自定义的 beforeListener
、ModelBuildEventListener
、自定义的 afterListener
。
ReadSheet(工作表对象)参数
1、sheetNo
需要读取 Sheet 的编号,建议使用这个来指定读取哪个 Sheet。
2、sheetName
可以根据名字去匹配 Sheet,需要注意的是 Excel 2003 不支持根据名字去匹配。
5.4 写入 API
写入时通用参数
以下参数是 WriteWorkbook
、WriteSheet
都会有的参数,如果为空,默认使用上级。
1、converter
转换器,默认加载了很多转换器,也可以自定义。
2、writeHandler
写的处理器。可以实现 WorkbookWriteHandler
,SheetWriteHandler
,RowWriteHandler
,CellWriteHandler
,在写入 Excel 的不同阶段会调用,对使用者透明不可见。
3、relativeHeadRowIndex
距离多少行后开始,简单来说就是开头空几行。
4、needHead
是否导出头
5、head
与 c1azz
二选一。写入文件的头列表,建议使用class,表示写入文件的头对应的 class,也可以使用注解。
6、autoTrim
字符串、表头等数据自动移除空格
WriteWorkbook(工作簿对象)参数
1、excelType
当前 Excel 的类型,默认为 xlsx
2、outputStream
与 file
二选一,前者表示写入文件的流,后者表示写入的文件
3、templateInputStream
模板的文件流
4、templateFile
模板文件
5、autoCloseStream
自动关闭流。
6、password
写的时候是否需要使用密码
7、useDefaultStyle
写的时候是否是使用默认头
WriteSheet(工作表对象)参数
1、sheetNo
需要写入的编号,默认值是 0。
2、sheetName
需要写的 Sheet 的名称,默认同 sheetNo
。
6.Easy Excel 数据填充
6.1 填充单组数据
我们导出的 Excel 是默认的样式,朴实无华,我想让它变得花哨一点应该这么做呢?
如果使用 Apache POI 的话,需要编写很多代码来进行样式设置。
在 Easy Excel 中,我们可以准备一份 Excel 模板,然后导出的数据就按照模板的来,简单又方便。👍
准备模板
那首先就得准备一份模板,在模板中:
Excel 模板中用 {}
来包裹要填充的变量,如果单元格文本中本来就有 {
、}
左右大括号,那么需要在括号前面使用斜杠进行转义,比如:\{
、\}
。
代码中被填充数据的实体对象的属性名或被填充 Map 集合的 key 需要和 Excel 模板中被{}
包裹的变量名称一致。
而且使用模板进行数据写入时,生成的 Excel 文件会保原留模板的样式。
是不是有点懵?没事,来看个小案例。🤓
这是我们准备的模板:
模板有点丑?要的就是这个效果,看看生成的 Excel 文件是否会保留模板样式。
编写一个简单实体类:
1 2 3 4 5 6 7 8 9 10 11 12 @Getter @Setter @ToString @NoArgsConstructor @AllArgsConstructor public class FileData { private String name; private int age; }
然后编写测试代码并运行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test public void testFillingSingleSetData () throws IOException { File fileTemplate = new ClassPathResource ("/excel/fileTemplate_1.xlsx" ).getFile(); ExcelWriterBuilder excelWriterBuilder = EasyExcel.write("填充单组数据.xlsx" , FileData.class).withTemplate(fileTemplate); ExcelWriterSheetBuilder sheet = excelWriterBuilder.sheet(); FileData fileData = new FileData ("默烦" , 19 ); sheet.doFill(fileData); }
最终生成的 Excel 文件内的数据:
是保留了模板样式的吧~ 😃
当然了,除了使用实体的方式填充数据,还可以使用 Map,修改测试方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void testFillingSingleSetData () throws IOException { File fileTemplate = new ClassPathResource ("/excel/fileTemplate_1.xlsx" ).getFile(); ExcelWriterBuilder excelWriterBuilder = EasyExcel.write("填充单组数据.xlsx" , FileData.class).withTemplate(fileTemplate); ExcelWriterSheetBuilder sheet = excelWriterBuilder.sheet(); Map<String, Object> map = new HashMap <>(16 ); map.put("name" , "mofan" ); map.put("age" , 20 ); sheet.doFill(map); }
运行后也是可以成功填充数据的,就不上截图了哈。
6.2 填充多组数据
实际情况下,写入 Excel 的数据基本不会是单组数据,而是多组数据。
Excel 模板中用 {. }
来表示包裹要填充的多组数据变量,如果单元格文本中本来就有 {
、}
左右大括号,那么需要在括号前面使用斜杠进行转义,比如: \{
、\}
。
代码中被填充数据的实体对象的属性名或被填充 Map 集合的 key 需要和 Excel 模板中被{}
包裹的变量名称一致。
准备填充多组数据的模板:
这次的数据咱们不要实体类进行写入,使用 Map,因此直接上测试方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Test public void testFillingMultipleSetsData () throws IOException { File fileTemplate = new ClassPathResource ("/excel/fileTemplate_2.xlsx" ).getFile(); ExcelWriterBuilder excelWriterBuilder = EasyExcel.write("填充多组数据.xlsx" , FileData.class).withTemplate(fileTemplate); ExcelWriterSheetBuilder sheet = excelWriterBuilder.sheet(); List<Map<String, Object>> data = new ArrayList <>(); for (int i = 0 ; i < 3 ; i++) { Map<String, Object> map = new HashMap <>(); map.put("name" , "mofan_" + i); map.put("age" , 18 + i); data.add(map); } sheet.doFill(data); }
可以看到,填充多组数据和填充单组数据其实是类似的,只不过传入 doFile()
方法的参数类型变成了 List
。
最终生成的 Excel 文件内的数据:
填充成功! 👏
6.3 组合填充
实际情况下填充的多组数据并不只有多组数据,而是单组数据和多组数据都有,我们暂且称其为组合填充。
读写文件会用到 IO 流,最后肯定会关闭流,但是上述编码中我们并没有关闭流,这是怎么回事呢?❓
其实奥秘就藏在 doFill()
方法里,在这个方法里帮我们做了流的关闭。感兴趣可以进入这个方法内部看看。
怎么突然说这个? 因为在组合填充时,我们需要手动关闭流,而不能自动关闭。
还需注意的是: Easy Excel 为了节省内存,所以没有采用把整个文档在内存中组织好之后再整体写入到文件的做法,而采用的是一行一行写入的方式,因此不能实现删除和移动行,也不支持备注写入。在多组数据写入的时候,如果需要新增行,可以在最后一行增加,不能在中间位置添加,或者设置换行。因此,在设计模板时,建议将单组数据放在多组数据之前,以减少换行操作。
来实际操作一下,准备组合填充的模板:
上测试方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Test public void testCombinationFilling () throws IOException { File fileTemplate = new ClassPathResource ("/excel/fileTemplate_3.xlsx" ).getFile(); ExcelWriter workBook = EasyExcel.write("组合填充数据.xlsx" , FileData.class).withTemplate(fileTemplate).build(); WriteSheet sheet = EasyExcel.writerSheet().build(); List<FileData> fileData = new ArrayList <>(); for (int i = 0 ; i < 4 ; i++) { FileData data = new FileData (); data.setName("默烦_" + i); data.setAge(20 + i); fileData.add(data); } Map<String, Object> map = new HashMap <>(); map.put("date" , "2021-01-01" ); map.put("total" , "100" ); FillConfig fillConfig = FillConfig.builder().forceNewRow(true ).build(); workBook.fill(fileData, fillConfig, sheet); workBook.fill(map, sheet); workBook.finish(); }
6.4 水平填充
在填充数据时还可以进行水平填充,水平填充就是横着填充数据,不理解没关系,咱们待会上案例。
水平填充和多组填充模板基本一样,不一样的地方在于填充时需要通过 FillConfig
对象设置水平填充。
水平填充的模板:
上测试方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Test public void testHorizontalFilling () throws IOException { File fileTemplate = new ClassPathResource ("/excel/fileTemplate_4.xlsx" ).getFile(); ExcelWriter workBook = EasyExcel.write("水平填充数据.xlsx" , FileData.class) .withTemplate(fileTemplate).build(); WriteSheet sheet = EasyExcel.writerSheet().build(); List<FileData> fileData = new ArrayList <>(); for (int i = 0 ; i < 4 ; i++) { FileData data = new FileData (); data.setName("默烦_" + i); data.setAge(20 + i); fileData.add(data); } FillConfig fillConfig = FillConfig.builder() .direction(WriteDirectionEnum.HORIZONTAL).build(); workBook.fill(fileData, fillConfig, sheet); workBook.finish(); }
至于为啥有些列或者行的样式怪怪的,是因为我们提供的模板就是那样的,生成的文件会保留模板的样式嘛。😉
7. 图片与样式
本节作为 Easy Excel 数据写入的拓展。
7.1 图片的写入
使用 Easy Excel 也可以完成图片的写入,图片支持 String、URL、File 等类型。
编写一个 ImageData
实体,用于图片的写入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Setter @Getter @NoArgsConstructor @ContentRowHeight(100) @ColumnWidth(100 / 8) public class ImageData { private File file; private InputStream inputStream; @ExcelProperty(converter = StringImageConverter.class) private String string; private byte [] byteArray; private URL url; }
编写一个测试类进行测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 @Test public void testWriteImage () throws IOException { List<ImageData> list = new ArrayList <>(); ImageData imageData = new ImageData (); File file = new ClassPathResource ("/img/avatar.jpg" ).getFile(); imageData.setFile(file); imageData.setInputStream(new FileInputStream (file)); imageData.setString(file.toString()); byte [] data = null ; FileImageInputStream inputStream = new FileImageInputStream (file); ByteArrayOutputStream outputStream = new ByteArrayOutputStream (); byte [] bytes = new byte [1024 ]; int len; while ((len = inputStream.read(bytes)) != -1 ) { outputStream.write(bytes, 0 , len); } data = outputStream.toByteArray(); imageData.setByteArray(data); imageData.setUrl(new URL ("http://i2.hdslb.com/bfs/archive/dd522118202012c937f5870fa0a15cad8f50060a.jpg" )); list.add(imageData); EasyExcel.write("WriteImage.xlsx" , ImageData.class).sheet().doWrite(list); outputStream.close(); inputStream.close(); }
当然了,实际开发场景下不要直接 throws
异常,还是老实 try-catch
。
最终图片写入结果(虽然样式不大好看,但还是成功写入了):
7.2 写入样式
在写入数据时,可以使用一些 注解 对单元格设置样式,比如字体大小、背景色等。
这里给出一个参考的案例,当然最终生成结果远远没达到美化的效果。 😂
一个简单的实体类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @HeadStyle(fillPatternType = FillPatternType.SOLID_FOREGROUND, fillForegroundColor = 10) @HeadFontStyle(fontHeightInPoints = 20) @ContentStyle(fillPatternType = FillPatternType.SOLID_FOREGROUND, fillForegroundColor = 17) @ContentFontStyle(fontHeightInPoints = 20) @HeadRowHeight(40) @ColumnWidth(40) @Getter @Setter @ToString public class StyleData { @HeadStyle(fillPatternType = FillPatternType.SOLID_FOREGROUND, fillForegroundColor = 14) @HeadFontStyle(fontHeightInPoints = 30) @ContentStyle(fillPatternType = FillPatternType.SOLID_FOREGROUND, fillForegroundColor = 40) @ContentFontStyle(fontHeightInPoints = 30) @ExcelProperty("字符串标题") private String string; @ExcelProperty("日期标题") @DateTimeFormat("yyyy-MM-dd") private Date date; @ExcelProperty("数字标题") private Double doubleData; }
一个朴实无华的测试类:😆
1 2 3 4 5 6 7 8 9 10 @Test public void testWriteStyle () { List<StyleData> list = new ArrayList <>(); StyleData data = new StyleData (); data.setDate(new Date ()); data.setDoubleData(212.12 ); data.setString("默烦" ); list.add(data); EasyExcel.write("writeStyle.xlsx" , StyleData.class).sheet().doWrite(list); }
运行后的结果:
啊,奇怪的审美增加了…
7.4 单元格合并
一个简单的、与合并相关的实体类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Setter @Getter @ToString @ColumnWidth(15) public class MergeData { @ContentLoopMerge(eachRow = 2) @ExcelProperty("字符串标题") private String string; @ExcelProperty("日期标题") @DateTimeFormat("yyyy-MM-dd") private Date date; @ExcelProperty("数字标题") private Double doubleData; }
一个朴实无华的测试方法:
1 2 3 4 5 6 7 8 9 10 11 12 @Test public void testWriteMerge () { ArrayList<MergeData> list = new ArrayList <>(); for (int i = 0 ; i < 5 ; i++) { MergeData mergeData = new MergeData (); mergeData.setString("mofan-" + i); mergeData.setDate(new Date ()); mergeData.setDoubleData(2.12 + i); list.add(mergeData); } EasyExcel.write("writeMerge.xlsx" , MergeData.class).sheet().doWrite(list); }
写入成功后的数据:
使用 @ContentLoopMerge
可以让某一列中的若干行进行合并。
还有一个更牛掰的注解 @OnceAbsoluteMerge
,它可以指定从哪一行开始,哪一行结束,哪一列开始,哪一列结束,然后进行合并单元格。
试一下 ⚔️ 给实体类用上这个注解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @Setter @Getter @ToString @ColumnWidth(15) @OnceAbsoluteMerge(firstRowIndex = 1,lastRowIndex = 2, firstColumnIndex = 1,lastColumnIndex = 2) public class MergeData { @ExcelProperty("字符串标题") private String string; @ExcelProperty("日期标题") @DateTimeFormat("yyyy-MM-dd") private Date date; @ExcelProperty("数字标题") private Double doubleData; }
再来运行一下测试类,瞄一瞄写入成功后的数据:
到此,Easy Excel 的基本内容就介绍完毕了,更全面的使用请参考官方操作手册:EasyExcel 操作手册
使用 Java 操作 Excel 完