EasyExcel 是一个基于 Java 的简单、省内存的读写 Excel 的开源项目,在尽可能节约内存的情况下支持读写百 M 的 Excel。
但注意,其不支持:
常见问题
新建 Maven 项目,如下配置:
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<maven.compiler.source>8maven.compiler.source>
<maven.compiler.target>8maven.compiler.target>
properties>
<dependencies>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>easyexcelartifactId>
<version>3.1.1version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.22version>
<scope>providedscope>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.47version>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
<scope>testscope>
dependency>
dependencies>
准备如下 excel 文件。
编写数据类,用来对从 Excel 文件读取到文件内容进行封装。
@Data
public class PersonData {
private Long id;
private Date birthday;
private String name;
}
编写读取代码,使用 EasyExcel 提供 API 读取 Excel 文件内容。
// 不要求:JDK8+
// 不用额外写一个 XxxListener,直接用内置 ReadListener
@Test
public void read() {
String fileName = "Excel 文件路径";
// 这里需要指定读用哪个 class 去读,然后读取第一个 sheet 文件流会自动关闭
EasyExcel.read(fileName, PersonData.class, new ReadListener<PersonData>() {
/**
* 单次缓存的数据量
*/
private static final int BATCH_COUNT = 100;
/**
* 临时存储
*/
private List<PersonData> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
@Override
public void invoke(PersonData data, AnalysisContext context) {
cachedDataList.add(data);
if (cachedDataList.size() >= BATCH_COUNT) {
printData();
// 存储完成清理 list
cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
printData();
}
/**
* 打印数据
*/
private void printData() {
for (PersonData personData : cachedDataList) {
System.out.println(personData);
}
}
}).sheet().doRead();
}
// 要求:JDK8+,EasyExcel since:3.0.0-beta1
// 不用额外写一个 XxxListener,直接用内置 PageReadListener
@Test
public void read() {
String fileName = "Excel 文件路径";
// 这里 需要指定读用哪个 class 去读,然后读取第一个 sheet 文件流会自动关闭
// 这里每次会读取 100 条数据 然后返回过来 直接调用使用数据就行
EasyExcel.read(fileName, PersonData.class, new PageReadListener<PersonData>(dataList -> {
dataList.forEach(System.out::println);
})).sheet().doRead();
}
@Data
public class PersonData {
@ExcelProperty(index = 1) // 指定读取的下标,下标默认从 0 开始。这里不建议 index 和 name 同时用,要么一个对象只用 index,要么一个对象只用 name 去匹配
private Long id;
@ExcelProperty("出生日期") // 指定读取的列名
private Date birthday;
@ExcelProperty("姓名") // 指定读取的列名
private String name;
}
读取时抛出异常,则代码会停止。若在监听器中重写处理异常的方法,则代码还会正常执行,继续读取其他行的数据。
准备如下 excel 文件。
编写读取代码。
@Test
public void read() {
String fileName = "Excel 文件路径";
EasyExcel.read(fileName, PersonData.class, new ReadListener<PersonData>() {
/**
* 单次缓存的数据量
*/
private static final int BATCH_COUNT = 100;
/**
* 临时存储
*/
private List<PersonData> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
// 读取时出现异常会执行如下方法,并且不导致读取操作终止
public void onException(Exception exception, AnalysisContext context) {
// 打印异常
System.out.println(exception.getMessage());
}
@Override
public void invoke(PersonData data, AnalysisContext context) {
cachedDataList.add(data);
if (cachedDataList.size() >= BATCH_COUNT) {
printData();
// 存储完成清理 list
cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
printData();
}
/**
* 打印数据
*/
private void printData() {
for (PersonData personData : cachedDataList) {
System.out.println(personData);
}
}
})
.sheet().doRead();
}
先准备好如下 Excel 文件。
编写读取代码。
// 读多个或者全部 sheet,这里注意一个 sheet 不能读取多次,多次读取需要重新读取文件
//----------------------------读取全部 sheet---------
@Test
public void read() {
String fileName = "Excel 文件路径";
// 读取全部 sheet
// PageReadListener 的 doAfterAllAnalysed 会在每个 sheet 读取完毕后调用一次。
EasyExcel.read(fileName, PersonData.class, new PageReadListener<PersonData>(dataList -> {
dataList.forEach(System.out::println);
})).doReadAll();
// ---------------------读取指定或者所有 sheet----------------------------
try (ExcelReader excelReader = EasyExcel.read(fileName).build()) {
//创建读的工作簿对象
ReadSheet readSheet1 = EasyExcel.readSheet(0)
.head(PersonData.class)
.registerReadListener(new PageReadListener<PersonData>(dataList -> {
dataList.forEach(System.out::println);
}))
.build();
ReadSheet readSheet2 = EasyExcel.readSheet(1)
.head(PersonData.class)
.registerReadListener(new PageReadListener<PersonData>(dataList -> {
dataList.forEach(System.out::println);
}))
.build();
// 这里注意 一定要把工作簿一起传进去,不然有个问题就是 03 版的 Excel 会读取多次,浪费性能
excelReader.read(readSheet1, readSheet2);
}
}
// --------------------指定读取某个 sheet------------------------------
@Test
public void read() {
String fileName = "Excel 文件路径";
EasyExcel.read(fileName, PersonData.class, new PageReadListener<PersonData>(dataList -> {
dataList.forEach(System.out::println);
})).sheet(0).doRead();
EasyExcel.read(fileName, PersonData.class, new PageReadListener<PersonData>(dataList -> {
dataList.forEach(System.out::println);
})).sheet(1).doRead();
}
先准备好如下 Excel 文件。
public class CustomStringStringConverter implements Converter<String> {
// 支持 Java 的类型
@Override
public Class<?> supportJavaTypeKey() {
return String.class;
}
// 支持 Excel 单元格类型
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
/**
* 当读到的数据符合类型,封装的字段也符合类型,按照下面方法转换数据
* 这么转换,是由开发者在方法中重写
* @param context
* @return
*/
@Override
public String convertToJavaData(ReadConverterContext<?> context) {
//context.getReadCellData().getStringValue(): 是原本的数据
return "大神:" + context.getReadCellData().getStringValue();
}
/**
* 这里是写的时候会调用
* 当读到的数据符合类型,封装的字段也符合类型,按照下面方法转换数据
* 这么转换,是由开发者在方法中重写
* @return
*/
@Override
public WriteCellData<?> convertToExcelData(WriteConverterContext<String> context) {
return new WriteCellData<>(context.getValue());
}
}
编写数据类。
@Data
public class PlayerData {
@ExcelProperty(converter = CustomStringStringConverter.class)//----------->转换器
private String name;
@DateTimeFormat("yyyy年MM月dd日")
private Date birthday;
private BigDecimal bf;
}
编写读取代码。
@Test
public void read() {
String fileName = "Excel 文件路径";
// 这里需要指定读用哪个 class 去读,然后读取第一个 sheet 文件流会自动关闭
// 这里每次会读取 100 条数据 然后返回过来 直接调用使用数据就行
EasyExcel.read(fileName, PlayerData.class, new PageReadListener<PlayerData>(dataList -> {
dataList.forEach(System.out::println);
}))
// 如果就想单个字段使用请使用 @ExcelProperty 指定 converter
// .registerConverter(new CustomStringStringConverter())
.sheet().doRead();
}
默认情况下 EasyExcel 从第 1 行(0 行是列头)读取。把上面测试代码修改如下进行测试。
@Test
public void read() {
String fileName = "Excel 文件路径";
EasyExcel.read(fileName, PlayerData.class, new PageReadListener<PlayerData>(dataList -> {
dataList.forEach(System.out::println);
}))
.sheet()
.headRowNumber(2) // 手动指定从从第 2 行读取
.doRead();
}
修改数据类。
@Data
public class PersonData {
private Long id;
private Date birthday;
private String name;
private String ignore;
public PersonData() {}
public PersonData(Long id, Date birthday, String name) {
this.id = id;
this.birthday = birthday;
this.name = name;
}
}
编写模拟提供数据类。
public class PersonMock {
public static List<PersonData> data() {
return Arrays.asList(
new PersonData(1L, new Date(), "刘备"),
new PersonData(2L, new Date(), "关羽"),
new PersonData(3L, new Date(), "张飞"),
new PersonData(4L, new Date(), "赵云"),
new PersonData(5L, new Date(), "诸葛亮"),
new PersonData(6L, new Date(), "黄忠"),
new PersonData(7L, new Date(), "魏延"),
new PersonData(8L, new Date(), "庞统"),
new PersonData(9L, new Date(), "法正"),
new PersonData(10L, new Date(), "黄权")
);
}
}
编写写入代码。
// 注意在数据量不大的情况下可以使用(5000 以内,具体也要看实际情况),数据量大请参照后面重复多次写入
@Test
public void write() {
// 写法 1,要求:JDK8+,EasyExcel since:3.0.0-beta1
String fileName = "Excel 文件路径";
// 这里需要指定写用哪个 class 去写,然后写到第一个 sheet,工作薄名字为模板,然后文件流会自动关闭
EasyExcel.write(fileName, PersonData.class)
.sheet("模板")
.doWrite(PersonMock::data);//这才是真正的写入
// 写法 2
// 这里需要指定写用哪个 class 去写
try (ExcelWriter excelWriter = EasyExcel.write(fileName, PersonData.class).build()) {
WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
excelWriter.write(PersonMock::data, writeSheet);
}
}
效果如下:
方式 1:修改数据类,使用注解 @ExcelIgnore。
@Data
public class PersonData {
private Long id;
private Date birthday;
private String name;
/**
* 忽略这个字段
*/
// @ExcelIgnore
private String ignore;
public PersonData() {}
public PersonData(Long id, Date birthday, String name) {
this.id = id;
this.birthday = birthday;
this.name = name;
}
}
效果如下:
方式 2:修改测试代码。
@Test
public void write() {
String fileName = "Excel 文件路径";
Set<String> excludeColumnFieldNames = new HashSet<>();
excludeColumnFieldNames.add("birthday");
EasyExcel.write(fileName, PersonData.class)
.excludeColumnFieldNames(excludeColumnFieldNames) // 指定写入时排除哪些列
.sheet("模板")
.doWrite(PersonMock::data);
}
效果如下:
@Test
public void write() {
String fileName = "Excel 文件路径";
Set<String> includeColumnFieldNames = new HashSet<>();
includeColumnFieldNames.add("id");
includeColumnFieldNames.add("name");
EasyExcel.write(fileName, PersonData.class)
.includeColumnFieldNames(includeColumnFieldNames) // 指定写入时包含哪些列
.sheet("模板")
.doWrite(PersonMock::data);
}
修改数据类,使用 @ExcelProperty 注解指定写入时的列名和顺序。
@Data
public class PersonData {
@ExcelProperty(value = "id", index = 1) // 这个注解可以指定读写时的列名及顺序
private Long id;
@ExcelProperty(value = "出生日期", index = 3)
private Date birthday;
@ExcelProperty(value = "姓名", index = 2)
private String name;
public PersonData() {}
public PersonData(Long id, Date birthday, String name) {
this.id = id;
this.birthday = birthday;
this.name = name;
}
}
编写写入代码。
@Test
public void write() {
String fileName = "Excel 文件路径";
EasyExcel.write(fileName, PersonData.class)
.sheet("模板")
.doWrite(PersonMock::data);
}
效果如下:
修改数据类。
@Data
public class PersonData {
@ExcelProperty({"蜀国", "id"}) // 第一个主列名,第二次级列名
private Long id;
@ExcelProperty({"蜀国", "出生日期"})
private Date birthday;
@ExcelProperty({"蜀国", "姓名"})
private String name;
public PersonData() {}
public PersonData(Long id, Date birthday, String name) {
this.id = id;
this.birthday = birthday;
this.name = name;
}
}
编写写入代码。
@Test
public void write() {
String fileName = "Excel 文件路径";
EasyExcel.write(fileName, PersonData.class)
.sheet("模板")
.doWrite(PersonMock::data);
}
效果如下:
支持单个 sheet 多次写入,或者支持写多个 sheet。
往一个 sheet 多次写入,写入代码如下:
@Test
public void write() {
String fileName = "Excel 文件路径";
try (ExcelWriter excelWriter = EasyExcel.write(fileName, PersonData.class).build()) {
// 注意若同一个 sheet 只要创建一次
WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
// 调用 2 次写入,实际使用时根据数据库分页的总的页数来
for (int i = 0; i < 2; i++) {
// 分页去数据库查询数据 这里可以去数据库查询每一页的数据
List<PersonData> data = PersonMock.data();
excelWriter.write(data, writeSheet);
}
}
}
效果如下:
写入代码如下:
@Test
public void write() {
String fileName = "Excel 文件路径";
try (ExcelWriter excelWriter = EasyExcel.write(fileName, PersonData.class).build()) {
for (int i = 0; i < 2; i++) {
// 每次都要创建 writeSheet,需要指定 sheet 缩影,而且 sheet 名称必须不一样
WriteSheet writeSheet = EasyExcel.writerSheet(i, "模板" + i).build();
// 写入数据
excelWriter.write(PersonMock.data(), writeSheet);
}
}
}
效果如下:
修改数据类。
@Data
public class PlayerData {
private String name;
private Date birthday;
private BigDecimal bf;
public PlayerData() { }
public PlayerData(String name, Date birthday, BigDecimal bf) {
this.name = name;
this.birthday = birthday;
this.bf = bf;
}
}
编写模拟提供数据类。
public class PlayerMock {
public static List<PlayerData> data() {
return Arrays.asList(
new PlayerData("C罗", new Date(), new BigDecimal("0.07")),
new PlayerData("乔丹", new Date(), new BigDecimal("0.03"))
);
}
}
编写写入代码。
public void write5() {
String fileName = "Excel 文件路径";
try (ExcelWriter excelWriter = EasyExcel.write(fileName).build()) {
// 向第一个 sheet 写入 PersonData 类型的数据
WriteSheet writeSheet0 = EasyExcel.writerSheet(0, "蜀国")
.head(PersonData.class) // 设置第一行列标题,列头
.build();
excelWriter.write(PersonMock.data(), writeSheet0);
// 向第二个 sheet 写入 PlayerData 类型的数据
WriteSheet writeSheet1 = EasyExcel.writerSheet(1, "运动员")
.head(PlayerData.class) // // 设置第一行列标题
.build();
excelWriter.write(PlayerMock.data(), writeSheet1);
}
}
效果如下:
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());
}
}
@Data
public class PlayerData {
@ExcelProperty(converter = CustomStringStringConverter.class)//------------>转换器
private String name;
@DateTimeFormat("yyyy年MM月dd日")
private Date birthday;
@NumberFormat("#.##%")
private BigDecimal bf;
public PlayerData() { }
public PlayerData(String name, Date birthday, BigDecimal bf) {
this.name = name;
this.birthday = birthday;
this.bf = bf;
}
}
编写写入的代码。
@Test
public void write() {
String fileName = "Excel 文件路径";
EasyExcel.write(fileName, PlayerData.class)
.sheet("模板")
.doWrite(PlayerMock::data);
}
效果如下:
修改数据类,使用注解指定列宽、行高。
@Data
@HeadRowHeight(30) // 指定列头行高度
@ContentRowHeight(20) // 指定内容行高度
@ColumnWidth(25) // 指定列宽
public class PlayerData {
private String name;
private Date birthday;
private BigDecimal bf;
public PlayerData() { }
public PlayerData(String name, Date birthday, BigDecimal bf) {
this.name = name;
this.birthday = birthday;
this.bf = bf;
}
}
编写写入的代码。
@Test
public void write() {
String fileName = "Excel 文件路径";
EasyExcel.write(fileName, PlayerData.class)
.sheet("模板")
.doWrite(PlayerMock::data);
}
效果如下:
新建 Spring Boot 项目,添加如下依赖:
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>easyexcelartifactId>
<version>3.1.1version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.47version>
dependency>
dependencies>
把上面项目的的数据类 PersonData 和提供模拟数据类 PersonMock 拷贝到这个项目。
所谓导出,就把后端数据库中数据,通过网络已 Excel 文件形式下载到客户端。
@GetMapping("/export")
public void exportExcel(HttpServletResponse response) throws IOException {
response.setHeader("Content-disposition", "attachment;filename=person.xlsx");
EasyExcel.write(response.getOutputStream(), PersonData.class)
.sheet("模板")
.doWrite(PersonMock::data);
}
所谓导入,就把客户端 Excel 文件上传到后端,后端通过代码读取 Excel 文件的数据存入到数据库中。
@PostMapping("/import")
@ResponseBody
public String importExcel(MultipartFile file) throws IOException {
EasyExcel.read(file.getInputStream(), PersonData.class, new PageReadListener<PersonData>((dataList) -> {
dataList.forEach(System.out::println);
}))
.sheet().doRead();
return "success";
}
@PostMapping("/import")
@ResponseBody
public String importExcel(MultipartFile file) throws IOException {
// 同步读取(适合数据量小)
List<Object> objects = EasyExcel.read(file.getInputStream()).head(PersonData.class).sheet().doReadSync();
for (Object object : objects) {
System.out.println(object);
}
return "success";
}