EasyExcel教程
github: alibaba/easyexcel: 快速、简洁、解决大文件内存溢出的java处理Excel工具 (github.com)
最新版本:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.1.2</version>
</dependency>
Excel 实体对象
正如数据库中的表需要对应一个 Java 实体类,Excel 中的表也有这样的规则。
对于这一块内容可以先稍作了解,建议先去看 读 Excel,途中遇到疑惑的地方再回上来看
实体类指定列名
@ExcelProperty
先介绍这个注解的两个重要属性:
- value,是个字符串数组
- index,代表列的下标
单行表头
姓名 | 学号 | 生日 |
---|---|---|
lgz | 3190421121 | 1999-01-01 |
cjw | 3190421127 | 2000-12-12 |
使用 @ExcelProperty
注解,通过 value
指定列名,或者 index
指定列号
示例:
@Getter
@Setter
@EqualsAndHashCode
public class DemoData {
@ExcelProperty(index = 0)
private String name;
@ExcelProperty("学号")
private String number;
@ExcelProperty("生日")
private Date birthday;
}
同一个实体类中,不建议同时使用 index 和 value 指定列
多行表头
信息 | ||
---|---|---|
姓名 | 学号 | 生日 |
lgz | 3190421121 | 1999-01-01 |
cjw | 3190421127 | 2000-12-12 |
@ExcelProperty
的 value
属性是一个字符串数组
当我们传入多个字符串时,EasyExcel 首先找到所有使用注解的字段中,value
数组最长的一个,然后根据数组的长度锁定行号。
随后,所有注解的 value
只取最后一个字符串的值,来匹配这一行的表头
示例:
@Getter
@Setter
@EqualsAndHashCode
public class DemoData {
// 2. 匹配第二行中的“姓名”表头
@ExcelProperty("姓名")
private String name;
// 1. 这个注解的value最长,匹配第二行,并取最后的字符串“学号”作为表头匹配
@ExcelProperty({"信息", "学号"})
private String number;
// 3. 没有使用注解,那么按照定义的顺序匹配到第二行中的第三列
private Date birthday;
}
下面是特殊示例,有助于理解多行表头的匹配规则:
@Getter
@Setter
@EqualsAndHashCode
// 表格还是上头那一个
public class DemoData {
// 2. 前面的字符串都被忽略,即便aa不存在,这里只取lgz进行第三行的匹配,匹配成功,所以最后获取到的值是 cjw
@ExcelProperty("aa", "lgz")
private String text1;
// 1. 根据长度匹配到第三行,但是没有gg这个表头,所以最终获取的输入值为null
@ExcelProperty({"信息", "love", "gg"})
private String text2;
// 3. 虽然上面一个字段匹配失败,但是text3还是顺位第三,最终获取到的值是 24,也就是第三列的位置
private String text3;
}
当然还有更特殊的
信息 | a | |
---|---|---|
s | ||
姓名 | 学号 | 生日 |
lgz | 3190421121 | 1999-01-01 |
cjw | 3190421127 | 2000-12-12 |
这种情况下,姓名、学号、生日,到底算第二行还是算第三行?
这根据实体类指定了哪些列来判断。
如果实体类只包括左边两列,那么信息单元格只看做一行。这时,姓名和学号算作第二行。
如果实体类还包括了第三列,那么信息单元格看做两行。这时,姓名、学号、生日看做第三行。
关于 EasyExcel 如何解析表头,可以自己写一个 ReadListener 并重写 invokeHead 方法来查看
这里不再给出代码,只贴上一段日志
[INFO] 16:12:10 cn.cimoc.easyexcel.DemoHeadDataListener.invokeHead(DemoHeadDataListener.java:35):
解析到一条头数据
[INFO] 16:12:10 cn.cimoc.easyexcel.DemoHeadDataListener.invokeHead(DemoHeadDataListener.java:37):
0:信息
[INFO] 16:12:10 cn.cimoc.easyexcel.DemoHeadDataListener.invokeHead(DemoHeadDataListener.java:37):
1:null
[INFO] 16:12:10 cn.cimoc.easyexcel.DemoHeadDataListener.invokeHead(DemoHeadDataListener.java:37):
2:a
[INFO] 16:12:10 cn.cimoc.easyexcel.DemoHeadDataListener.invokeHead(DemoHeadDataListener.java:35):
解析到一条头数据
[INFO] 16:12:10 cn.cimoc.easyexcel.DemoHeadDataListener.invokeHead(DemoHeadDataListener.java:37):
0:null
[INFO] 16:12:10 cn.cimoc.easyexcel.DemoHeadDataListener.invokeHead(DemoHeadDataListener.java:37):
1:null
[INFO] 16:12:10 cn.cimoc.easyexcel.DemoHeadDataListener.invokeHead(DemoHeadDataListener.java:37):
2:s
[INFO] 16:12:10 cn.cimoc.easyexcel.DemoHeadDataListener.invokeHead(DemoHeadDataListener.java:35):
解析到一条头数据
[INFO] 16:12:10 cn.cimoc.easyexcel.DemoHeadDataListener.invokeHead(DemoHeadDataListener.java:37):
0:姓名
[INFO] 16:12:10 cn.cimoc.easyexcel.DemoHeadDataListener.invokeHead(DemoHeadDataListener.java:37):
1:学号
[INFO] 16:12:10 cn.cimoc.easyexcel.DemoHeadDataListener.invokeHead(DemoHeadDataListener.java:37):
2:生日
[INFO] 16:12:11 cn.cimoc.easyexcel.DemoHeadDataListener.invoke(DemoHeadDataListener.java:30):
读取1条数据:[text1:lgz, text2:3190421121, text3:1999/1/1]
[INFO] 16:12:11 cn.cimoc.easyexcel.DemoHeadDataListener.invoke(DemoHeadDataListener.java:30):
读取1条数据:[text1:cjw, text2:3190421127, text3:2000/12/12]
字段格式化注解
@DateTimeFormat
该注解包含两个参数:
- value,格式化字符串
- use1904windowing,是否使用1904作为起始时间,因为有的Excel是1900,而有的是1904
value
与 JDK中的 DateTimeFormatter
模式串规则相同,这里提供常用格式,更多格式请参考 JDK
格式 | 含义 |
---|---|
yyyy | 年 |
MM | 月 |
dd | 日 |
HH | 24小时制的小时 |
hh | 12小时制的小时 |
a | 上午/下午 |
mm | 分钟 |
ss | 秒 |
@NumberFormat
该注解包含两个参数:
- value,格式化字符串
- roundingMode,保留小数的规则,默认是四舍五入
value
由 3 种字符构成:#,.,%
- # 号代表数字的位数(只能控制小数位数)
- . 号代表小数点
- % 号代表百分比,若输入数据中不带有百分号,那么最终呈现效果将乘以100
示例:我们将读取如下表格中的“数据”一栏
行号 | 数据 |
---|---|
1 | 22 |
2 | 23.6 |
3 | 24.1234 |
4 | 15% |
5 | 115.345% |
@NumberFormat("#")
读取到的数据分别为 22、24、24、0、1
@NumberFormat("#.")
读取到的数据分别为 22.、24.、24.、0.、1.
@NumberFormat("#.##")
读取到的数据分别为 22、23.6、24.12、0.15、1.15
@NumberFormat("#%")
读取到的数据分别为 2200%、2360%、2412%、15%、115%
@NumberFormat("#.#%")
读取到的数据分别为 2200%、2360%、2412.3%、15%、115.3%
roundingMode
使用 java.math
包下的枚举类 RoundingMode
,JDK中有详细的注释,这里不在赘述
自定义格式化
只需要写一个类,实现 Converter
接口 ,使用的时候在实体属性的 @ExcelProperty
注解中指定 converter
即可
@ExcelProperty(converter = CustomStringStringConverter.class)
private String string;
这里贴上官方的一个示例
public class CustomStringStringConverter implements Converter<String> {
@Override
public Class<?> supportJavaTypeKey() {
return String.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
/**
* 这里读的时候会调用
*
* @param context
* @return
*/
@Override
public String convertToJavaData(ReadConverterContext<?> context) {
return "自定义:" + context.getReadCellData().getStringValue();
}
/**
* 这里是写的时候会调用 不用管
*
* @return
*/
@Override
public WriteCellData<?> convertToExcelData(WriteConverterContext<String> context) {
return new WriteCellData<>(context.getValue());
}
}
回调监听器
介绍
EasyExcel 不会自动帮我们将数据读写到 Java 对象,而是提供了回调监听器,类似于每一段数据的生命周期,在不同的时期,我们可以做不同的操作,这也包括了将数据写入 Java 对象或存入数据库等操作
接口
这是 EasyExcel 提供的监听器接口
public interface ReadListener<T> extends Listener {
/**
* 当读取出现异常时调用的方法
*/
default void onException(Exception exception, AnalysisContext context) throws Exception {
throw exception;
}
/**
* 读取表头(按行)时调用的方法,参数中的headMap一次对应一行的表头
* @param headMap key是列号,value是表头的值
*/
default void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {}
/**
* 读取数据(按行)时调用的方法,参数中的data对应的是一行数据
* @param data 泛型是自定义的实体类,数据是一行的数据
*/
void invoke(T data, AnalysisContext context);
/**
* 读取额外信息时调用
*/
default void extra(CellExtra extra, AnalysisContext context) {}
/**
* 所有数据读取完毕后调用
*/
void doAfterAllAnalysed(AnalysisContext context);
/**
* 判断是否有下一行数据
*/
default boolean hasNext(AnalysisContext context) {
return true;
}
}
PageReadListener
EasyExcel 帮我们封装了一个简单的读取监听器 PageReadListener
其内部包含一个缓存 list 与数量上限 BATCH_COUNT,以及一个函数式接口 Consumer (我们作为使用者唯一需要传入的)。
内部的逻辑是:
每读取一行就放入缓存,当缓存数量大于等于上限时,执行我们传入的方法,然后清空缓存。
除了缓存达到上限能执行方法以外,在所有数据读取完毕后,也将执行一次我们的方法。
源码如下
public class PageReadListener<T> implements ReadListener<T> {
/**
* Single handle the amount of data
*/
public static int BATCH_COUNT = 100;
/**
* Temporary storage of data
*/
private List<T> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
/**
* consumer
*/
private final Consumer<List<T>> consumer;
public PageReadListener(Consumer<List<T>> consumer) {
this.consumer = consumer;
}
@Override
public void invoke(T data, AnalysisContext context) {
cachedDataList.add(data);
if (cachedDataList.size() >= BATCH_COUNT) {
consumer.accept(cachedDataList);
cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
if (CollectionUtils.isNotEmpty(cachedDataList)) {
consumer.accept(cachedDataList);
}
}
}
简单工厂 EasyExcel
EasyExcel 是一个空的类,继承自 EasyExcelFactory,是一个简单工厂,也是我们使用 EasyExcel 的核心入口。
EasyExcelFactory 内部集成了多种读写方式的 builder,根据需要,我们可以使用不同参数的 read
、readSheet
、write
、writeSheet
、writeTable
方法来获取对应的 builder
读 Excel
开始之前
详细代码请参考 官方教程
我们先导入 junit、lombok 和 log4j ,方便测试
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.32</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.32</version>
</dependency>
测试类
@Slf4j
public class ReadTest {
String filePath = "C:\\Users\\11047\\Desktop\\demo.xlsx";
}
实体类
@Getter
@Setter
@EqualsAndHashCode
public class DemoData {
String text1;
String text2;
String text3;
@Override
public String toString() {
return String.format("[text1:%s, text2:%s, text3:%s]", text1, text2, text3);
}
}
Excel 中的数据
姓名 | 生日 | 年龄 |
---|---|---|
lgz | 1999/1/1 | 22 |
cjw | 2000/12/12 | 23 |
简单的读取
前置知识:PageReadListener
@Test
public void simpleRead() {
// 存放读取的数据
List<MyData> list = new ArrayList<>();
// 指定文件路径、实体类、回调监听器
// 这里使用EasyExcel自带的读取监听器,并且将其内部的缓存添加到我们自己定义的 list,达到一个取出数据的作用
EasyExcel.read(filePath, MyData.class, new PageReadListener<MyData>(dataList -> {
list.addAll(dataList);
})).sheet().doRead();
log.info("读取到{}条数据", list.size());
}
这是最简单的读取逻辑,作用也只有一个,那就是读取数据然后存放到我们自己定义的 list 中
如果我们想要更复杂的操作,例如读取的同时存入数据库,那么 PageReadListener 就无法满足了,我们需要自己写一个监听器
自定义监听器
1. 方法一:实现 ReadListener 接口
ReadListener 只有两个方法需要我们重写,分别是读取数据时执行的 invoke
方法,和读取完毕后执行的 doAfterAllAnalysed
方法。
下面的代码模拟存入数据库的过程:
@Slf4j
public class DemoDataListener implements ReadListener<DemoData> {
public static final int BATCH_COUNT = 100;
private List<DemoData> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
@Override
public void invoke(DemoData data, AnalysisContext context) {
log.info("读取1条数据:{}", data);
cachedDataList.add(data);
if (cachedDataList.size() >= BATCH_COUNT) {
saveData();
cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
log.info("数据读取完毕");
saveData();
}
private void saveData() {
log.info("正在存入数据库...");
log.info("存入数据库成功");
}
}
测试:
@Test
public void simpleRead1() {
EasyExcel.read(filePath, DemoData.class, new DemoDataListener()).sheet().doRead();
}
2. 方法二:匿名类
对于逻辑简单,代码量少,而且不需要重复使用的监听器,我们可以直接用匿名类的方式定义
@Test
public void simpleRead2() {
EasyExcel.read(filePath, DemoData.class, new ReadListener<MyData>() {
@Override
public void invoke(Demodata data, AnalysisContext context) {
log.info("读取一条数据:{}", data);
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
log.info("数据读取完毕");
}
}).sheet().doRead();
}
指定列号或列名
上面我们用的实体类是这样的
@Getter
@Setter
@EqualsAndHashCode
public class DemoData {
String text1;
String text2;
String text3;
@Override
public String toString() {
return String.format("[text1:%s, text2:%s, text3:%s]", text1, text2, text3);
}
}
属性名是随便取的,也没有添加任何注解,这个时候 EasyExcel 会按照顺序读取,第一列的给 text1,第二列给 text2,…
如果想要指定列号或者列名,那么需要使用到 @ExcelProperty
注解
为了更高的可读性,我也会用上更合适的属性名
@Getter
@Setter
@EqualsAndHashCode
public class IndexOrNameData {
// 不建议一个类中index和name混用
@ExcelProperty(index = 0)
private String name;
@ExcelProperty("学号")
private String number;
@ExcelProperty("年龄")
private Integer age;
@Override
public String toString() {
return String.format("[name:%s, number:%s, age:%d]", name, number, age);
}
}
关于注解的更多详细说明,可以参考上面的 @ExcelProperty
@Test
public void indexOrNameRead() {
EasyExcel.read(filePath, IndexOrNameData.class, new IndexOrNameDataListener()).sheet().doRead();
}
指定 sheet 表
上面的例子我们都只读取了一张 sheet 表,由于没有指定下标, 所以默认是从第 0 张表开始(这个 0 当然是计算机中下标的起始,应该不用我多说了,实际上对应的就是 Excel 文件中的第一张 sheet)
想要指定很简单,只需要在调用 sheet
方法时传入一个下标参数即可
多次读取
1. 读多张 sheet 表
如果你已经熟练掌握上面的例子,你应该会发现我们只能进行一张 sheet 表的读取,使用的方法是 doRead
点进去看源码,你会发现:读的动作实际使用的是 ExcelReader
对象的 read
方法,随后调用 finish
关闭了文件流
public void doRead() {
if (excelReader == null) {
throw new ExcelGenerateException("Must use 'EasyExcelFactory.read().sheet()' to call this method");
}
excelReader.read(build());
excelReader.finish();
}
那么我们想要读取多张 sheet 表也很简单,只要能拿到 ExcelReader
就可以了,当然,不要忘记在最后关闭文件流
ExcelReader
实现了 Closeable
接口,在 JDK 8 中,我们可以直接用 try-catch 来自动关闭流
// EasyExcel 中的 build 方法可以帮助我们获取到 ExcelReader 实例
try (ExcelReader excelReader = EasyExcel.read(filePath, DemoData.class, new DemoDataListener()).build()) {
}
ExcelReader
的 read
方法接收 ReadSheet
参数,工厂中同样提供了构造方式
ReadSheet sheet = EasyExcel.readSheet(下标).build();
表结构相同
那么如果我们想要读取前两张表,可以这么写:
@Test
public void simpleRead3() {
try (ExcelReader excelReader = EasyExcel.read(filePath, DemoData.class, new DemoDataListener()).build()) {
// 读取第一张
ReadSheet sheet = EasyExcel.readSheet(0).build();
excelReader.read(sheet);
// 读取第二张
ReadSheet sheet = EasyExcel.readSheet(1).build();
excelReader.read(sheet);
}
}
表结构不同
那么我们构造 ExcelReader 时就不需要指定实体类和监听器,而是由后续传入
@Test
public void simpleRead4() {
try (ExcelReader excelReader = EasyExcel.read(filePath).build()) {
// 指定第一张 sheet 表
ReadSheet sheet1 = EasyExcel.readSheet(0)
// 设置实体类
.head(IndexOrNameData.class)
// 设置监听器
.registerReadListener(new IndexOrNameDataListener())
// 构造对象
.build();
// 指定第二张 sheet 表
ReadSheet sheet2 = EasyExcel.readSheet(1)
.head(DemoData.class)
.registerReadListener(new DemoDataListener())
.build();
// 进行读取
excelReader.read(sheet1, sheet2);
}
}
2. 读全部的 sheet 表
直接使用 doReadAll
方法即可
@Test
public void repeatRead() {
EasyExcel.read(filePath, MyData.class, new MyDataListener()).doReadAll();
}
数据类型转换
常用的转换有:
- 日期格式转换 @DateTimeFormat
- 数字格式转换 @NumberFormat
日期格式化示例
@DateTimeFormat("yyyy年MM月dd日 HH时mm分ss秒")
private String date;
数字格式化示例
@NumberFormat(value = "#.##%")
private String doubleData;
自定义格式化
第一步,写一个自定义的格式转换器,需要实现 Converter
接口,泛型是最终返回数据的类型
public class CustomStringStringConverter implements Converter<String> {
// 这里返回支持转换的Java数据类型
@Override
public Class<?> supportJavaTypeKey() {
return String.class;
}
// 这里返回支持转换的Excel数据类型
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
// 这里就是我们自定义的格式化逻辑
@Override
public String convertToJavaData(ReadConverterContext<?> context) throws Exception {
// 将需要格式化的字符串前面加上“自定义”三个字
return "自定义" + context.getReadCellData().getStringValue();
}
// 这里是写入Excel的时候的转换,保持原样就可以
@Override
public WriteCellData<?> convertToExcelData(WriteConverterContext<String> context) throws Exception {
return new WriteCellData<>(context.getValue());
}
}
随后,我们使用 @ExcelProperty 注解指定 converter
复杂表头
信息 | a | |
---|---|---|
s | ||
姓名 | 学号 | 生日 |
lgz | 3190421121 | 1999-01-01 |
cjw | 3190421127 | 2000-12-12 |
如上所示的表格,我们在实体类中该怎么指定呢。
请仔细阅读 @ExcelProperty 部分
表头监听器 & 异常处理
ReadListener
中还有几个不是一定要重写的方法
这一节就要介绍其中的两个
@Slf4j
public class DemoHeadDataListener implements ReadListener<DemoData> {
// onException 在出现异常时执行,并且不会中断程序
@Override
public void onException(Exception exception, AnalysisContext context) throws Exception {
log.error("解析失败,但是继续解析下一行:{}", exception.getMessage());
if (exception instanceof ExcelDataConvertException) {
ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException)exception;
log.error("第{}行,第{}列解析异常,数据为:{}", excelDataConvertException.getRowIndex(), excelDataConvertException.getColumnIndex(), excelDataConvertException.getCellData());
}
}
@Override
public void invoke(DemoData data, AnalysisContext context) {
}
// invokeHead 在解析头数据是执行,也是按行解析
@Override
public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
log.info("解析到一条头数据");
for (Map.Entry<Integer, ReadCellData<?>> entry : headMap.entrySet()) {
log.info("{}:{}", entry.getKey(), entry.getValue().getStringValue());
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
}
}
CellData
CellData 是Excel 的原始数据类型,实体类中的属性可以用 CellData 来包裹
写 Excel
开始之前
准备好一个测试类
@Slf4j
public class WriteTest {
String filePath = "C:\\Users\\11047\\Desktop\\writeDemo.xlsx";
// 准备好一个方法,模拟从数据库获取数据
private List<DemoData> data() {
List<DemoData> list = new ArrayList<>();
DemoData data1 = new DemoData();
DemoData data2 = new DemoData();
list.add(data1);
list.add(data2);
data1.setText1("hello");
data1.setText2("world");
data1.setText3("!!");
data2.setText1("今天");
data2.setText2("是");
data2.setText3("星期一");
return list;
}
}
简单的写
实体类
@Getter
@Setter
@EqualsAndHashCode
public class DemoData {
String text1;
String text2;
String text3;
}
测试方法
@Test
public void simpleWrite() {
// 方法1
EasyExcel.write(filePath, DemoData.class).sheet().doWrite(this::data);
// 方法2
EasyExcel.write(filePath, DemoData.class).sheet().doWrite(data());
}
输出结果
text1 | text2 | text3 |
---|---|---|
hello | world | !! |
今天 | 是 | 星期一 |
指定包含哪些列
测试方法
@Test
public void includeWrite() {
Set<String> includeColumnNames = new HashSet<>();
includeColumnNames.add("text2");
EasyExcel.write(filePath, DemoData.class).includeColumnFieldNames(columnNames).sheet().doWrite(data());
}
输出结果
text2 |
---|
world |
是 |
指定排除哪些列
测试方法
@Test
public void excludeOrIncludeWrite() {
Set<String> excludeColumnNames = new HashSet<>();
excludeColumnNames.add("text2");
EasyExcel.write(filePath, DemoData.class).excludeColumnFieldNames(excludeColumnNames).sheet().doWrite(data());
}
输出结果
text1 | text3 |
---|---|
hello | !! |
今天 | 星期一 |
多次写入
1. 写入同一张 sheet
和多次读取一样,多次写入也是先创建一个 ExcelWriter 对象,每次写入就创建一个 WriteSheet
测试方法
@Test
public void repeatWrite() {
try (ExcelWriter writer = EasyExcel.write(filePath, DemoData.class).build()) {
WriteSheet sheet = EasyExcel.writerSheet().build();
// 这里用循环来模拟多次写入,实际业务中应该从数据库中分页读取
for (int i = 0; i < 5; i++) {
writer.write(data(), sheet);
}
}
}
输出结果
text1 | text2 | text3 |
---|---|---|
hello | world | !! |
今天 | 是 | 星期一 |
hello | world | !! |
今天 | 是 | 星期一 |
hello | world | !! |
今天 | 是 | 星期一 |
hello | world | !! |
今天 | 是 | 星期一 |
hello | world | !! |
今天 | 是 | 星期一 |
2. 写入不同 sheet
同样可以参考多次读取助于理解
新用到的实体类
@Getter
@Setter
@EqualsAndHashCode
public class IndexOrNameData {
// 不建议一个类中index和name混用
@ExcelProperty(index = 0)
private String name;
@ExcelProperty("学号")
private String number;
@ExcelProperty("年龄")
private Integer age;
}
测试方法
@Test
public void repeatWrite2() {
try (ExcelWriter writer = EasyExcel.write(filePath, DemoData.class).build()) {
// 第一张表用DemoData
WriteSheet sheet = EasyExcel.writerSheet(0).head(DemoData.class).build();
writer.write(data(), sheet);
// 第二张表用IndexOrNameData
WriteSheet sheet1 = EasyExcel.writerSheet(1).head(IndexOrNameData.class).build();
writer.write(Collections.emptyList(), sheet1);
}
}
输出结果
第一张 sheet
text1 | text2 | text3 |
---|---|---|
hello | world | !! |
今天 | 是 | 星期一 |
第二张 sheet(只有表头,数据为空,因为参数传入的是空集合)
name | 学号 | 年龄 |
---|---|---|
特殊数据
图片
实体类
@Getter
@Setter
@EqualsAndHashCode
public class ImageDemoData {
/**
* 这些类型都是EasyExcel支持的图片类型,我们只要传入即可,不用关心实际实现
*/
// 方法1:直接以File文件的形式
private File file;
// 方法2:以输入流的形式
private InputStream inputStream;
// 方法3:以图片路径的形式,由于String类型默认是写入文字,所以要指定一个converter
@ExcelProperty(converter = StringImageConverter.class)
private String string;
// 方法4:字节数组,一般都是File或者InputStream转换而来
private byte[] byteArray;
// 方法5:网络图片,只需要url地址
private URL url;
// 方法6:复杂形式
private WriteCellData<Void> writeCellData;
}
1. 基本方法
测试方法
@Test
public void imageWrite() {
String imagePath = "E:\\img\\5.jpg";
try (InputStream inputStream = FileUtils.openInputStream(new File(imagePath))) {
// 创建一行数据
List<ImageDemoData> list = ListUtils.newArrayList();
ImageDemoData imageDemoData = new ImageDemoData();
list.add(imageDemoData);
// 下面五种方法都是图片的导入,实际使用只要选一种方式即可
imageDemoData.setByteArray(FileUtils.readFileToByteArray(new File(imagePath)));
imageDemoData.setFile(new File(imagePath));
imageDemoData.setString(imagePath);
imageDemoData.setInputStream(inputStream);
imageDemoData.setUrl(new URL("https://foruda.gitee.com/avatar/1666774722876138257/8677752_sagiri-kawaii01_1666774722.png!avatar60"));
// 写入
EasyExcel.write(filePath, ImageDemoData.class).sheet().doWrite(list);
} catch (Exception e) {
log.error("", e);
}
}
2. 复杂操作
上面介绍的基本方法,是一个单元格对应一张图片
而实际需求可能是,一个单元格有两个图片,或者既有文字又有图片。那么这时候就需要用 WriteCellData
来详细构造单元格了
测试方法
@Test
public void complexImageWrite() {
String imagePath = "E:\\img\\5.jpg";
try {
// 创建一行数据
List<ImageDemoData> list = ListUtils.newArrayList();
ImageDemoData imageDemoData = new ImageDemoData();
list.add(imageDemoData);
// 创建一个单元格对象
WriteCellData<Void> writeCellData = new WriteCellData<>();
imageDemoData.setWriteCellData(writeCellData);
// 先写入文字
writeCellData.setType(CellDataTypeEnum.STRING);
writeCellData.setStringValue("额外放入文字");
// 准备放入两张图片
List<ImageData> imageDataList = new ArrayList<>();
// ImageData是EasyExcel提供的图片类
ImageData imageData = new ImageData();
// 放入第一张图片(这里只是放入这个对象,图片数据还没读取呢)
imageDataList.add(imageData);
// 单元格设置图片集合
writeCellData.setImageDataList(imageDataList);
// 读取图片
imageData.setImage(FileUtils.readFileToByteArray(new File(imagePath)));
// 设置类型
imageData.setImageType(ImageData.ImageType.PICTURE_TYPE_PNG);
// 上 右 下 左 需要留空
// 这个类似于 css 的 margin
// 这里实测 不能设置太大 超过单元格原始大小后 打开会提示修复。暂时未找到很好的解法。
imageData.setTop(5);
imageData.setRight(40);
imageData.setBottom(5);
imageData.setLeft(5);
// 第二张图片
imageData = new ImageData();
imageDataList.add(imageData);
writeCellData.setImageDataList(imageDataList);
imageData.setImage(FileUtils.readFileToByteArray(new File(imagePath)));
imageData.setImageType(ImageData.ImageType.PICTURE_TYPE_PNG);
imageData.setTop(5);
imageData.setRight(5);
imageData.setBottom(5);
imageData.setLeft(50);
// 设置图片的位置 假设 现在目标 是 覆盖 当前单元格 和当前单元格右边的单元格
// 起点相对于当前单元格为0 当然可以不写
imageData.setRelativeFirstRowIndex(0);
imageData.setRelativeFirstColumnIndex(0);
imageData.setRelativeLastRowIndex(0);
// 前面3个可以不写 下面这个需要写 也就是 结尾 需要相对当前单元格 往右移动一格
// 也就是说 这个图片会覆盖当前单元格和 后面的那一格
imageData.setRelativeLastColumnIndex(1);
// 写入数据
EasyExcel.write(filePath, ImageDemoData.class).sheet().doWrite(list);
} catch (IOException e) {
log.error("", e);
}
}
超链接
实体类
@Getter
@Setter
@EqualsAndHashCode
public class HyperLinkDemoData {
private WriteCellData<String> hyperLink;
}
测试方法
@Test
public void hyperlinkWrite() {
// 创建一行数据
ArrayList<HyperLinkDemoData> data = new ArrayList<>();
HyperLinkDemoData hyperLinkDemoData = new HyperLinkDemoData();
data.add(hyperLinkDemoData);
// 创建单元格,显示的内容是 github
WriteCellData<String> hyperLink = new WriteCellData<>("github");
hyperLinkDemoData.setHyperLink(hyperLink);
// HyperLinkData是EasyExcel提供的超链接类
HyperlinkData hyperlinkData = new HyperlinkData();
// 为单元格设置超链接
hyperLink.setHyperlinkData(hyperlinkData);
// 设置超链接的类型以及地址
hyperlinkData.setHyperlinkType(HyperlinkData.HyperlinkType.URL);
hyperlinkData.setAddress("https://github.com");
// 写入数据
EasyExcel.write(filePath, HyperLinkDemoData.class).sheet().doWrite(data);
}
输出数据
hyperLink |
---|
github |
鼠标点击 github 单元格后,会打开浏览器进入 github 的网站
备注
实体类
@Getter
@Setter
@EqualsAndHashCode
public class CommentDemoData {
private WriteCellData<String> comment;
}
测试方法
@Test
public void commentWrite() {
// 创建一行数据
ArrayList<CommentDemoData> data = new ArrayList<>();
CommentDemoData commentDemoData = new CommentDemoData();
data.add(commentDemoData);
// 创建单元格
WriteCellData<String> comment = new WriteCellData<>("备注的单元格信息");
commentDemoData.setComment(comment);
// CommentData是EasyExcel提供的备注类
CommentData commentData = new CommentData();
comment.setCommentData(commentData);
// 设置备注信息
commentData.setAuthor("Sagiri_kawaii");
commentData.setRichTextStringData(new RichTextStringData("这是一个备注"));
// 备注的默认大小是按照单元格的大小 这里想调整到4个单元格那么大 所以向后 向下 各额外占用了一个单元格
commentData.setRelativeLastColumnIndex(1);
commentData.setRelativeLastRowIndex(1);
// 写入数据,需要打开 inMemory,评论和富文本数据需要在内存中渲染。
EasyExcel.write(filePath, CommentDemoData.class).inMemory(true).sheet().doWrite(data);
}
公式
实体类
@Getter
@Setter
@EqualsAndHashCode
public class FormulaDemoData {
private WriteCellData<String> formula;
}
测试方法
@Test
public void formulaWrite() {
// 创建一行数据
ArrayList<FormulaDemoData> data = new ArrayList<>();
FormulaDemoData formulaDemoData = new FormulaDemoData();
data.add(formulaDemoData);
// 创建单元格
WriteCellData<String> formula = new WriteCellData<>();
formulaDemoData.setFormula(formula);
// FormulaData是EasyExcel提供的公式类
FormulaData formulaData = new FormulaData();
formula.setFormulaData(formulaData);
// 设置公式
// 将 123456789 中的第一个数字替换成 2
// 这里只是例子 如果真的涉及到公式 能内存算好尽量内存算好 公式能不用尽量不用
formulaData.setFormulaValue("REPLACE(123456789, 1, 1, 2)");
// 写入数据
EasyExcel.write(filePath, FormulaDemoData.class).sheet().doWrite(data);
}
样式
实体类
@Getter
@Setter
@EqualsAndHashCode
public class StyleDemoData {
private WriteCellData<String> text1;
private WriteCellData<String> text2;
private WriteCellData<String> text3;
}
单个单元格
1. 单种样式
测试方法
@Test
public void styleWrite() {
// 创建一行数据
ArrayList<StyleDemoData> data = new ArrayList<>();
StyleDemoData styleDemoData = new StyleDemoData();
data.add(styleDemoData);
// 设置单个单元格的样式
WriteCellData<String> cell = new WriteCellData<>("单元格样式");
cell.setType(CellDataTypeEnum.STRING);
styleDemoData.setText1(cell);
// WriteCellStyle是EasyExcel提供的样式类
WriteCellStyle writeCellStyleData = new WriteCellStyle();
cell.setWriteCellStyle(writeCellStyleData);
// 这里需要指定 FillPatternType 为FillPatternType.SOLID_FOREGROUND 不然无法显示背景颜色.
writeCellStyleData.setFillPatternType(FillPatternType.SOLID_FOREGROUND);
// 背景绿色
writeCellStyleData.setFillForegroundColor(IndexedColors.GREEN.getIndex());
// 写入数据
EasyExcel.write(filePath, StyleDemoData.class).sheet().doWrite(data);
}
2. 多种样式
测试方法
@Test
public void styleWrite2() {
// 创建一行数据
ArrayList<StyleDemoData> data = new ArrayList<>();
StyleDemoData styleDemoData = new StyleDemoData();
data.add(styleDemoData);
// 创建单元格
WriteCellData<String> cell = new WriteCellData<>();
cell.setType(CellDataTypeEnum.RICH_TEXT_STRING);
styleDemoData.setText1(cell);
// RichTextStringData是EasyExcel提供的富文本类
RichTextStringData richTextStringData = new RichTextStringData();
cell.setRichTextStringDataValue(richTextStringData);
richTextStringData.setTextString("红色绿色默认");
// 设置前两字为红色
WriteFont writeFont = new WriteFont();
writeFont.setColor(IndexedColors.RED.getIndex());
richTextStringData.applyFont(0, 2, writeFont);
// 设置后两字为绿色
writeFont = new WriteFont();
writeFont.setColor(IndexedColors.GREEN.getIndex());
richTextStringData.applyFont(2, 4, writeFont);
// 写入数据,需要打开 inMemory,富文本数据需要在内存中渲染
EasyExcel.write(filePath, StyleDemoData.class).inMemory(true).sheet().doWrite(data);
}
注解
1. 介绍
样式注解
注解 | 含义 | 使用范围 |
---|---|---|
@HeadStyle | 表头 单元格 样式 | 类、属性 |
@ContentStyle | 内容 单元格 样式 | 类、属性 |
@HeadFontStyle | 表头 字体 样式 | 类、属性 |
@ContentFontStyle | 内容 字体 样式 | 类、属性 |
@ColumnWidth | 列宽 | 类、属性 |
@HeadRowHeight | 表头行高 | 类 |
@ContentRowHeight | 内容行高 | 类 |
@Target({ElementType.FIELD, ElementType.TYPE})
写在类(Type)上相当于写在所有属性(All Field)上
而优先级 Type < Field,即属性上的样式会覆盖类的样式
注解参数
@HeadStyle
与 @ContentStyle
参数相同
@HeadFontStyle
与 @ContentFontStyle
参数相同
@ContentFontStyle
参数 | 类型 | 含义 | 可选值 |
---|---|---|---|
fontName | String | 字体名 | 参考Excel里的字体名 |
fontHeightInPoints | short | 字体大小 | 参考Excel字体大小 |
italic | BooleanEnum | 是否斜体 | TRUE / FALSE |
strikeout | BooleanEnum | 是否加删除线 | TRUE / FALSE |
bold | BooleanEnum | 是否加粗 | TRUE / FALSE |
color | short | 字体颜色 | IndexedColors中枚举了65中颜色,参考对应的数字即可 |
typeOffset | short | 字体位置,上标或下标 | poi 的 Font 中枚举了 SS_开头的 3 个常量 |
underline | byte | 下划线 | poi 的 Font 中枚举了 U_开头的 5 个常量 |
charset | int | 字符集 | poi 的 FontCharset |
@ContentStyle
参数 | 类型 | 含义 | 可选值 |
---|---|---|---|
dataFormat | short | 数据格式化,例如数值、货币、日期、百分比等 | EasyExcel 的 BuiltinFormats |
hidden | BooleanEnum | 是否隐藏,不过目前好像还没有效果 | TRUE / FALSE |
locked | BooleanEnum | 是否锁定,需要配合拦截器 | TRUE / FALSE |
quotePrefix | BooleanEnum | 在单元格前面增加`符号,数字或公式将以字符串形式展示 | TRUE / FALSE |
horizontalAlignment | HorizontalAlignmentEnum | 水平对齐方式 | EasyExcel 的 HorizontalAlignmentEnum |
wrapped | BooleanEnum | 设置文本是否应换行显示 | TRUE / FALSE |
verticalAlignment | VerticalAlignmentEnum | 垂直对齐方式 | EasyExcel 的 VerticalAlignmentEnum |
rotation | short | 设置单元格中文本旋转角度。03版本的Excel旋转角度区间为-90°90°,07版本的Excel旋转角度区间为0°180° | |
indent | short | 文本缩进的空格数量 | |
borderLeft | BorderStyleEnum | 左边框样式 | EasyExcel 的 BorderStyleEnum |
borderRight | BorderStyleEnum | 右边框样式 | EasyExcel 的 BorderStyleEnum |
borderTop | BorderStyleEnum | 上边框样式 | EasyExcel 的 BorderStyleEnum |
borderBottom | BorderStyleEnum | 下边框样式 | EasyExcel 的 BorderStyleEnum |
leftBorderColor | short | 左边框颜色 | IndexedColors中枚举了65中颜色,参考对应的数字即可 |
rightBorderColor | short | 右边框颜色 | IndexedColors中枚举了65中颜色,参考对应的数字即可 |
topBorderColor | short | 上边框颜色 | IndexedColors中枚举了65中颜色,参考对应的数字即可 |
bottomBorderColor | short | 下边框颜色 | IndexedColors中枚举了65中颜色,参考对应的数字即可 |
fillPatternType | FillPatternTypeEnum | 填充类型 | EasyExcel 的 FillPatternTypeEnum |
fillBackgroundColor | short | 背景色 | IndexedColors中枚举了65中颜色,参考对应的数字即可 |
fillForegroundColor | short | 前景色 | IndexedColors中枚举了65中颜色,参考对应的数字即可 |
shrinkToFit | BooleanEnum | 单元格是否自动适应大小 | TRUE / FALSE |
@ColumnWidth
参数 | 类型 | 含义 |
---|---|---|
value | int | 列宽,默认自动,单位(字数) |
@HeadRowHeight
参数 | 类型 | 含义 |
---|---|---|
value | short | 行高,默认自动 |
@ContentRowHeight
参数 | 类型 | 含义 |
---|---|---|
value | short | 行高,默认自动 |
2. 测试
实体类
@Getter
@Setter
@EqualsAndHashCode
// 头背景设置成红色 IndexedColors.RED.getIndex()
@HeadStyle(fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND, fillForegroundColor = 10)
// 头字体设置成20
@HeadFontStyle(fontHeightInPoints = 20)
// 内容的背景设置成绿色 IndexedColors.GREEN.getIndex()
@ContentStyle(fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND, fillForegroundColor = 17)
// 内容字体设置成20
@ContentFontStyle(fontHeightInPoints = 20)
public class DemoStyleData {
// 字符串的头背景设置成粉红 IndexedColors.PINK.getIndex()
@HeadStyle(fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND, fillForegroundColor = 14)
// 字符串的头字体设置成30
@HeadFontStyle(fontHeightInPoints = 30)
// 字符串的内容的背景设置成天蓝 IndexedColors.SKY_BLUE.getIndex()
@ContentStyle(fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND, fillForegroundColor = 40)
// 字符串的内容字体设置成30
@ContentFontStyle(fontHeightInPoints = 30)
@ExcelProperty("字符串标题")
private String string;
@ExcelProperty("日期标题")
private Date date;
@ExcelProperty("数字标题")
private Double doubleData;
}
测试方法
@Test
public void annotationStyleWrite() {
// 创建一行数据
ArrayList<DemoStyleData> data = new ArrayList<>();
DemoStyleData demoStyleData = new DemoStyleData();
data.add(demoStyleData);
demoStyleData.setDoubleData(23.9);
demoStyleData.setString("hhh");
// 写入数据
EasyExcel.write(filePath, DemoStyleData.class).sheet().doWrite(data);
}
拦截器
1. 使用已有的策略
测试方法
@Test
public void handlerStyleWrite1() {
// 方法1 使用已有的策略 推荐
// HorizontalCellStyleStrategy 每一行的样式都一样 或者隔行一样
// AbstractVerticalCellStyleStrategy 每一列的样式都一样 需要自己回调每一页
// 头的策略
WriteCellStyle headWriteCellStyle = new WriteCellStyle();
// 背景设置为红色
headWriteCellStyle.setFillForegroundColor(IndexedColors.RED.getIndex());
WriteFont headWriteFont = new WriteFont();
headWriteFont.setFontHeightInPoints((short)20);
headWriteCellStyle.setWriteFont(headWriteFont);
// 内容的策略
WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
// 这里需要指定 FillPatternType 为FillPatternType.SOLID_FOREGROUND 不然无法显示背景颜色.头默认了 FillPatternType所以可以不指定
contentWriteCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND);
// 背景绿色
contentWriteCellStyle.setFillForegroundColor(IndexedColors.GREEN.getIndex());
WriteFont contentWriteFont = new WriteFont();
// 字体大小
contentWriteFont.setFontHeightInPoints((short)20);
contentWriteCellStyle.setWriteFont(contentWriteFont);
// 这个策略是 头是头的样式 内容是内容的样式 其他的策略可以自己实现
HorizontalCellStyleStrategy horizontalCellStyleStrategy =
new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);
// 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
EasyExcel.write(filePath, DemoData.class)
.registerWriteHandler(horizontalCellStyleStrategy)
.sheet("模板")
.doWrite(data());
}
2. 使用easyexcel的方式完全自己写
测试方法
@Test
public void handlerStyleWrite2() {
// 方法2: 使用easyexcel的方式完全自己写 不太推荐 尽量使用已有策略
EasyExcel.write(filePath, DemoData.class)
.registerWriteHandler(new CellWriteHandler() {
@Override
public void afterCellDispose(CellWriteHandlerContext context) {
// 当前事件会在 数据设置到poi的cell里面才会回调
// 判断不是头的情况 如果是fill 的情况 这里会==null 所以用not true
if (BooleanUtils.isNotTrue(context.getHead())) {
// 第一个单元格
// 只要不是头 一定会有数据 当然fill的情况 可能要context.getCellDataList() ,这个需要看模板,因为一个单元格会有多个 WriteCellData
WriteCellData<?> cellData = context.getFirstCellData();
// 这里需要去cellData 获取样式
// 很重要的一个原因是 WriteCellStyle 和 dataFormatData绑定的 简单的说 比如你加了 DateTimeFormat
// ,已经将writeCellStyle里面的dataFormatData 改了 如果你自己new了一个WriteCellStyle,可能注解的样式就失效了
// 然后 getOrCreateStyle 用于返回一个样式,如果为空,则创建一个后返回
WriteCellStyle writeCellStyle = cellData.getOrCreateStyle();
writeCellStyle.setFillForegroundColor(IndexedColors.RED.getIndex());
// 这里需要指定 FillPatternType 为FillPatternType.SOLID_FOREGROUND
writeCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND);
// 这样样式就设置好了 后面有个FillStyleCellWriteHandler 默认会将 WriteCellStyle 设置到 cell里面去 所以可以不用管了
}
}
}).sheet("模板")
.doWrite(data());
}
3. 使用poi的样式完全自己写
测试方法
@Test
public void handlerStyleWrite3() {
// 方法3: 使用poi的样式完全自己写 不推荐
// 坑1:style里面有dataformat 用来格式化数据的 所以自己设置可能导致格式化注解不生效
// 坑2:不要一直去创建style 记得缓存起来 最多创建6W个就挂了
EasyExcel.write(filePath, DemoData.class)
.registerWriteHandler(new CellWriteHandler() {
@Override
public void afterCellDispose(CellWriteHandlerContext context) {
// 当前事件会在 数据设置到poi的cell里面才会回调
// 判断不是头的情况 如果是fill 的情况 这里会==null 所以用not true
if (BooleanUtils.isNotTrue(context.getHead())) {
Cell cell = context.getCell();
// 拿到poi的workbook
Workbook workbook = context.getWriteWorkbookHolder().getWorkbook();
// 这里千万记住 想办法能复用的地方把他缓存起来 一个表格最多创建6W个样式
// 不同单元格尽量传同一个 cellStyle
CellStyle cellStyle = workbook.createCellStyle();
cellStyle.setFillForegroundColor(IndexedColors.RED.getIndex());
// 这里需要指定 FillPatternType 为FillPatternType.SOLID_FOREGROUND
cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
cell.setCellStyle(cellStyle);
// 由于这里没有指定dataformat 最后展示的数据 格式可能会不太正确
// 这里要把 WriteCellData的样式清空, 不然后面还有一个拦截器 FillStyleCellWriteHandler 默认会将 WriteCellStyle 设置到
// cell里面去 会导致自己设置的不一样
context.getFirstCellData().setWriteCellStyle(null);
}
}
}).sheet("模板")
.doWrite(data());
}
合并单元格
注解
注解 | 含义 | 使用范围 |
---|---|---|
@ContentLoopMerge | 内容合并 | 属性 |
@OnceAbsoluteMerge | 一次性合并 | 类 |
@ContentLoopMerge
参数 | 类型 | 含义 |
---|---|---|
eachRow | int | 纵向合并的高度 |
columnExtend | int | 横向合并的宽度 |
@OnceAbsoluteMerge
参数 | 类型 | 含义 |
---|---|---|
firstRowIndex | int | 需要合并的范围第一行下标 |
lastRowIndex | int | 需要合并的范围最后一行下标 |
firstColumnIndex | int | 需要合并的范围第一列下标 |
lastColumnIndex | int | 需要合并的范围最后一列下标 |
实体类
@Getter
@Setter
@EqualsAndHashCode
// 将第6-7行的2-3列合并成一个单元格
// @OnceAbsoluteMerge(firstRowIndex = 5, lastRowIndex = 6, firstColumnIndex = 1, lastColumnIndex = 2)
public class DemoMergeData {
// 这一列 每隔2行 合并单元格
@ContentLoopMerge(eachRow = 2)
@ExcelProperty("字符串标题")
private String string;
@ExcelProperty("日期标题")
private Date date;
@ExcelProperty("数字标题")
private Double doubleData;
}
测试方法
@Test
public void mergeWrite() {
ArrayList<DemoMergeData> data = new ArrayList<>();
DemoMergeData demoMergeData = new DemoMergeData();
demoMergeData.setString("aaa");
demoMergeData.setDate(new Date());
demoMergeData.setDoubleData(12.3);
data.add(demoMergeData);
data.add(demoMergeData);
data.add(demoMergeData);
data.add(demoMergeData);
EasyExcel.write(filePath, DemoMergeData.class).sheet().doWrite(data);
}
拦截器
测试方法
@Test
public void mergeWrite2() {
// 每隔2行会合并 把eachColumn 设置成 3 也就是我们数据的长度,所以就第一列会合并。当然其他合并策略也可以自己写
LoopMergeStrategy loopMergeStrategy = new LoopMergeStrategy(2, 0);
// 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
EasyExcel.write(filePath, DemoData.class).registerWriteHandler(loopMergeStrategy).sheet("模板").doWrite(data());
}
一张 sheet 多个表
测试方法
@Test
public void tableWrite() {
// 方法1 这里直接写多个table的案例了,如果只有一个 也可以直一行代码搞定,参照其他案
// 这里 需要指定写用哪个class去写
try (ExcelWriter excelWriter = EasyExcel.write(filePath, DemoData.class).build()) {
// 把sheet设置为不需要头 不然会输出sheet的头 这样看起来第一个table 就有2个头了
WriteSheet writeSheet = EasyExcel.writerSheet("模板").needHead(Boolean.FALSE).build();
// 这里必须指定需要头,table 会继承sheet的配置,sheet配置了不需要,table 默认也是不需要
WriteTable writeTable0 = EasyExcel.writerTable(0).needHead(Boolean.TRUE).build();
WriteTable writeTable1 = EasyExcel.writerTable(1).needHead(Boolean.TRUE).build();
// 第一次写入会创建头
excelWriter.write(data(), writeSheet, writeTable0);
// 第二次写如也会创建头,然后在第一次的后面写入数据
excelWriter.write(data(), writeSheet, writeTable1);
}
}
自定义拦截器
这些复杂操作就需要用到 apache 的 poi 了
头部超链接
拦截器
@Slf4j
public class CustomCellWriteHandler implements CellWriteHandler {
@Override
public void afterCellDispose(CellWriteHandlerContext context) {
Cell cell = context.getCell();
// 这里可以对cell进行任何操作
log.info("第{}行,第{}列写入完成。", cell.getRowIndex(), cell.getColumnIndex());
if (BooleanUtils.isTrue(context.getHead()) && cell.getColumnIndex() == 0) {
CreationHelper createHelper = context.getWriteSheetHolder().getSheet().getWorkbook().getCreationHelper();
Hyperlink hyperlink = createHelper.createHyperlink(HyperlinkType.URL);
hyperlink.setAddress("https://github.com/alibaba/easyexcel");
cell.setHyperlink(hyperlink);
}
}
}
测试方法
@Test
public void hyperHandlerTest() {
EasyExcel.write(filePath, DemoData.class).registerWriteHandler(new CustomCellWriteHandler()).sheet().doWrite(data());
}
下拉框
拦截器
@Slf4j
public class CustomSheetWriteHandler implements SheetWriteHandler {
@Override
public void afterSheetCreate(SheetWriteHandlerContext context) {
log.info("第{}个Sheet写入成功。", context.getWriteSheetHolder().getSheetNo());
// 区间设置 第一列第一行和第二行的数据。由于第一行是头,所以第一、二行的数据实际上是第二三行
CellRangeAddressList cellRangeAddressList = new CellRangeAddressList(1, 2, 0, 0);
DataValidationHelper helper = context.getWriteSheetHolder().getSheet().getDataValidationHelper();
DataValidationConstraint constraint = helper.createExplicitListConstraint(new String[] {"测试1", "测试2"});
DataValidation dataValidation = helper.createValidation(constraint, cellRangeAddressList);
context.getWriteSheetHolder().getSheet().addValidationData(dataValidation);
}
}
测试方法
@Test
public void sheetHandlerTest() {
EasyExcel.write(filePath, DemoData.class).registerWriteHandler(new CustomSheetWriteHandler()).sheet().doWrite(data());
}
批注
拦截器
@Slf4j
public class CommentWriteHandler implements RowWriteHandler {
@Override
public void afterRowDispose(RowWriteHandlerContext context) {
if (BooleanUtils.isTrue(context.getHead())) {
Sheet sheet = context.getWriteSheetHolder().getSheet();
Drawing<?> drawingPatriarch = sheet.createDrawingPatriarch();
// 在第一行 第二列创建一个批注
Comment comment =
drawingPatriarch.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, (short)1, 0, (short)2, 1));
// 输入批注信息
comment.setString(new XSSFRichTextString("创建批注!"));
// 将批注添加到单元格对象中
sheet.getRow(0).getCell(1).setCellComment(comment);
}
}
}
测试方法
@Test
public void commentHandlerTest() {
EasyExcel.write(filePath, DemoData.class).registerWriteHandler(new CommentWriteHandler()).sheet().doWrite(data());
}
筛选
拦截器
public class CellFilterHandler implements CellWriteHandler {
@Override
public void afterCellDispose(CellWriteHandlerContext context) {
Cell cell = context.getCell();
if (BooleanUtils.isTrue(context.getHead()) && cell.getColumnIndex() == 0) {
cell.getSheet().setAutoFilter(CellRangeAddress.valueOf(cell.getAddress().toString()));
}
}
}
测试方法
@Test
public void filterHandlerTest() {
EasyExcel.write(filePath, DemoData.class).registerWriteHandler(new CellFilterHandler()).sheet().doWrite(data());
}