Easy Excel
Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。
easyexcel重写了poi对07版Excel的解析,一个3M的excel用POI sax解析依然需要100M左右内存,改用easyexcel可以降低到几M,并且再大的excel也不会出现内存溢出;03版依赖POI的sax模式,在上层做了模型转换的封装,让使用者更加简单方便。
网站:
- 官方网站:https://easyexcel.opensource.alibaba.com/
- github地址:https://github.com/alibaba/easyexcel
- gitee地址:https://gitee.com/easyexcel/easyexcel
1. 写Excel
写Excel,简单来说就是将数据写出到系统外部,比如导出Excel,就属于写Execl。
在正式编写写Excel的代码之前,我们先要了解一些Easy Excel提供给我们的基础知识,比如注解、以及参数的了解。
1.1 注解
使用注解很简单,只要在对应的实体类上面加上注解即可。
1.1.1 ExcelProperty注解
用于匹配excel和实体类的匹配,参数如下:
名称 | 默认值 | 描述 |
---|---|---|
value | 空 | 用于匹配excel中的头,必须全匹配,如果有多行头,会匹配最后一行头 |
order | Integer.MAX_VALUE | 优先级高于value ,会根据order 的顺序来匹配实体和excel中数据的顺序 |
index | -1 | 优先级高于value 和order ,会根据index 直接指定到excel中具体的哪一列 |
converter | 自动选择 | 指定当前字段用什么转换器,默认会自动选择。写的情况下只要实现com.alibaba.excel.converters.Converter#convertToExcelData(com.alibaba.excel.converters.WriteConverterContext<T>) 方法即可 |
1.1.2 ExcelIgnore注解
默认所有字段都会和excel去匹配,加了这个注解会忽略该字段
1.1.3 ExcelIgnoreUnannotated
默认不管加不加ExcelProperty
的注解的所有字段都会参与读写,加了ExcelIgnoreUnannotated
注解以后,不加ExcelProperty
注解的字段就不会参与
1.1.4 DateTimeFormat注解
日期转换,用String
去接收excel日期格式的数据会调用这个注解,参数如下:
名称 | 默认值 | 描述 |
---|---|---|
value | 空 | 参照java.text.SimpleDateFormat 书写即可 |
use1904windowing | 自动选择 | excel中时间是存储1900年起的一个双精度浮点数,但是有时候默认开始日期是1904,所以设置这个值改成默认1904年开始 |
1.1.5 NumberFormat注解
数字转换,用String
去接收excel数字格式的数据会调用这个注解。
名称 | 默认值 | 描述 |
---|---|---|
value | 空 | 参照java.text.DecimalFormat 书写即可 |
roundingMode | RoundingMode.HALF_UP | 格式化的时候设置舍入模式 |
1.2 参数
1.2.1 概念介绍
WriteWorkbook
可以理解成一个excelWriteSheet
理解成一个excel里面的一个表单WriteTable
一个表单里面如果有多个实际用的表格,则可以用WriteTable
1.2.2 通用参数
WriteWorkbook
,WriteSheet
,WriteTable
都会有的参数,如果为空,默认使用上级。
名称 | 默认值 | 描述 |
---|---|---|
converter | 空 | 默认加载了很多转换器,这里可以加入不支持的字段 |
writeHandler | 空 | 写的处理器。可以实现WorkbookWriteHandler 、SheetWriteHandler 、RowWriteHandler 、CellWriteHandler ,在写入excel的不同阶段会调用 |
relativeHeadRowIndex | 0 | 写入到excel和上面空开几行 |
head | 空 | 与clazz 二选一。读取文件头对应的列表,会根据列表匹配数据,建议使用class |
clazz | 空 | 与head 二选一。读取文件的头对应的class,也可以使用注解。如果两个都不指定,则会读取全部数据 |
autoTrim | true |
会对头、读取到的数据等进行自动trim |
use1904windowing | false | excel中时间是存储1900年起的一个双精度浮点数,但是有时候默认开始日期是1904,所以设置这个值改成默认1904年开始 |
useScientificFormat | false | 数字转文本的时候在较大的数值的是否是否采用科学计数法 |
needHead | true | 是否需要写入头到excel |
useDefaultStyle | true | 是否使用默认的样式 |
automaticMergeHead | true | 自动合并头,头中相同的字段上下左右都会去尝试匹配 |
excludeColumnIndexes | 空 | 需要排除对象中的index 的数据 |
excludeColumnFieldNames | 空 | 需要排除对象中的字段的数据 |
includeColumnIndexes | 空 | 只要导出对象中的index 的数据 |
includeColumnFieldNames | 空 | 只要导出对象中的字段的数据 |
orderByIncludeColumn | false | @since 3.3.0 在使用了参数includeColumnFieldNames 或者 includeColumnIndexes 的时候,会根据传入集合的顺序排序 |
filedCacheLocation | THREAD_LOCAL | @since 3.3.0 解析class 的field 会有缓存,以前全局放到Map里面,3.3.0 以后默认放到ThreadLocal ,也就是说每次读写都会重新解析class ,可以反射修改class 的注解,并发场景不会相互影像。 THREAD_LOCAL :默认,每次读写都会缓存,但是不是同一次不会影响MEMORY :放到全局的内存里面,理论上性能会更好,但是无法通过反射、排除等方法修改导出的对象NONE :不缓存,性能会变差,涉及到读的同时要写,而且还要反射、排除等方法区修改对象的情况下可以考虑使用。 |
1.2.3 WriteWorkbook
书写位置如下:
EasyExcel.write(fileName, DemoData.class)
// 在 write 方法之后, 在 sheet方法之前都是设置WriteWorkbook的参数
.sheet("模板")
.doWrite(() -> {
// 分页查询数据
return data();
});
WriteWorkbook
独有的参数:
名称 | 默认值 | 描述 |
---|---|---|
excelType | 空 | 当前excel的类型,支持XLS、XLSX、CSV |
outputStream | 空 | 与file 二选一。写入文件的流 |
file | 空 | 与outputStream 二选一。写入的文件 |
templateInputStream | 空 | 模板的文件流 |
templateFile | 空 | 模板文件 |
charset | Charset#defaultCharset | 只有csv文件有用,写入文件的时候使用的编码 |
autoCloseStream | true |
自动关闭写入的流。 |
password | 空 | 读取文件的密码 |
inMemory | false | 是否在内存处理,默认会生成临时文件以节约内存。内存模式效率会更好,但是容易OOM |
writeExcelOnException | false | 写入过程中抛出异常了,是否尝试把数据写入到excel |
[!tip]
这里展示的参数是
WriteWorkbook
独有的参数,还有一部分参数参考1.2.2小节。
1.2.4 WriteSheet
书写位置如下:
EasyExcel.write(fileName, DemoData.class)
.sheet("模板")
// 在 sheet 方法之后, 在 doWrite方法之前都是设置WriteSheet的参数
.doWrite(() -> {
// 分页查询数据
return data();
});
WriteSheet
独有的参数:
名称 | 默认值 | 描述 |
---|---|---|
sheetNo | 0 | 需要写入的编码 |
sheetName | 空 | 需要写的Sheet名称,默认同sheetNo |
[!tip]
这里展示的参数是
WriteSheet
独有的参数,还有一部分参数参考1.2.2小节。
1.2.5 WriteTable
书写位置如下:
EasyExcel.write(fileName, DemoData.class)
.sheet("模板")
.table()
// 在 table 方法之后, 在 doWrite方法之前都是设置WriteTable的参数
.doWrite(() -> {
// 分页查询数据
return data();
});
WriteTable
独有的参数:
名称 | 默认值 | 描述 |
---|---|---|
tableNo | 0 | 需要写入的编码 |
[!tip]
这里展示的参数是
WriteTable
独有的参数,还有一部分参数参考1.2.2小节。
1.3 写Excel代码编写
在你了解了前面几个小节,这里我们给出写Excel的准备工作:
导入Easy Excel相关依赖
<dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>选择自己合适的版本</version> </dependency>
[!note]
其他的依赖:MySQL、Hutool等基本依赖这里就不讲究了。
数据库文件准备
这里我准备的数据库文件如下:
相关实体类:
/** * 用户表 * @TableName user */ @TableName(value ="user") @Data public class User implements Serializable { @TableId(value = "id", type = IdType.AUTO) private Long id; @TableField(value = "user_name") private String userName; @TableField(value = "user_account") private String userAccount; @TableField(value = "avatar_url") private String avatarUrl; @TableField(value = "gender") private Integer gender; @TableField(value = "profile") private String profile; @TableField(value = "phone") private String phone; @TableField(value = "email") private String email; @TableField(value = "user_password") private String userPassword; @TableField(value = "create_time") private Date createTime; @TableField(value = "update_time") private Date updateTime; @TableField(value = "is_delete") private Integer isDelete; @TableField(value = "user_role") private Integer userRole; @TableField(value = "planet_code") private String planetCode; @TableField(value = "tags") private String tags; @TableField(value = "user_status") private Integer userStatus; @TableField(exist = false) private static final long serialVersionUID = 1L; }
写Excel的实体类:
@Data public class UserData { @ExcelProperty("用户ID") private Long id; @ExcelProperty("用户昵称") private String userName; @ExcelProperty("创建时间") private Date createTime; /** * 忽略这个字段 */ @ExcelIgnore private String phone; }
[!note]
这里的注解的含义就不说了,前面几小节已经讲解了。
数据获取相关方法:
@RestController @RequestMapping("test") @Slf4j public class TestController { @Resource private UserService userService; @GetMapping("getUserDataList") public List<UserData> getUserDataList(){ return userService.getUserDataList(); } }
public interface UserService extends IService<User> { List<UserData> getUserDataList(); }
@Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{ @Override public List<UserData> getUserDataList() { List<User> userList = this.list(); List<User> rangeUserList = userList.stream().skip(0).limit(10).collect(Collectors.toList()); return BeanUtil.copyToList(rangeUserList,UserData.class); } }
[!note]
这里是基本数据的查询,就不多说了。
文件路径相关工具类:
import org.apache.commons.collections4.CollectionUtils; public class TestFileUtil { public static InputStream getResourcesFileInputStream(String fileName) { return Thread.currentThread().getContextClassLoader().getResourceAsStream("" + fileName); } public static String getPath() { return TestFileUtil.class.getResource("/").getPath(); } public static TestPathBuild pathBuild() { return new TestPathBuild(); } public static File createNewFile(String pathName) { File file = new File(getPath() + pathName); if (file.exists()) { file.delete(); } else { if (!file.getParentFile().exists()) { file.getParentFile().mkdirs(); } } return file; } public static File readFile(String pathName) { return new File(getPath() + pathName); } public static File readUserHomeFile(String pathName) { return new File(System.getProperty("user.home") + File.separator + pathName); } /** * build to test file path **/ public static class TestPathBuild { private TestPathBuild() { subPath = new ArrayList<>(); } private final List<String> subPath; public TestPathBuild sub(String dirOrFile) { subPath.add(dirOrFile); return this; } public String getPath() { if (CollectionUtils.isEmpty(subPath)) { return TestFileUtil.class.getResource("/").getPath(); } if (subPath.size() == 1) { return TestFileUtil.class.getResource("/").getPath() + subPath.get(0); } StringBuilder path = new StringBuilder(TestFileUtil.class.getResource("/").getPath()); path.append(subPath.get(0)); for (int i = 1; i < subPath.size(); i++) { path.append(File.separator).append(subPath.get(i)); } return path.toString(); } } }
数据和基本的方法都准备好了之后,现在我们正式开始使用Java代码来向Excel来写数据。
1.3.1 最简单的写
现在给出最简单的向Excel写数据的方法:
@Test
public void simpleWrite() {
// 注意 simpleWrite在数据量不大的情况下可以使用(5000以内,具体也要看实际情况),数据量大参照 重复多次写入
String fileName = TestFileUtil.getPath() + "simpleWrite.xlsx";
EasyExcel.write(fileName, UserData.class)// 参数1:可以是文件名(包含路径),也可以是流;参数2:写到的目标类
.sheet("最简单的写Excel")//sheet的名称
.doWrite(() -> {
// 分页查询数据
return userService.getUserDataList();
});
}
执行看看:
可见是否和要求的。
上面的代码,使用的链式调用,下面对上面的一些方法简单解读一下:
EasyExcel.write
EasyExcel.write方法执行之后,返回一个
ExcelWriterBuilder
对象。该方法有多个重载方法,对应的重载方法如下:ExcelWriterSheetBuilder的sheet方法
如果你同时指定了sheetNo和sheetName,sheetName会覆盖掉之前的sheetNo。
ExcelWriterSheetBuilder的doWrite方法
其实,只要你再往里面跟一步,就能看到真正的write方法,点击
excelWriter.write
方法,跟进去看看:
[!tip]
这里的方法只是把代码给展示出来了,并没有仔细解读,后面使用到了再一一讲解。
1.3.2 根据参数只导出指定列
示例代码:
@Test
public void excludeOrIncludeWrite() {
String fileName = TestFileUtil.getPath() + "excludeWrite.xlsx";
// 根据用户传入字段 假设我们要忽略 userName
Set<String> excludeColumnFiledNames = new HashSet<>();
excludeColumnFiledNames.add("userName");
EasyExcel.write(fileName, UserData.class)
.excludeColumnFieldNames(excludeColumnFiledNames) //排除excludeColumnFiledNames中的字段
.sheet("根据参数只导出指定列-忽略")
.doWrite(()->userService.getUserDataList());
fileName = TestFileUtil.getPath() + "includeWrite.xlsx";
// 根据用户传入字段 假设我们只要导出 userName
Set<String> includeColumnFiledNames = new HashSet<>();
includeColumnFiledNames.add("userName");
EasyExcel.write(fileName, UserData.class)
.includeColumnFieldNames(includeColumnFiledNames) //仅要includeColumnFiledNames中的字段
.sheet("根据参数只导出指定列-包含")
.doWrite(()->userService.getUserDataList());
}
执行结果:
这里使用了AbstractExcelWriterParameterBuilder
这个抽象类中的excludeColumnFieldNames
和includeColumnIndexes
方法:
1.3.3 指定写入的列
示例代码:
@Data
public class UserData {
@ExcelProperty(value = "用户ID",index = 0)
private Long id;
@ExcelProperty(value = "用户昵称",index = 1)
private String userName;
/**
* 这里设置3,跳了一个数 会导致index=2也就是第三列为空
*/
@ExcelProperty(value = "创建时间",index = 3)
private Date createTime;
/**
* 忽略这个字段
*/
@ExcelIgnore
private String phone;
}
这里我使用了ExcelProperty
注解中的index
参数,它表示写入的列的序号,从0开始。
上诉我的实体类中,设置了0、1,之后就直接到3了,也就是第4列,它会导致第3列为空。
@Test
public void indexWrite() {
String fileName = TestFileUtil.getPath() + "indexWrite.xlsx";
EasyExcel.write(fileName, UserData.class)
.sheet("写入指定的列")
.doWrite(()->userService.getUserDataList());
}
执行结果:
可见,将字段写入指定的列,是依靠ExcelProperty
注解中的index
参数来控制的。
1.3.4 复杂头写入
示例代码:
@Data
public class UserData {
@ExcelProperty(value = {"主标题","二级标题-1","用户ID"})
private Long id;
@ExcelProperty(value = {"主标题","二级标题-1","用户昵称"})
private String userName;
@ExcelProperty(value = {"主标题","二级标题-2","创建时间"})
private Date createTime;
/**
* 忽略这个字段
*/
@ExcelIgnore
private String phone;
}
执行结果:
可见,对于复杂头的写入是依靠ExcelProperty
中的value
参数来指定。
1.3.5 重复多次写入(写到单个或者多个Sheet)
这里需要新增部分代码,为方便本小节测试。
新增实体类UserOtherData
:
@Data
public class UserOtherData {
/**
* 用户邮箱
*/
@ExcelProperty(value = "邮箱")
private String email;
/**
* 密码
*/
@ExcelProperty(value = "密码")
private String userPassword;
@ExcelProperty(value = "用户标签")
private String tags;
}
新增实现方法:
@Override
public List<UserOtherData> getUserOtherDataList() {
List<User> userList = this.list();
List<User> rangeUserList = userList.stream().skip(0).limit(10).collect(Collectors.toList());
return BeanUtil.copyToList(rangeUserList,UserOtherData.class);
}
还原UserData
实体类:
@Data
public class UserData {
@ExcelProperty(value = "用户ID")
private Long id;
@ExcelProperty(value = "用户昵称")
private String userName;
@ExcelProperty(value = "创建时间")
private Date createTime;
/**
* 忽略这个字段
*/
@ExcelIgnore
private String phone;
}
示例代码:
@Test
public void repeatedWrite() {
// 方法1: 如果写到同一个sheet
String fileName = TestFileUtil.getPath() + "repeatedWrite-1.xlsx";
// 这里 需要指定写用哪个class去写
try (ExcelWriter excelWriter = EasyExcel.write(fileName, UserData.class).build()) {
// 这里注意 如果同一个sheet只要创建一次
WriteSheet writeSheet = EasyExcel.writerSheet("重复多次写入").build();
// 去调用写入,这里我调用了三次,实际使用时根据数据库分页的总的页数来
for (int i = 0; i < 3; i++) {
// 分页去数据库查询数据 这里可以去数据库查询每一页的数据
List<UserData> data = userService.getUserDataList();
excelWriter.write(data, writeSheet);
}
}
// 方法2: 如果写到不同的sheet 同一个对象
fileName = TestFileUtil.getPath() + "repeatedWrite-2.xlsx";
// 这里 指定文件
try (ExcelWriter excelWriter = EasyExcel.write(fileName, UserData.class).build()) {
// 去调用写入,这里我调用了三次,实际使用时根据数据库分页的总的页数来。这里最终会写到3个sheet里面
for (int i = 0; i < 3; i++) {
// 每次都要创建writeSheet 这里注意必须指定sheetNo 而且sheetName必须不一样
WriteSheet writeSheet = EasyExcel.writerSheet(i, "重复多次写入-" + i).build();
// 分页去数据库查询数据 这里可以去数据库查询每一页的数据
List<UserData> data = userService.getUserDataList();
excelWriter.write(data, writeSheet);
}
}
// 方法3 如果写到不同的sheet 不同的对象
fileName = TestFileUtil.getPath() + "repeatedWrite-3.xlsx";
// 这里 指定文件
try (ExcelWriter excelWriter = EasyExcel.write(fileName).build()) {
// 去调用写入,这里我调用了三次,实际使用时根据数据库分页的总的页数来。这里最终会写到3个sheet里面
for (int i = 0; i < 3; i++) {
// 每次都要创建writeSheet 这里注意必须指定sheetNo 而且sheetName必须不一样。
// 这里注意UserData.class 可以每次都变,我这里为了方便 所以用的同一个class
// 实际上可以一直变
if (i % 2 == 0){
WriteSheet writeSheet = EasyExcel.writerSheet(i, "重复多次写入-" + i)
.head(UserData.class)
.build();
// 分页去数据库查询数据 这里可以去数据库查询每一页的数据
List<UserData> data = userService.getUserDataList();
excelWriter.write(data, writeSheet);
}else {
WriteSheet writeSheet = EasyExcel.writerSheet(i, "重复多次写入-" + i)
.head(UserOtherData.class)
.build();
// 分页去数据库查询数据 这里可以去数据库查询每一页的数据
List<UserOtherData> data = userService.getUserOtherDataList();
excelWriter.write(data, writeSheet);
}
}
}
}
执行结果:
[!tip]
可能会有人看不懂本小节的代码,建议你看看1.3.1小节-最简单的写,后面的参数解读,自己去看看源码,你大概就能明白一些了。
1.3.6 日期、数字或者自定义格式转换
实体类数据:
@Data
public class UserData {
@ExcelProperty(value = "用户ID")
private Long id;
@ExcelProperty(value = "用户昵称",converter = CustomStringStringConverter.class)
private String username;
@DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
@ExcelProperty(value = "创建时间")
private Date createTime;
/**
* 忽略这个字段
*/
@ExcelIgnore
private String phone;
}
[!tip]
可见指定日期类型使用注解:@DateTimeFormat。自定义格式转换使用注解@ExcelProperty中的参数converter。
这里没有给出数字的转换,其实使用@NumberFormat注解即可。可自行学习使用。
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.converters.ReadConverterContext;
import com.alibaba.excel.converters.WriteConverterContext;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.data.WriteCellData;
/**
* @author 念心卓
* @version 1.0
* @description: TODO
* @date 2024/7/16 21:31
*/
public class CustomStringStringConverter implements Converter<String> {
/**
* 返回本类支持的Java类型键。
* 该方法用于指示本类处理的Java类型。在本例中,返回的是String.class,表示这个类支持的Java类型是String。
* 这种类型的支持通常用于类型转换、数据绑定或其他需要明确指定支持类型的情境。
*
* @return 支持的Java类型的Class对象。在这个方法中,返回的是String.class。
*/
@Override
public Class<?> supportJavaTypeKey() {
return String.class;
}
/**
* 返回本类支持的Excel单元格数据类型。
*
* @return 返回CellDataTypeEnum.STRING,表示本类处理字符串类型的Excel单元格数据。
*/
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
/**
* 读excel文件转为Java对象的时候会自动调用
*
* @param context read converter context
* @return
* @throws Exception
*/
@Override
public String convertToJavaData(ReadConverterContext<?> context) throws Exception {
return context.getReadCellData().getStringValue();
}
/**
* 写excel文件时自动调用
*
* @param context write context
* @return
* @throws Exception
*/
@Override
public WriteCellData<?> convertToExcelData(WriteConverterContext<String> context) throws Exception {
return new WriteCellData<>("自定义:" + context.getValue());
}
}
[!caution]
这里实现的
Converter
类是com.alibaba.exce
l类下的,包别导错了。
实现代码:
@Test
public void converterWrite() {
String fileName = TestFileUtil.getPath() + "converterWrite.xlsx";
// 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
EasyExcel.write(fileName, UserData.class)
.sheet("日期、数字或者自定义格式转换")
.doWrite(userService.getUserDataList());
}
执行结果:
1.3.7 图片导出
再开始讲解本小节之前,先来了解一下一个Easy Excel中新的类:WriteCellData
WriteCellData
类是 EasyExcel 库中用于写入 Excel 单元格数据的扩展类,它继承了 CellData
类,并添加了一些特定于写操作的属性和方法。该类允许用户在写入 Excel 文件时,设置单元格的数据、样式、富文本、图片、评论和超链接等。
类的结构:
@Getter
@Setter
@EqualsAndHashCode
@NoArgsConstructor
public class WriteCellData<T> extends CellData<T> {
// 用于支持日期类型的数据(仅在写入时支持)
private LocalDateTime dateValue;
// 用于支持富文本字符串(仅在写入时支持)
private RichTextStringData richTextStringDataValue;
// 用于支持图片
private List<ImageData> imageDataList;
// 用于支持评论
private CommentData commentData;
// 用于支持超链接
private HyperlinkData hyperlinkData;
// 用于设置单元格样式
private WriteCellStyle writeCellStyle;
// 原始单元格样式,如果为空,将创建一个新的。如果 writeCellStyle 和 originCellStyle 都存在,将从 writeCellStyle 复制到 originCellStyle
private CellStyle originCellStyle;
// 构造函数,根据传入的字符串值创建 WriteCellData 对象,并设置类型为 STRING
public WriteCellData(String stringValue) {
//...
}
// 构造函数,根据传入的类型创建 WriteCellData 对象
public WriteCellData(CellDataTypeEnum type) {
//...
}
// 构造函数,根据传入的类型和字符串值创建 WriteCellData 对象,并设置类型为 STRING 或 ERROR
public WriteCellData(CellDataTypeEnum type, String stringValue) {
//...
}
// 构造函数,根据传入的数字值创建 WriteCellData 对象,并设置类型为 NUMBER
public WriteCellData(BigDecimal numberValue) {
//...
}
// 构造函数,根据传入的布尔值创建 WriteCellData 对象,并设置类型为 BOOLEAN
public WriteCellData(Boolean booleanValue) {
//...
}
// 构造函数,根据传入的日期值创建 WriteCellData 对象,并设置类型为 DATE
public WriteCellData(Date dateValue) {
//...
}
// 构造函数,根据传入的 LocalDateTime 值创建 WriteCellData 对象,并设置类型为 DATE
public WriteCellData(LocalDateTime dateValue) {
//...
}
// 构造函数,根据传入的图片字节数组创建 WriteCellData 对象,并设置类型为 EMPTY
public WriteCellData(byte[] image) {
//...
}
// 获取或创建单元格样式
public WriteCellStyle getOrCreateStyle() {
//...
}
}
详细解释:
LocalDateTime dateValue
用于存储日期时间值,仅在写入 Excel 时支持。RichTextStringData richTextStringDataValue
: 用于存储富文本字符串,仅在写入 Excel 时支持。List<ImageData> imageDataList
: 用于存储图片数据。CommentData commentData
: 用于存储单元格的评论数据。HyperlinkData hyperlinkData
: 用于存储单元格的超链接数据。WriteCellStyle writeCellStyle
: 用于存储单元格的样式信息。CellStyle originCellStyle
: 用于存储原始单元格样式,如果writeCellStyle
和originCellStyle
都存在,则从writeCellStyle
复制到originCellStyle
。
构造函数:
WriteCellData(String stringValue)
: 根据字符串值创建WriteCellData
对象,并将类型设置为STRING
。WriteCellData(CellDataTypeEnum type)
: 根据指定类型创建WriteCellData
对象。WriteCellData(CellDataTypeEnum type, String stringValue)
: 根据类型和字符串值创建WriteCellData
对象,仅支持STRING
和ERROR
类型。WriteCellData(BigDecimal numberValue)
根据数字值创建WriteCellData
对象,并将类型设置为NUMBER
。WriteCellData(Boolean booleanValue)
: 根据布尔值创建WriteCellData
对象,并将类型设置为BOOLEAN
。WriteCellData(Date dateValue)
: 根据日期值创建WriteCellData
对象,并将类型设置为DATE
。WriteCellData(LocalDateTime dateValue)
: 根据LocalDateTime
值创建WriteCellData
对象,并将类型设置为DATE
。WriteCellData(byte[] image)
: 根据图片字节数组创建WriteCellData
对象,并将类型设置为EMPTY
。
方法:
getOrCreateStyle()
: 获取或创建单元格样式。如果writeCellStyle
为空,将创建一个新的WriteCellStyle
对象。
WriteCellData
类提供了丰富的功能来处理单元格的数据和样式,使用户能够更灵活地操作 Excel 文件中的单元格内容。
从WriteCellData
的源码可知,它继承于CellData
类,我们现在再来看看CellData
类。
CellData
类是 EasyExcel 库中用于表示 Excel 单元格数据的类,它继承了 AbstractCell
类,并封装了单元格的数据类型和值。该类提供了一些基础属性和方法,用于存储和操作单元格中的数据。
类的结构:
@Getter
@Setter
@EqualsAndHashCode
public class CellData<T> extends AbstractCell {
// 单元格的数据类型
private CellDataTypeEnum type;
// 数字类型的单元格值,对应 CellDataTypeEnum.NUMBER
private BigDecimal numberValue;
// 字符串或错误类型的单元格值,对应 CellDataTypeEnum.STRING 和 CellDataTypeEnum.ERROR
private String stringValue;
// 布尔类型的单元格值,对应 CellDataTypeEnum.BOOLEAN
private Boolean booleanValue;
// 转换后的结果数据
private T data;
// 单元格的公式数据
private FormulaData formulaData;
// 确保对象不为空的方法
public void checkEmpty() {
//...
}
}
详细解释:
CellDataTypeEnum type
: 表示单元格的数据类型。可能的值包括STRING
,NUMBER
,BOOLEAN
,ERROR
,EMPTY
等。BigDecimal numberValue
: 表示数字类型的单元格值,对应CellDataTypeEnum.NUMBER
。String stringValue
: 表示字符串或错误类型的单元格值,对应CellDataTypeEnum.STRING
和CellDataTypeEnum.ERROR
。Boolean booleanValue
: 表示布尔类型的单元格值,对应CellDataTypeEnum.BOOLEAN
。T data
: 表示转换后的结果数据。这个属性在一些数据转换操作中会被用到。FormulaData formulaData
: 表示单元格的公式数据。
方法:
checkEmpty()
: 该方法用于确保对象的类型和值不为空。如果类型为空,则将类型设置为CellDataTypeEnum.EMPTY
。然后根据类型检查相应的值是否为空,如果为空则将类型设置为CellDataTypeEnum.EMPTY
。
CellData
类是 EasyExcel 库中表示 Excel 单元格数据的基础类,提供了单元格的数据类型和值的封装。它允许开发者灵活地操作 Excel 单元格中的数据,包括数字、字符串、布尔值和公式等。通过 checkEmpty()
方法,还可以确保单元格的数据类型和值不为空,从而提高数据处理的健壮性。
现在再来看看本章的例子。
实体类对象:
@Data
@ContentRowHeight(100)
@ColumnWidth(100 / 8)
public class ImageDemoData {
private File file;
private InputStream inputStream;
/**
* 如果string类型 必须指定转换器,string默认转换成string
*/
@ExcelProperty(converter = StringImageConverter.class)
private String string;
private byte[] byteArray;
/**
* 根据url导出
*
* @since 2.1.1
*/
private URL url;
/**
* 根据文件导出 并设置导出的位置。
*
* @since 3.0.0-beta1
*/
private WriteCellData<Void> writeCellDataFile;
}
实现方法:
@Test
public void imageWrite() throws Exception {
String fileName = TestFileUtil.getPath() + "imageWrite.xlsx";
// 这里注意下 所有的图片都会放到内存 暂时没有很好的解法,大量图片的情况下建议 2选1:
// 1. 将图片上传到oss 或者其他存储网站: https://www.aliyun.com/product/oss ,然后直接放链接
// 2. 使用: https://github.com/coobird/thumbnailator 或者其他工具压缩图片
String imagePath = TestFileUtil.getPath() + "img.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://md.cqwulyj.cn//Java/%E7%AC%AC%E4%B8%89%E6%96%B9%E6%A1%86%E6%9E%B6/EasyExcel/image-20240718204950221.png"));
// 这里演示
// 需要额外放入文字
// 而且需要放入2个图片
// 第一个图片靠左
// 第二个靠右 而且要额外的占用他后面的单元格
WriteCellData<Void> writeCellData = new WriteCellData<>();
imageDemoData.setWriteCellDataFile(writeCellData);
// 这里可以设置为 EMPTY 则代表不需要其他数据了
writeCellData.setType(CellDataTypeEnum.STRING);
writeCellData.setStringValue("额外的放一些文字");
// 可以放入多个图片
List<ImageData> imageDataList = new ArrayList<>();
ImageData imageData = new ImageData();
imageDataList.add(imageData);
writeCellData.setImageDataList(imageDataList);
// 放入2进制图片
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(fileName, ImageDemoData.class)
.sheet()
.doWrite(list);
}
}
执行结果:
1.3.8 超链接、备注、公式、指定单个单元格的样式、单个单元格多种样式
实体类:
@Data
public class WriteCellDemoData {
/**
* 超链接
*
* @since 3.0.0-beta1
*/
private WriteCellData<String> hyperlink;
/**
* 备注
*
* @since 3.0.0-beta1
*/
private WriteCellData<String> commentData;
/**
* 公式
*
* @since 3.0.0-beta1
*/
private WriteCellData<String> formulaData;
/**
* 指定单元格的样式。当然样式 也可以用注解等方式。
*
* @since 3.0.0-beta1
*/
private WriteCellData<String> writeCellStyle;
/**
* 指定一个单元格有多个样式
*
* @since 3.0.0-beta1
*/
private WriteCellData<String> richText;
}
实现方法:
@Test
public void writeCellDataWrite() {
String fileName = TestFileUtil.getPath() + "writeCellDataWrite.xlsx";
WriteCellDemoData writeCellDemoData = new WriteCellDemoData();
// 设置超链接
WriteCellData<String> hyperlink = new WriteCellData<>("官方网站");
HyperlinkData hyperlinkData = new HyperlinkData();
hyperlinkData.setAddress("https://github.com/alibaba/easyexcel");
hyperlinkData.setHyperlinkType(HyperlinkData.HyperlinkType.URL);
hyperlink.setHyperlinkData(hyperlinkData);
writeCellDemoData.setHyperlink(hyperlink);
// 设置备注
WriteCellData<String> comment = new WriteCellData<>("备注的单元格信息");
CommentData commentData = new CommentData();
commentData.setAuthor("Jiaju Zhuang");
commentData.setRichTextStringData(new RichTextStringData("这是一个备注"));
// 备注的默认大小是按照单元格的大小 这里想调整到4个单元格那么大 所以向后 向下 各额外占用了一个单元格
commentData.setRelativeLastColumnIndex(1);
commentData.setRelativeLastRowIndex(1);
comment.setCommentData(commentData);
writeCellDemoData.setCommentData(comment);
// 设置公式
WriteCellData<String> formula = new WriteCellData<>();
FormulaData formulaData = new FormulaData();
// 将 123456789 中的第一个数字替换成 2
// 这里只是例子 如果真的涉及到公式 能内存算好尽量内存算好 公式能不用尽量不用
formulaData.setFormulaValue("REPLACE(123456789,1,1,2)");
formula.setFormulaData(formulaData);
writeCellDemoData.setFormulaData(formula);
// 设置单个单元格的样式 当然样式 很多的话 也可以用注解等方式。
WriteCellData<String> writeCellStyle = new WriteCellData<>("单元格样式");
writeCellStyle.setType(CellDataTypeEnum.STRING);
WriteCellStyle writeCellStyleData = new WriteCellStyle();
// 这里需要指定 FillPatternType 为FillPatternType.SOLID_FOREGROUND 不然无法显示背景颜色.
writeCellStyleData.setFillPatternType(FillPatternType.SOLID_FOREGROUND);
// 背景绿色
writeCellStyleData.setFillForegroundColor(IndexedColors.GREEN.getIndex());
writeCellStyle.setWriteCellStyle(writeCellStyleData);
writeCellDemoData.setWriteCellStyle(writeCellStyle);
// 设置单个单元格多种样式
// 这里需要设置 inMomery=true 不然会导致无法展示单个单元格多种样式,所以慎用
WriteCellData<String> richTest = new WriteCellData<>();
richTest.setType(CellDataTypeEnum.RICH_TEXT_STRING);
RichTextStringData richTextStringData = new RichTextStringData();
richTextStringData.setTextString("红色绿色默认");
// 前2个字红色
WriteFont writeFont = new WriteFont();
writeFont.setColor(IndexedColors.RED.getIndex());
richTextStringData.applyFont(0, 2, writeFont);
// 接下来2个字绿色
writeFont = new WriteFont();
writeFont.setColor(IndexedColors.GREEN.getIndex());
richTextStringData.applyFont(2, 4, writeFont);
richTest.setRichTextStringDataValue(richTextStringData);
writeCellDemoData.setRichText(richTest);
List<WriteCellDemoData> data = new ArrayList<>();
data.add(writeCellDemoData);
EasyExcel.write(fileName, WriteCellDemoData.class)
.inMemory(true)
.sheet("超链接、备注、公式、指定单个单元格的样式、单个单元格多种样式")
.doWrite(data);
}
执行结果:
上诉是官网的例子,当你看懂了之后自己也能写一个:
package com.example.easyexcel;
import com.alibaba.excel.metadata.data.WriteCellData;
import lombok.Data;
import java.util.Date;
/**
* @author 念心卓
* @version 1.0
* @description: TODO
* @date 2024/7/18 21:46
*/
@Data
public class UserDataWriteCell {
/**
* 我要给用户id加上备注
*/
private WriteCellData<Long> id;
/**
* 我要给用户昵称单元格设置背景
*/
private WriteCellData<String> username;
/**
* 我要给创建时间单元格设置链接
*/
private WriteCellData<Date> createTime;
}
实现方法:
@Test
public void writeCellDataWrite2(){
List<UserData> userDataList = userService.getUserDataList();
List<UserDataWriteCell> userDataWriteCellList = new ArrayList<>();
userDataList.forEach(item->{
//处理ID数据
UserDataWriteCell userDataWriteCell = new UserDataWriteCell();
WriteCellData<Long> id = new WriteCellData<>(new BigDecimal(item.getId()));
CommentData commentData = new CommentData();
commentData.setAuthor("念心卓");
commentData.setRichTextStringData(new RichTextStringData("这是用户ID哟"));
id.setCommentData(commentData);
userDataWriteCell.setId(id);
//处理username数据
WriteCellData<String> username = new WriteCellData<>(item.getUsername());
WriteCellStyle writeCellStyle = new WriteCellStyle();
writeCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND);
writeCellStyle.setFillForegroundColor(IndexedColors.ORANGE.getIndex());
WriteFont writeFont = new WriteFont();
writeCellStyle.setWriteFont(writeFont);
username.setWriteCellStyle(writeCellStyle);
userDataWriteCell.setUsername(username);
//处理creatTime数据
WriteCellData<Date> creatTime = new WriteCellData<>(item.getCreateTime());
HyperlinkData hyperlinkData = new HyperlinkData();
hyperlinkData.setAddress("https://github.com/alibaba/easyexcel");
hyperlinkData.setHyperlinkType(HyperlinkData.HyperlinkType.URL);
creatTime.setHyperlinkData(hyperlinkData);
userDataWriteCell.setCreateTime(creatTime);
userDataWriteCellList.add(userDataWriteCell);
});
String fileName = TestFileUtil.getPath() + "myWriteCellDataWrite.xlsx";
EasyExcel.write(fileName,UserDataWriteCell.class)
.sheet("我的超链接、备注、公式、指定单个单元格的样式、单个单元格多种样式")
.doWrite(userDataWriteCellList);
}
[!caution]
这里对于
WriteCellData
这个类,其实是有坑的。因为你查看它的源码,你会发现:
你可能会将每个单元格的数据存放到这个data中,你想根据自己实体类中属性的类型,来使用泛型,将数据存到这个data中,但是后续你会发现,当你执行到
EasyExcel...doWrite(数据集合)
的时候,程序会报空指针错误,其实就是你的单元格没值了,导致空指针。你可能会疑惑,为啥会没值呢?明明自己把他存放到了data中去了呀,调试也发现,确实是存进去了,那么这是怎么回事呢?
这里其实使用
CellData
的checkEmpty
方法你就会明白:这个空指针其实就是将你的单元格的类型设置为了EMPTY,所以后面程序获取 row的值的时候,就报空指针了。
你可能会疑惑,明明我的数据类型是Long的时候,我执行下面的代码:
WriteCellData<Long> writeCellData = new WriteCellData<>(); id.setData(10L);
这你应该明白为啥你的
numberValue
为null
了把,你根本就没有设置numberValue
,你看WriteCellData
这个类的源码你就会明白了:可见,只有将数据的值交给WriteCellData的构造函数,来构建,才不会被后面的置为EMPTY。
所以不要直接将值交给CellData中的data属性,应该交给WriteCellData使用构造函数来创建。
执行结果:
1.3.9 根据模板写入
[!note]
展示不做讲解
1.3.10 列宽、行高
其实列宽和行高主要是受到这几个注解的影响:
@ColumnWidth
:该注解用于设置列的宽度。其参数是一个整数,表示列宽的字符数。@HeadRowHeight
:该注解用于设置表头行的高度。其参数也是一个整数,表示行高的点数。@ContentRowHeight
:该注解用于设置内容行(即数据行)的高度。其参数是一个整数,表示行高的点数。
使用示例:
@Data
@ContentRowHeight(30)
@HeadRowHeight(20)
@ColumnWidth(25)
public class UserData {
@ExcelProperty(value = "用户ID")
private Long id;
@ExcelProperty(value = "用户昵称",converter = CustomStringStringConverter.class)
private String username;
@DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
@ExcelProperty(value = "创建时间")
private Date createTime;
/**
* 忽略这个字段
*/
@ExcelIgnore
private String phone;
}
@Test
public void widthAndHeightWrite() {
String fileName = TestFileUtil.getPath() + "widthAndHeightWrite.xlsx";
EasyExcel.write(fileName, UserData.class)
.sheet("列宽、行高")
.doWrite(userService.getUserDataList());
}
执行结果:
1.3.11 注解形式自定义样式
@ContentFontStyle
@ContentFontStyle
注解用于设置单元格内容的字体样式。属性:
fontName
: 字体名称。fontHeightInPoints
: 字体大小。bold
: 是否加粗。italic
: 是否斜体。strikeout
: 是否有删除线。typeOffset
: 字体偏移(上标、下标)。underline
: 是否有下划线。color
: 字体颜色。charset
:设置字符编码
@ContentStyle
@ContentStyle
注解用于设置单元格内容的样式。属性:
dataFormat
: 数据格式。hidden
: 是否隐藏单元格。locked
: 是否锁定单元格。quotePrefix
: 是否使用引号前缀。horizontalAlignment
: 水平对齐方式。verticalAlignment
: 垂直对齐方式。rotation
: 旋转角度。wrapText
: 是否自动换行。shrinkToFit
: 是否缩小字体填充。fillPatternType
: 填充模式。fillForegroundColor
: 前景色。fillBackgroundColor
: 背景色。borderLeft
,borderRight
,borderTop
,borderBottom
: 边框类型。leftBorderColor
,rightBorderColor
,topBorderColor
,bottomBorderColor
: 边框颜色。
@HeadFontStyle
@HeadFontStyle
注解用于设置表头的字体样式。属性:
fontName
: 字体名称。fontHeightInPoints
: 字体大小。bold
: 是否加粗。italic
: 是否斜体。strikeout
: 是否有删除线。typeOffset
: 字体偏移(上标、下标)。underline
: 是否有下划线。color
: 字体颜色。
**@HeadStyle **
@HeadStyle
注解用于设置表头的样式。属性:
dataFormat
: 数据格式。hidden
: 是否隐藏单元格。locked
: 是否锁定单元格。quotePrefix
: 是否使用引号前缀。horizontalAlignment
: 水平对齐方式。verticalAlignment
: 垂直对齐方式。rotation
: 旋转角度。wrapText
: 是否自动换行。shrinkToFit
: 是否缩小字体填充。fillPatternType
: 填充模式。fillForegroundColor
: 前景色。fillBackgroundColor
: 背景色。borderLeft
,borderRight
,borderTop
,borderBottom
: 边框类型。leftBorderColor
,rightBorderColor
,topBorderColor
,bottomBorderColor
: 边框颜色。
示例:
@Data // 头背景设置成红色 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) // 字符串的头字体设置成20 @HeadFontStyle(fontHeightInPoints = 30) // 字符串的内容的背景设置成天蓝 IndexedColors.SKY_BLUE.getIndex() @ContentStyle(fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND, fillForegroundColor = 40) // 字符串的内容字体设置成20 @ContentFontStyle(fontHeightInPoints = 30) @ExcelProperty(value = "用户ID") private Long id; @ExcelProperty(value = "用户昵称",converter = CustomStringStringConverter.class) private String username; @DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒") @ExcelProperty(value = "创建时间") private Date createTime; }
@Test public void annotationStyleWrite() { String fileName = TestFileUtil.getPath() + "annotationStyleWrite.xlsx"; EasyExcel.write(fileName, DemoStyleData.class) .sheet("注解形式自定义样式") .doWrite(userService.getUserDataList()); }
执行结果:
1.3.12 自定义样式
@Test
public void handlerStyleWrite() {
// 方法1 使用已有的策略 推荐
// HorizontalCellStyleStrategy 每一行的样式都一样 或者隔行一样
// AbstractVerticalCellStyleStrategy 每一列的样式都一样 需要自己回调每一页
String fileName = TestFileUtil.getPath() + "handlerStyleWrite.xlsx";
// 头的策略
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(fileName, UserData.class)
.registerWriteHandler(horizontalCellStyleStrategy)
.sheet("自定义样式-推荐")
.doWrite(userService.getUserDataList());
// 方法2: 使用easyexcel的方式完全自己写 不太推荐 尽量使用已有策略
// @since 3.0.0-beta2
fileName = TestFileUtil.getPath() + "handlerStyleWrite-2.xlsx";
EasyExcel.write(fileName, UserData.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(userService.getUserDataList());
// 方法3: 使用poi的样式完全自己写 不推荐
// @since 3.0.0-beta2
// 坑1:style里面有dataformat 用来格式化数据的 所以自己设置可能导致格式化注解不生效
// 坑2:不要一直去创建style 记得缓存起来 最多创建6W个就挂了
fileName = TestFileUtil.getPath() + "handlerStyleWrite-3.xlsx";
EasyExcel.write(fileName, UserData.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(userService.getUserDataList());
}
1.3.13 合并单元格
在 EasyExcel 中,@OnceAbsoluteMerge
和 @ContentLoopMerge
注解用于实现单元格的合并操作。
@OnceAbsoluteMerge:
@OnceAbsoluteMerge
注解用于一次性合并绝对位置的单元格。它通常用于合并表格中的特定区域,而不依赖于数据内容。属性:
firstRowIndex
: 合并区域的起始行索引(从0开始)。lastRowIndex
: 合并区域的结束行索引(从0开始)。firstColumnIndex
: 合并区域的起始列索引(从0开始)。lastColumnIndex
: 合并区域的结束列索引(从0开始)。
@ContentLoopMerge:
@ContentLoopMerge
注解用于根据内容循环合并单元格。它通常用于合并具有相同内容的连续单元格。属性:
eachRow
: 每隔多少行进行一次合并。columnExtend
: 向右扩展合并的列数,其实就是多少列合并为1列。
示例:
@Data
// 将第6-7行的2-3列合并成一个单元格
// @OnceAbsoluteMerge(firstRowIndex = 5, lastRowIndex = 6, firstColumnIndex = 1, lastColumnIndex = 2)
public class DemoMergeData {
@ExcelProperty(value = "用户ID")
private Long id;
//这一列 每隔2行 合并单元格
@ContentLoopMerge(eachRow = 2)
@ExcelProperty(value = "用户昵称")
private String username;
@DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
@ExcelProperty(value = "创建时间")
private Date createTime;
}
合并方法一:使用注解合并
@Test
public void mergeWrite() {
String fileName = TestFileUtil.getPath() + "mergeWrite-1.xlsx";
// 在DemoStyleData里面加上ContentLoopMerge注解
EasyExcel.write(fileName, DemoMergeData.class)
.sheet("合并单元格")
.doWrite(userService.getUserDataList());
}
执行结果:
合并方法二:自定义合并策略合并
@Test
public void mergeWrite() {
// 方法2 自定义合并单元格策略
fileName = TestFileUtil.getPath() + "mergeWrite-2.xlsx";
LoopMergeStrategy loopMergeStrategy = new LoopMergeStrategy(2, 1);
EasyExcel.write(fileName, UserData.class)
.registerWriteHandler(loopMergeStrategy)
.sheet("合并单元格")
.doWrite(userService.getUserDataList());
}
其中LoopMergeStrategy
就是合并策略,这是EasyExcel
自带的合并策略。其中LoopMergeStrategy
的构造函数有解读:
[!tip]
这里我选择最多的一个构造参数。
public LoopMergeStrategy(int eachRow, int columnExtend, int columnIndex)
- eachRow:每隔多少行进行一次合并。这个参数决定了在每个合并区域中包含多少行。例如,如果
eachRow
设置为2,那么每两行会合并成一个单元格。 - columnExtend:从指定列开始,向右扩展的列数。这意味着当前列以及向右的
columnExtend
列都会被一起合并。例如,如果columnExtend
设置为1,那么当前列和它右边的一列都会被合并。 - columnIndex:需要应用合并策略的列索引(从0开始)。这个参数指定了从哪一列应用合并策略。例如,如果
columnIndex
设置为2,那么从第3列开始应用合并策略。
执行结果:
1.3.14 使用table去写入
示例:
@Test
public void tableWrite() {
String fileName = TestFileUtil.getPath() + "tableWrite.xlsx";
// 方法1 这里直接写多个table的案例了,如果只有一个 也可以直一行代码搞定,参照其他案
// 这里 需要指定写用哪个class去写
try (ExcelWriter excelWriter = EasyExcel.write(fileName, UserData.class).build()) {
// 把sheet设置为不需要头 不然会输出sheet的头 这样看起来第一个table 就有2个头了
WriteSheet writeSheet = EasyExcel
.writerSheet("使用table去写入")
.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(userService.getUserDataList(), writeSheet, writeTable0);
// 第二次写如也会创建头,然后在第一次的后面写入数据
excelWriter.write(userService.getUserDataList(), writeSheet, writeTable1);
}
}
执行结果:
[!tip]
其实这里的使用table写入,其实就是一个sheet中,存在多个table这种场景。
1.3.15 动态头,实时生成头写入
示例:
@Test
public void dynamicHeadWrite() {
String fileName = TestFileUtil.getPath() + "dynamicHeadWrite.xlsx";
EasyExcel.write(fileName)
// 这里放入动态头
.head(head()).sheet("动态头,实时生成头写入")
// 当然这里数据也可以用 List<List<String>> 去传入
.doWrite(userService.getUserDataList());
}
private List<List<String>> head() {
List<List<String>> list = new ArrayList<>();
List<String> head0 = new ArrayList<>();
head0.add("数字" + System.currentTimeMillis());
List<String> head1 = new ArrayList<>();
head1.add("字符串" + System.currentTimeMillis());
List<String> head2 = new ArrayList<>();
head2.add("日期" + System.currentTimeMillis());
list.add(head0);
list.add(head1);
list.add(head2);
return list;
}
执行结果:
1.3.16 自动列宽(不太精确)
为了演示方便,我们这里我们给某一列的表头弄宽一点。
实体类:
@Data
public class UserData {
@ExcelProperty(value = "用户ID")
private Long id;
@ExcelProperty(value = "用户昵称特别特别特别特别特别特别长长长")
private String username;
@DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
@ExcelProperty(value = "创建时间")
private Date createTime;
/**
* 忽略这个字段
*/
@ExcelIgnore
private String phone;
}
示例方法:
/**
* 自动列宽(不太精确)
* <p>
* 这个目前不是很好用,比如有数字就会导致换行。而且长度也不是刚好和实际长度一致。 所以需要精确到刚好列宽的慎用。 当然也可以自己参照
* {@link LongestMatchColumnWidthStyleStrategy}重新实现.
* <p>
* poi 自带{@link SXSSFSheet#autoSizeColumn(int)} 对中文支持也不太好。目前没找到很好的算法。 有的话可以推荐下。
*
* <p>
* 1. 创建excel对应的实体对象 参照{@link LongestMatchColumnWidthData}
* <p>
* 2. 注册策略{@link LongestMatchColumnWidthStyleStrategy}
* <p>
* 3. 直接写即可
*/
@Test
public void longestMatchColumnWidthWrite() {
String fileName =
TestFileUtil.getPath() + "longestMatchColumnWidthWrite.xlsx";
EasyExcel.write(fileName, UserData.class)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
.sheet("自动列宽(不太精确)")
.doWrite(dataLong());
}
private List<UserData> dataLong() {
List<UserData> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
UserData data = new UserData();
data.setUsername("测试很长的字符串测试很长的字符串测试很长的字符串" + i);
data.setCreateTime(new Date());
data.setId((long) i);
list.add(data);
}
return list;
}
执行结果:
1.3.17 自定义拦截器(上面几点都不符合但是要对单元格进行操作的参照这个)
单元格拦截器接口:CellWriteHandler
package com.alibaba.excel.write.handler;
import java.util.List;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.write.handler.context.CellWriteHandlerContext;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteTableHolder;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
public interface CellWriteHandler extends WriteHandler {
default void beforeCellCreate(CellWriteHandlerContext context) {
beforeCellCreate(context.getWriteSheetHolder(), context.getWriteTableHolder(), context.getRow(),
context.getHeadData(), context.getColumnIndex(), context.getRelativeRowIndex(), context.getHead());
}
default void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row,
Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead) {}
default void afterCellCreate(CellWriteHandlerContext context) {
afterCellCreate(context.getWriteSheetHolder(), context.getWriteTableHolder(), context.getCell(),
context.getHeadData(), context.getRelativeRowIndex(), context.getHead());
}
default void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell,
Head head, Integer relativeRowIndex, Boolean isHead) {}
default void afterCellDataConverted(CellWriteHandlerContext context) {
WriteCellData<?> writeCellData = CollectionUtils.isNotEmpty(context.getCellDataList()) ? context
.getCellDataList().get(0) : null;
afterCellDataConverted(context.getWriteSheetHolder(), context.getWriteTableHolder(), writeCellData,
context.getCell(), context.getHeadData(), context.getRelativeRowIndex(), context.getHead());
}
default void afterCellDataConverted(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder,
WriteCellData<?> cellData, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {}
default void afterCellDispose(CellWriteHandlerContext context) {
afterCellDispose(context.getWriteSheetHolder(), context.getWriteTableHolder(), context.getCellDataList(),
context.getCell(), context.getHeadData(), context.getRelativeRowIndex(), context.getHead());
}
default void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder,
List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {}
}
方法解释:
Sheet拦截器接口:SheetWriteHandler
public interface SheetWriteHandler extends WriteHandler {
default void beforeSheetCreate(SheetWriteHandlerContext context) {
beforeSheetCreate(context.getWriteWorkbookHolder(), context.getWriteSheetHolder());
}
default void beforeSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {}
default void afterSheetCreate(SheetWriteHandlerContext context) {
afterSheetCreate(context.getWriteWorkbookHolder(), context.getWriteSheetHolder());
}
default void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {}
}
方法解释:
- beforeSheetCreate:在创建工作表之前调用
- afterSheetCreate:在工作表创建之后调用
**定义拦截器 **:
package com.example.easyexcel;
import com.alibaba.excel.write.handler.SheetWriteHandler;
import com.alibaba.excel.write.handler.context.SheetWriteHandlerContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.DataValidation;
import org.apache.poi.ss.usermodel.DataValidationConstraint;
import org.apache.poi.ss.usermodel.DataValidationHelper;
import org.apache.poi.ss.util.CellRangeAddressList;
@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);
}
}
package com.example.easyexcel;
import com.alibaba.excel.util.BooleanUtils;
import com.alibaba.excel.write.handler.CellWriteHandler;
import com.alibaba.excel.write.handler.context.CellWriteHandlerContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.common.usermodel.HyperlinkType;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CreationHelper;
import org.apache.poi.ss.usermodel.Hyperlink;
/**
* 自定义拦截器。对第一行第一列的头超链接到:https://github.com/alibaba/easyexcel
*
* @author Jiaju Zhuang
*/
@Slf4j
public class CustomCellWriteHandler implements CellWriteHandler {
/**
* 在所有单元格数据操作完成之后调用
* @param context
*/
@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);
}
}
}
实现方法:
/**
* 下拉,超链接等自定义拦截器(上面几点都不符合但是要对单元格进行操作的参照这个)
* <p>
* demo这里实现2点。1. 对第一行第一列的头超链接到:https://github.com/alibaba/easyexcel
*2. 对第一列第一行和第二行的数据新增下拉框,显示 测试1 测试2
* <p>
* 1. 创建excel对应的实体对象 参照{@link UserData}
* <p>
* 2. 注册拦截器 {@link CustomCellWriteHandler} {@link CustomSheetWriteHandler}
* <p>
* 2. 直接写即可
*/
@Test
public void customHandlerWrite() {
String fileName = TestFileUtil.getPath() + "customHandlerWrite.xlsx";
EasyExcel.write(fileName, UserData.class)
.registerWriteHandler(new CustomSheetWriteHandler())
.registerWriteHandler(new CustomCellWriteHandler())
.sheet("定义拦截器")
.doWrite(userService.getUserDataList());
}
执行结果:
1.3.18 插入批注(拦截器实现)
行拦截器:RowWriteHandler
package com.alibaba.excel.write.handler;
import com.alibaba.excel.write.handler.context.RowWriteHandlerContext;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteTableHolder;
import org.apache.poi.ss.usermodel.Row;
/**
* intercepts handle row creation
*
* @author Jiaju Zhuang
*/
public interface RowWriteHandler extends WriteHandler {
//在创建行之前调用
default void beforeRowCreate(RowWriteHandlerContext context) {
beforeRowCreate(context.getWriteSheetHolder(), context.getWriteTableHolder(), context.getRowIndex(),
context.getRelativeRowIndex(), context.getHead());
}
//在创建行之前调用
default void beforeRowCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Integer rowIndex,
Integer relativeRowIndex, Boolean isHead) {}
//在行创建之后调用
default void afterRowCreate(RowWriteHandlerContext context) {
afterRowCreate(context.getWriteSheetHolder(), context.getWriteTableHolder(), context.getRow(),
context.getRelativeRowIndex(), context.getHead());
}
//在行创建之后调用
default void afterRowCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row,
Integer relativeRowIndex, Boolean isHead) {}
//在行的所有操作完成之后调用。在填充数据的情况下,可能会被多次调用。
default void afterRowDispose(RowWriteHandlerContext context) {
afterRowDispose(context.getWriteSheetHolder(), context.getWriteTableHolder(), context.getRow(),
context.getRelativeRowIndex(), context.getHead());
}
//在行的所有操作完成之后调用。在填充数据的情况下,可能会被多次调用。
default void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row,
Integer relativeRowIndex, Boolean isHead) {}
}
示例:
package com.example.easyexcel;
import com.alibaba.excel.util.BooleanUtils;
import com.alibaba.excel.write.handler.RowWriteHandler;
import com.alibaba.excel.write.handler.context.RowWriteHandlerContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Comment;
import org.apache.poi.ss.usermodel.Drawing;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.usermodel.XSSFClientAnchor;
import org.apache.poi.xssf.usermodel.XSSFRichTextString;
/**
* 自定义拦截器.新增注释,第一行头加批注
*
* @author Jiaju Zhuang
*/
@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 commentWrite() {
String fileName = TestFileUtil.getPath() + "commentWrite.xlsx";
// 这里要注意inMemory 要设置为true,才能支持批注。目前没有好的办法解决 不在内存处理批注。这个需要自己选择。
EasyExcel.write(fileName, UserData.class)
.inMemory(Boolean.TRUE)
.registerWriteHandler(new CommentWriteHandler())
.sheet("插入批注")
.doWrite(userService.getUserDataList());
}
执行结果:
1.3.19 可变标题处理(包括标题国际化等)
示例方法:
/**
* 可变标题处理(包括标题国际化等)
* <p>
* 简单的说用List<List<String>>的标题 但是还支持注解
* <p>
* 1. 创建excel对应的实体对象 参照{@link UserData}
* <p>
* 2. 直接写即可
*/
@Test
public void variableTitleWrite() {
// 写法1
String fileName = TestFileUtil.getPath() + "variableTitleWrite.xlsx";
EasyExcel.write(fileName, UserData.class)
.head(variableTitleHead())
.sheet("可变标题处理(包括标题国际化等)")
.doWrite(userService.getUserDataList());
}
private List<List<String>> variableTitleHead() {
List<List<String>> list = ListUtils.newArrayList();
List<String> head0 = ListUtils.newArrayList();
head0.add("string" + System.currentTimeMillis());
List<String> head1 = ListUtils.newArrayList();
head1.add("number" + System.currentTimeMillis());
List<String> head2 = ListUtils.newArrayList();
head2.add("date" + System.currentTimeMillis());
list.add(head0);
list.add(head1);
list.add(head2);
return list;
}
执行结果:
1.3.20 不创建对象的写
示例方法:
@Test
public void noModelWrite() {
String fileName = TestFileUtil.getPath() + "noModelWrite.xlsx";
EasyExcel.write(fileName)
.head(head())
.sheet("不创建对象的写")
.doWrite(dataList());
}
private List<List<String>> head() {
List<List<String>> list = new ArrayList<>();
List<String> head0 = new ArrayList<>();
head0.add("数字" + System.currentTimeMillis());
List<String> head1 = new ArrayList<>();
head1.add("字符串" + System.currentTimeMillis());
List<String> head2 = new ArrayList<>();
head2.add("日期" + System.currentTimeMillis());
list.add(head0);
list.add(head1);
list.add(head2);
return list;
}
private List<List<Object>> dataList() {
List<List<Object>> list = ListUtils.newArrayList();
for (int i = 0; i < 10; i++) {
List<Object> data = ListUtils.newArrayList();
data.add("字符串" + i);
data.add(0.56);
data.add(new Date());
list.add(data);
}
return list;
}
执行结果:
1.3.21 web中的写
示例代码:
/**
* 文件下载(失败了会返回一个有部分数据的Excel)
* <p>
* 1. 创建excel对应的实体对象 参照{@link UserData}
* <p>
* 2. 设置返回的 参数
* <p>
* 3. 直接写,这里注意,finish的时候会自动关闭OutputStream,当然你外面再关闭流问题不大
*/
@GetMapping("download")
public void download(HttpServletResponse response) throws IOException {
// 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
String fileName = URLEncoder.encode("测试", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
EasyExcel.write(response.getOutputStream(), UserData.class)
.sheet("web中的写")
.doWrite(userService.getUserDataList());
//关闭流
response.getOutputStream().close();
}
执行结果:
1.3.22 web中的写并且失败的时候返回json
示例代码:
/**
* 文件下载并且失败的时候返回json(默认失败了会返回一个有部分数据的Excel)
*
* @since 2.1.1
*/
@GetMapping("downloadFailedUsingJson")
public void downloadFailedUsingJson(HttpServletResponse response) throws IOException {
// 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman
try {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
String fileName = URLEncoder.encode("测试", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
// 这里需要设置不关闭流
EasyExcel.write(response.getOutputStream(), UserData.class)
.autoCloseStream(Boolean.FALSE)
.sheet("web中的写并且失败的时候返回json")
.doWrite(userService.getUserDataList());
} catch (Exception e) {
// 重置response
response.reset();
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
Map<String, String> map = MapUtils.newHashMap();
map.put("status", "failure");
map.put("message", "下载文件失败" + e.getMessage());
response.getWriter().println(JSON.toJSONString(map));
}
}
[!tip]
至此,写Excel相关代码示范已演示完毕,下一节将展示读Excel的编写。
2. 读Excel
2.1 注解
[!tip]
本小节的注解参考小节1.1即可,二者并无区别。
2.2 参数
2.2.1 概念介绍
ReadWorkbook
可以理解成一个excelReadSheet
理解成一个excel里面的一个表单
2.2.2 通用参数
ReadWorkbook
,ReadSheet
都会有的参数,如果为空,默认使用上级。
名称 | 默认值 | 描述 |
---|---|---|
converter | 空 | 默认加载了很多转换器,这里可以加入不支持的字段 |
readListener | 空 | 可以注册多个监听器,读取excel的时候会不断的回调监听器中的方法 |
headRowNumber | 1 | excel中头的行数,默认1行 |
head | 空 | 与clazz 二选一。读取文件头对应的列表,会根据列表匹配数据,建议使用class |
clazz | 空 | 与head 二选一。读取文件的头对应的class,也可以使用注解。如果两个都不指定,则会读取全部数据 |
autoTrim | true |
会对头、读取数据等进行自动trim |
use1904windowing | false | excel中时间是存储1900年起的一个双精度浮点数,但是有时候默认开始日期是1904,所以设置这个值改成默认1904年开始 |
useScientificFormat | false | 数字转文本的时候在较大的数值的是否是否采用科学计数法 |
2.2.3 ReadWorkbook
书写的位置如下:
EasyExcel.read(fileName, DemoData.class, new DemoDataListener())
// 在 read 方法之后, 在 sheet方法之前都是设置ReadWorkbook的参数
.sheet()
.doRead();
ReadWorkbook
独有的参数:
名称 | 默认值 | 描述 |
---|---|---|
excelType | 空 | 当前excel的类型,支持XLS、XLSX、CSV |
inputStream | 空 | 与file 二选一。读取文件的流,如果接收到的是流就只用流,不用流则建议使用file 参数。因为使用了inputStream ,easyexcel会帮忙创建临时文件,最终还是file |
file | 空 | 与inputStream 二选一。读取文件的文件。 |
mandatoryUseInputStream | false |
强制使用 inputStream 来创建对象,性能会变差,但是不会创建临文件。 |
charset | Charset#defaultCharset | 只有csv文件有用,读取文件的时候使用的编码 |
autoCloseStream | true |
自动关闭读取的流。 |
readCache | 空 | 默认小于5M用内存,超过5M会使用 EhCache ,这里不建议使用这个参数。 |
readCacheSelector | SimpleReadCacheSelector | 用于选择什么时候用内存去存储临时数据,什么时候用磁盘存储临时数据。 |
ignoreEmptyRow | true | 忽略空的行 |
password | 空 | 读取文件的密码 |
xlsxSAXParserFactoryName | 空 | 指定sax读取使用的class的名称,例如:com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl |
useDefaultListener | true | @since 2.1.4 默认会加入ModelBuildEventListener 来帮忙转换成传入class 的对象,设置成false 后将不会协助转换对象,自定义的监听器会接收到Map<Integer,CellData> 对象,如果还想继续接听到class 对象,请调用readListener 方法,加入自定义的beforeListener 、 ModelBuildEventListener 、 自定义的afterListener 即可。 |
extraReadSet | 空 | 额外需要读取内容的set,默认不读取这些数据 |
readDefaultReturn | STRING | @since 3.2.0 STRING:会返回一个MapBigDecimal 、Boolean 、String 、LocalDateTime 、null,中的一个, READ_CELL_DATA: 会返回一个Map<Integer,ReadCellData<?>>的数组,其中? 类型参照ACTUAL_DATA的 |
[!tip]
这里展示的参数是
ReadWorkbook
独有的参数,还有一部分参数参考2.2.2小节。
2.2.4 ReadSheet
书写位置如下:
EasyExcel.read(fileName, DemoData.class, new DemoDataListener())
.sheet()
// 在 sheet 方法之后, 在 doRead方法之前都是设置ReadSheet的参数
.doRead();
ReadSheet
独有的参数:
名称 | 默认值 | 描述 |
---|---|---|
sheetNo | 0 | 需要读取Sheet的编码,建议使用这个来指定读取哪个Sheet |
sheetName | 空 | 根据名字去匹配Sheet |
[!tip]
这里展示的参数是
ReadSheet
独有的参数,还有一部分参数参考2.2.2小节。
2.3 读Excel编写
2.3.1 最简单的读
Excel示例:参考之前1.3.1小节生成的文件。
持久层:
package com.example.easyexcel;
import java.util.List;
/**
* 假设这个是你的DAO存储。当然还要这个类让spring管理,当然你不用需要存储,也不需要这个类。
**/
public class DemoDAO {
public void save(List<UserData> list) {
// 如果是mybatis,尽量别直接调用多次insert,自己写一个mapper里面新增一个方法batchInsert,所有数据一次性插入
}
}
package com.example.easyexcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.util.ListUtils;
import com.alibaba.fastjson2.JSON;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
// 有个很重要的点 UserDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
@Slf4j
public class UserDataListener implements ReadListener<UserData> {
/**
* 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收
*/
private static final int BATCH_COUNT = 100;
/**
* 缓存的数据
*/
private List<UserData> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
/**
* 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。
*/
private DemoDAO demoDAO;
public UserDataListener() {
// 这里是demo,所以随便new一个。实际使用如果到了spring,请使用下面的有参构造函数
demoDAO = new DemoDAO();
}
/**
* 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来
*
* @param demoDAO
*/
public UserDataListener(DemoDAO demoDAO) {
this.demoDAO = demoDAO;
}
/**
* 这个每一条数据解析都会来调用
*
* @param data one row value. Is is same as {@link AnalysisContext#readRowHolder()}
* @param context
*/
@Override
public void invoke(UserData data, AnalysisContext context) {
log.info("解析到一条数据:{}", JSON.toJSONString(data));
cachedDataList.add(data);
// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
if (cachedDataList.size() >= BATCH_COUNT) {
saveData();
// 存储完成清理 list
cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
}
}
/**
* 所有数据解析完成了 都会来调用
*
* @param context
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 这里也要保存数据,确保最后遗留的数据也存储到数据库
saveData();
log.info("所有数据解析完成!");
}
/**
* 加上存储数据库
*/
private void saveData() {
log.info("{}条数据,开始存储数据库!", cachedDataList.size());
demoDAO.save(cachedDataList);
log.info("存储数据库成功!");
}
}
[!important]
有个很重要的点 UserDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去。
示例方法:
/**
* 最简单的读
* <p>
* 1. 创建excel对应的实体对象 参照{@link UserData}
* <p>
* 2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照{@link UserDataListener}
* <p>
* 3. 直接读即可
*/
@Test
public void simpleRead() {
// 写法1:JDK8+ ,不用额外写一个DemoDataListener
// since: 3.0.0-beta1
String fileName = TestFileUtil.getPath() + "simpleWrite.xlsx";
// 这里默认每次会读取100条数据 然后返回过来 直接调用使用数据就行
// 具体需要返回多少行可以在`PageReadListener`的构造函数设置
EasyExcel.read(fileName, UserData.class, new PageReadListener<UserData>(dataList -> {
for (UserData userData : dataList) {
log.info("读取到一条数据{}", JSON.toJSONString(userData));
}
})).sheet().doRead();
// 写法2:
// 匿名内部类 不用额外写一个DemoDataListener
fileName = TestFileUtil.getPath() + "simpleWrite.xlsx";
// 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
EasyExcel.read(fileName, UserData.class, new ReadListener<UserData>() {
/**
* 单次缓存的数据量
*/
public static final int BATCH_COUNT = 100;
/**
*临时存储
*/
private List<UserData> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
@Override
public void invoke(UserData data, AnalysisContext context) {
cachedDataList.add(data);
if (cachedDataList.size() >= BATCH_COUNT) {
saveData();
// 存储完成清理 list
cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
saveData();
}
/**
* 加上存储数据库
*/
private void saveData() {
log.info("{}条数据,开始存储数据库!", cachedDataList.size());
log.info("存储数据库成功!");
}
}).sheet().doRead();
// 有个很重要的点 UserDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
// 写法3:
fileName = TestFileUtil.getPath() + "simpleWrite.xlsx";
EasyExcel.read(fileName, UserData.class, new UserDataListener()).sheet().doRead();
// 写法4
fileName = TestFileUtil.getPath() + "simpleWrite.xlsx";
// 一个文件一个reader
try (ExcelReader excelReader = EasyExcel.read(fileName, UserData.class, new UserDataListener()).build()) {
// 构建一个sheet 这里可以指定名字或者no
ReadSheet readSheet = EasyExcel.readSheet(0).build();
// 读取一个sheet
excelReader.read(readSheet);
}
}
2.3.2 指定列的下标或者列名
Excel示例:参考之前1.3.1小节生成的文件。
对象:
@Data
public class UserData {
/**
* 用名字去匹配,这里需要注意,如果名字重复,会导致只有一个字段读取到数据
*/
@ExcelProperty(value = "用户ID")
private Long id;
/**
* 强制读取第二列 这里不建议 index 和 name 同时用,要么一个对象只用index,要么一个对象只用name去匹配
*/
@ExcelProperty(index = 1)
private String username;
@DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
@ExcelProperty(value = "创建时间")
private Date createTime;
}
监听器:参考最简单的读监听器
示例方法:
/**
* 指定列的下标或者列名
*
* <p>1. 创建excel对应的实体对象,并使用{@link com.alibaba.excel.annotation.ExcelProperty}注解. 参照{@link UserData}
* <p>2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照{@link UserData}
* <p>3. 直接读即可
*/
@Test
public void indexOrNameRead() {
String fileName = TestFileUtil.getPath() + "simpleWrite.xlsx";
// 这里默认读取第一个sheet
EasyExcel.read(fileName, UserData.class, new UserDataListener()).sheet().doRead();
}
2.3.3 读多个sheet
Excel示例:参考之前1.3.5小节生成的文件。
监听器:参考最简单的读监听器。
示例方法:
/**
* 读多个或者全部sheet,这里注意一个sheet不能读取多次,多次读取需要重新读取文件
* <p>
* 1. 创建excel对应的实体对象 参照{@link UserData}
* <p>
* 2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照{@link UserDataListener}
* <p>
* 3. 直接读即可
*/
@Test
public void repeatedRead() {
String fileName = TestFileUtil.getPath() + "repeatedWrite-3.xlsx";
// 读取全部sheet
// 这里需要注意 UserDataListener的doAfterAllAnalysed 会在每个sheet读取完毕后调用一次。然后所有sheet都会往同一个UserDataListener里面写
EasyExcel.read(fileName, UserData.class, new UserDataListener()).doReadAll();
System.out.println("===============读取全部sheet读取完成,下面开始读取部分sheet===============");
// 读取部分sheet
fileName = TestFileUtil.getPath() + "repeatedWrite-3.xlsx";
// 写法1
try (ExcelReader excelReader = EasyExcel.read(fileName).build()) {
// 这里为了简单 所以注册了 同样的head 和Listener 自己使用功能不同必须不同的Listener
ReadSheet readSheet1 = EasyExcel.readSheet(0)
.head(UserData.class)
.registerReadListener(new UserDataListener())
.build();
ReadSheet readSheet2 = EasyExcel.readSheet(1)
.head(UserData.class)
.registerReadListener(new UserDataListener())
.build();
// 这里注意 一定要把sheet1 sheet2 一起传进去,不然有个问题就是03版的excel 会读取多次,浪费性能
excelReader.read(readSheet1, readSheet2);
}
}
[!caution]
- 这里需要注意
UserDataListener
的doAfterAllAnalysed
会在每个sheet读取完毕后调用一次。然后所有sheet都会往同一个UserDataListener
里面写。- 读取全部sheet最后使用
doReadAll
这个API。
2.3.4 日期、数字或者自定义格式转换
Excel示例:参考之前1.3.1小节生成的文件。
自定义转换器:
public class CustomStringStringConverter implements Converter<String> {
/**
* 返回本类支持的Java类型键。
* 该方法用于指示本类处理的Java类型。在本例中,返回的是String.class,表示这个类支持的Java类型是String。
* 这种类型的支持通常用于类型转换、数据绑定或其他需要明确指定支持类型的情境。
*
* @return 支持的Java类型的Class对象。在这个方法中,返回的是String.class。
*/
@Override
public Class<?> supportJavaTypeKey() {
return String.class;
}
/**
* 返回本类支持的Excel单元格数据类型。
*
* @return 返回CellDataTypeEnum.STRING,表示本类处理字符串类型的Excel单元格数据。
*/
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
/**
* 读excel文件转为Java对象的时候会自动调用
*
* @param context read converter context
* @return
* @throws Exception
*/
@Override
public String convertToJavaData(ReadConverterContext<?> context) throws Exception {
return "自定义:" + context.getReadCellData().getStringValue();
}
/**
* 写excel文件时自动调用
*
* @param context write context
* @return
* @throws Exception
*/
@Override
public WriteCellData<?> convertToExcelData(WriteConverterContext<String> context) throws Exception {
return new WriteCellData<>(context.getValue());
}
}
[!caution]
这个转换器和之前的自定义转换器有细微差别,注意比对。
对象:
@Data
public class UserData {
@ExcelProperty(value = "用户ID")
private Long id;
/**
* 我自定义 转换器,不管数据库传过来什么 。我给他加上“自定义:”
*/
@ExcelProperty(value = "用户昵称",converter = CustomStringStringConverter.class)
private String username;
/**
* 这里用string 去接日期才能格式化。我想接收年月日格式
*/
@DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
@ExcelProperty(value = "创建时间")
private String createTime;
}
监听器:参考最简单的读监听器。
方法示例:
@Test
public void converterRead() {
String fileName = TestFileUtil.getPath() + "simpleWrite.xlsx";
// 这里 需要指定读用哪个class去读,然后读取第一个sheet
EasyExcel.read(fileName, UserData.class, new UserDataListener())
// 这里注意 我们也可以registerConverter来指定自定义转换器, 但是这个转换变成全局了, 所有java为string,excel为string的都会用这个转换器。
// 如果就想单个字段使用请使用@ExcelProperty 指定converter
// .registerConverter(new CustomStringStringConverter())
// 读取sheet
.sheet().doRead();
}
2.3.5 多行头
Excel示例:参考之前1.3.1小节生成的文件。
监听器:参考最简单的读监听器。
示例方法:
@Test
public void complexHeaderRead() {
String fileName = TestFileUtil.getPath() + "simpleWrite.xlsx";
// 这里 需要指定读用哪个class去读,然后读取第一个sheet
EasyExcel.read(fileName, UserData.class, new UserDataListener())
.sheet()
// 这里可以设置1,因为头就是一行。如果多行头,可以设置其他值。不传入也可以,因为默认会根据UserData 来解析,他没有指定头,也就是默认1行
.headRowNumber(1).doRead();
}
这里有headRowNumber这个API,这里要注意headRowNumber
如果不指定, 会根据你传入的class
的ExcelProperty
中value
里面的表头的数量来决定行数,如果不传入class
则默认为1,当然你指定了headRowNumber
不管是否传入class
都是以你传入的为准。
2.3.5 同步的返回
Excel示例:参考之前1.3.1小节生成的文件。
示例方法:
@Test
public void synchronousRead() {
String fileName = TestFileUtil.getPath() + "simpleWrite.xlsx";
List<UserData> list = EasyExcel.read(fileName).head(UserData.class).sheet().doReadSync();
for (UserData data : list) {
log.info("读取到数据:{}", JSON.toJSONString(data));
}
// 这里 也可以不指定class,返回一个list,然后读取第一个sheet 同步读取会自动finish
List<Map<Integer, String>> listMap = EasyExcel.read(fileName).sheet().doReadSync();
for (Map<Integer, String> data : listMap) {
// 返回每条数据的键值对 表示所在的列 和所在列的值
log.info("读取到数据:{}", JSON.toJSONString(data));
}
}
[!caution]
- 同步的返回,不推荐使用,如果数据量大会把数据放到内存里面。
- 同步的返回,里面没有使用到自己定义的监听器,是用的同步读的监听器。
2.3.6 读取表头数据
Excel示例:参考之前1.3.1小节生成的文件。
监听器:参考最简单的读监听器,里面多了一个方法,只要重写invokeHeadMap
方法即可
/**
* 这里会一行行的返回头
*
* @param headMap
* @param context
*/
@Override
public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
log.info("解析到一条头数据:{}", JSON.toJSONString(headMap));
// 如果想转成成 Map<Integer,String>
// 方案1: 不要implements ReadListener 而是 extends AnalysisEventListener
// 方案2: 调用 ConverterUtils.convertToStringMap(headMap, context) 自动会转换
}
方法示例:
@Test
public void headerRead() {
String fileName = TestFileUtil.getPath() + "simpleWrite.xlsx";
EasyExcel.read(fileName, UserData.class, new UserDataListener()).sheet().doRead();
}
2.3.7 额外信息(批注、超链接、合并单元格信息读取)
对象:参考之前1.3.8小节的对象,但是数据类型要变一下。
package com.example.easyexcel;
import lombok.Data;
@Data
public class ReadCellDemoData {
/**
* 超链接
*
* @since 3.0.0-beta1
*/
private String hyperlink;
/**
* 备注
*
* @since 3.0.0-beta1
*/
private String commentData;
/**
* 公式
*
* @since 3.0.0-beta1
*/
private String formulaData;
/**
* 指定单元格的样式。当然样式 也可以用注解等方式。
*
* @since 3.0.0-beta1
*/
private String writeCellStyle;
/**
* 指定一个单元格有多个样式
*
* @since 3.0.0-beta1
*/
private String richText;
}
Excel示例:参考之前1.3.8小节生成的文件。
监听器:参考最简单的读监听器,里面多了一个 extra
方法。
@Slf4j
public class ReadCellListener implements ReadListener<ReadCellDemoData> {
@Override
public void invoke(ReadCellDemoData data, AnalysisContext context) {
log.info("解析到一条数据:{}", JSON.toJSONString(data));
}
@Override
public void extra(CellExtra extra, AnalysisContext context) {
log.info("读取到了一条额外信息:{}", JSON.toJSONString(extra));
switch (extra.getType()) {
case COMMENT:
log.info("额外信息是批注,在rowIndex:{},columnIndex;{},内容是:{}", extra.getRowIndex(), extra.getColumnIndex(),
extra.getText());
break;
case HYPERLINK:
if ("Sheet1!A1".equals(extra.getText())) {
log.info("额外信息是超链接,在rowIndex:{},columnIndex;{},内容是:{}", extra.getRowIndex(),
extra.getColumnIndex(), extra.getText());
} else if ("Sheet2!A1".equals(extra.getText())) {
log.info(
"额外信息是超链接,而且覆盖了一个区间,在firstRowIndex:{},firstColumnIndex;{},lastRowIndex:{},lastColumnIndex:{},"
+ "内容是:{}",
extra.getFirstRowIndex(), extra.getFirstColumnIndex(), extra.getLastRowIndex(),
extra.getLastColumnIndex(), extra.getText());
} else {
log.error("Unknown hyperlink!");
}
break;
case MERGE:
log.info(
"额外信息是超链接,而且覆盖了一个区间,在firstRowIndex:{},firstColumnIndex;{},lastRowIndex:{},lastColumnIndex:{}",
extra.getFirstRowIndex(), extra.getFirstColumnIndex(), extra.getLastRowIndex(),
extra.getLastColumnIndex());
break;
default:
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
log.info("所有数据解析完成!");
}
}
示例方法:
@Test
public void extraRead() {
String fileName = TestFileUtil.getPath() + "writeCellDataWrite.xlsx";
// 这里 需要指定读用哪个class去读,然后读取第一个sheet
EasyExcel.read(fileName, ReadCellDemoData.class, new ReadCellListener())
// 需要读取批注 默认不读取
.extraRead(CellExtraTypeEnum.COMMENT)
// 需要读取超链接 默认不读取
.extraRead(CellExtraTypeEnum.HYPERLINK)
// 需要读取合并单元格信息 默认不读取
.extraRead(CellExtraTypeEnum.MERGE).sheet().doRead();
}
[!caution]
由于是流式读取,没法在读取到单元格数据的时候直接读取到额外信息,所以只能最后通知哪些单元格有哪些额外信息。
2.3.8 读取公式和单元格类型
Excel示例:参考之前1.3.8小节生成的文件。
监听器:参考最简单的读监听器。
对象:参考之前1.3.8小节的对象,但是数据类型要变一下。
package com.example.easyexcel;
import com.alibaba.excel.metadata.data.CellData;
import lombok.Data;
@Data
public class ReadCellDemoData {
/**
* 超链接
*
* @since 3.0.0-beta1
*/
private CellData<String> hyperlink;
/**
* 备注
*
* @since 3.0.0-beta1
*/
private CellData<String> commentData;
/**
* 公式
*
* @since 3.0.0-beta1
*/
private CellData<String> formulaData;
/**
* 指定单元格的样式。当然样式 也可以用注解等方式。
*
* @since 3.0.0-beta1
*/
private CellData<String> writeCellStyle;
/**
* 指定一个单元格有多个样式
*
* @since 3.0.0-beta1
*/
private CellData<String> richText;
}
示例方法:
@Test
public void cellDataRead() {
String fileName = TestFileUtil.getPath() + "writeCellDataWrite.xlsx";
// 这里 需要指定读用哪个class去读,然后读取第一个sheet
EasyExcel.read(fileName, ReadCellDemoData.class, new ReadCellListener()).sheet().doRead();
}
2.3.9 数据转换等异常处理
Excel示例:参考之前1.3.1小节生成的文件。
对象:
package com.example.easyexcel;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import lombok.Data;
import java.util.Date;
@Data
public class UserData {
@ExcelProperty(value = "用户ID")
private Long id;
/**
* 用日期去接字符串 肯定报错
*/
@ExcelProperty(value = "用户昵称")
private Date username;
@DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
@ExcelProperty(value = "创建时间")
private Date createTime;
}
监听器:参考最简单的读监听器,里面多了一个方法,只要重写onException
方法即可
/**
* 在转换异常 获取其他异常下会调用本接口。抛出异常则停止读取。如果这里不抛出异常则 继续读取下一行。
*
* @param exception
* @param context
* @throws Exception
*/
@Override
public void onException(Exception exception, AnalysisContext context) throws Exception {
log.error("解析失败,但是继续解析下一行:{}", exception.getMessage());
// 如果是某一个单元格的转换异常 能获取到具体行号
// 如果要获取头的信息 配合invokeHeadMap使用
if (exception instanceof ExcelDataConvertException) {
ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException)exception;
log.error("第{}行,第{}列解析异常,数据为:{}", excelDataConvertException.getRowIndex(),
excelDataConvertException.getColumnIndex(), excelDataConvertException.getCellData());
}
}
方法示例:
@Test
public void exceptionRead() {
String fileName = TestFileUtil.getPath() + "simpleWrite.xlsx";
// 这里 需要指定读用哪个class去读,然后读取第一个sheet
EasyExcel.read(fileName, UserData.class, new UserDataListener()).sheet().doRead();
}
2.3.10 不创建对象的读
Excel示例:参考之前1.3.1小节生成的文件。
监听器:
package com.example.easyexcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.util.ListUtils;
import com.alibaba.fastjson2.JSON;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.Map;
@Slf4j
public class NoModelDataListener extends AnalysisEventListener<Map<Integer, String>> {
/**
* 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收
*/
private static final int BATCH_COUNT = 5;
private List<Map<Integer, String>> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
@Override
public void invoke(Map<Integer, String> data, AnalysisContext context) {
log.info("解析到一条数据:{}", JSON.toJSONString(data));
cachedDataList.add(data);
if (cachedDataList.size() >= BATCH_COUNT) {
saveData();
cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
saveData();
log.info("所有数据解析完成!");
}
/**
* 加上存储数据库
*/
private void saveData() {
log.info("{}条数据,开始存储数据库!", cachedDataList.size());
log.info("存储数据库成功!");
}
}
示例方法:
@Test
public void noModelRead() {
String fileName = TestFileUtil.getPath() + "simpleWrite.xlsx";
EasyExcel.read(fileName, new NoModelDataListener()).sheet().doRead();
}
2.3.11 web中的读
Excel示例:参考之前1.3.1小节生成的文件。
监听器:参考最简单的读监听器。
示例方法:
@Resource
private DemoDAO demoDAO;
/**
* 文件上传
*
* @param file
* @return
* @throws IOException
*/
@PostMapping("upload")
@ResponseBody
public String upload(MultipartFile file) throws IOException {
EasyExcel.read(file.getInputStream(), UserData.class, new UserDataListener(demoDAO)).sheet().doRead();
return "success";
}