在企业工作中,作为程序员,我们不可避免地或多或少需要使用Excel进行数据的导入导出。在JAVA程序中,我们常用 EasyExcel、Apache poi等框架进行数据的Excel操作。因为相比其它框架的优越的内存性能(详见下文官方文档与文档里的开源地址),许多企业优先选用 EasyExcel 进行 Excel 相关操作。本文着重介绍EasyExcel,根据官方文档,提供一些基础案例, 一起来边学边用吧!
参考文档:
EasyExcel 官方文档地址:https://easyexcel.opensource.alibaba.com/docs/current/
EasyExcel特点(总结自官方文档):
开源、效率高、使用方便、内存溢出风险低。
学习建议:
基础:JAVA语言基础(HttpServlet,IOStream)、数据库基础、一定的框架基础(Springboot 整合 SSM等)
标准:以官方文档为准,本文一些例子为个人学习过程中自己造的测试数据,以及参照官方文档自己写的例子。建议先阅读官方文档,再阅读本文。已阅读过官方文档的同学,通过本文作一个复习,可重点阅读“Web下的导入与导出”与“总结与补充”章节。
学习目标:
学会使用 EasyExcel 进行Excel 数据的导入、导出,并可将 EasyExcel 用于工作、个人项目。
依赖准备
<dependency>
<groupId>com.alibabagroupId>
<artifactId>easyexcelartifactId>
<version>3.1.0version>
dependency>
以下准备工作为可选项,根据读者需求选用。
准备数据表
准备测试数据:
准备实体类
/**
* @author: Sharry
* @createTime: 2023/3/6 14:50
* @version: Version-1.0
*/
@Data
public class DemoData implements Serializable {
private static final long serialVersionUID = 4194017968201414552L;
/**
* 主键id
*/
private Long id;
/**
* 测试类型:String名字
*/
private String name;
/**
* 测试类型:Date 日期
*/
private Date date;
/**
* 测试类型:Double 浮点数
*/
private Double doubleData;
}
准备MybatisPlus依赖及相关基础类
依赖:
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.4.2version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
配置类:
@Configuration
@MapperScan("cn.sharry.mplearning.mapper")
public class MybatisPlusConfig {
/**
* 分页插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
基础类:
/**
* @author: Sharry
* @createTime: 2023/3/6 15:22
* @version: Version-1.0
*/
@Repository
public interface DemoDataMapper extends BaseMapper<DemoData> {
}
/**
* @author: Sharry
* @createTime: 2023/3/6 15:23
* @version: Version-1.0
*/
public interface IDemoDataService extends IService<DemoData> {
}
/**
* @author: Sharry
* @createTime: 2023/3/6 15:24
* @version: Version-1.0
*/
@Service
public class DemoDataServiceImpl extends ServiceImpl<DemoDataMapper, DemoData> implements IDemoDataService {
}
准备生成测试数据的方法
以下方法建议写在测试类,或者自己方便找到的位置,接下来的学习,一些数据我们都需要基于以下测试数据。其中,生成的测试数据列表来自于官方文档,在表中直接查到的测试数据与生成的数据列表二选一用。
/**
* 通用测试数据
*/
private List<DemoData> data() {
List<DemoData> list = ListUtils.newArrayList();
for (int i = 0; i < 10; i++) {
DemoData data = new DemoData();
data.setName("字符串" + i);
data.setDate(new Date());
data.setDoubleData(0.56);
list.add(data);
}
return list;
}
@Autowired
private IDemoDataService demoDataService;
/**
* 获取表内的测试数据
*/
public List<DemoData> getTestData(){
return demoDataService.list();
}
Hutool工具包与Lombok(如果跟着本文操作则需要)
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>5.8.14version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
注意,以上准备工作均需要基于一个JAVA Maven工程(最好是SSM)。急需使用的同学,需要先看看公司所使用的框架是否已经集成EasyExcepl相关依赖。
提到导入与导出,在学习 EasyExcel 之前,我们首先会想到JAVA中的文件流。通过文件流,我们可以实现数据的文件读写。而Excel也是一种以".xlsx" 结尾的文件,因此我们使用EasyExcel,也会用到一些流的东西,整个导入导出流程,也和操作文件流类似。
我们先从导出Excel开始。
要导出Excel,我们首先要将实体类加上EasyExcel相关注解
/**
* @author: Sharry
* @createTime: 2023/3/6 14:50
* @version: Version-1.0
*/
@Data
@TableName("excel_demo_data")
public class DemoData implements Serializable {
private static final long serialVersionUID = 4194017968201414552L;
/**
* 主键id
*/
@ExcelProperty("主键ID")
private Long id;
/**
* 测试类型:String名字
*/
@ExcelProperty("字符串标题Name")
private String name;
/**
* 测试类型:Date 日期
*/
@ExcelProperty("日期标题Date")
private Date date;
/**
* 测试类型:Double 浮点数
*/
@ExcelProperty("浮点数标题DoubleData")
private Double doubleData;
/**
* 忽略这个字段
*/
@ExcelIgnore
@TableField(exist = false)
private String ignore;
}
测试类:
/**
* 测试简单写操作,导出Excel
*/
@Test
public void testSimpleWrite(){
//定义文件路径
String fileName = "E:\\DPlus\\Miscellaneous\\"+System.currentTimeMillis()+".xlsx";
log.trace("文件路径为{}",fileName);
//根据官方文档,以下操作会自动关闭文件流
//写法1
EasyExcel.write(fileName, DemoData.class)
.sheet("模板")
.doWrite(this::getTestData);
//写法2
EasyExcel.write(fileName, DemoData.class).sheet("模板").doWrite(getTestData());
//写法3
try (ExcelWriter excelWriter = EasyExcel.write(fileName,DemoData.class).build()){
WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
excelWriter.write(data(), writeSheet);
}
}
此处参照的是官方文档里提供的3种写法例子,读者可通过测试类选取测试,查看效果。此处案例的Excel会导出到我们定义的本地文件路径。
至此,我们完成了第一个导出!Hello EasyExcel !
在程序中定义表头的宽高、表头字段是常用的方法。有了第一个导出的基础,我们直接举例:
/*
* 以下涉及宽高的注解,为定义Excel表字段宽高的注解
*/
@Data
@ColumnWidth(25)
@HeadRowHeight(20)
@ContentRowHeight(18)
public class DemoData implements Serializable {
private static final long serialVersionUID = 4194017968201414552L;
/**
* 主键id
*/
@ColumnWidth(15)
@ExcelProperty("主键ID")
private Long id;
/**
* 测试类型:String名字
*/
@ColumnWidth(10)
@ExcelProperty("字符串标题Name")
private String name;
/**
* 测试类型:Date 日期
*/
@ColumnWidth(20)
@ExcelProperty("日期标题Date")
private Date date;
/**
* 测试类型:Double 浮点数
*/
@ColumnWidth(10)
@ExcelProperty("浮点数标题DoubleData")
private Double doubleData;
/**
* 忽略这个字段
*/
@ExcelIgnore
@ColumnWidth(30)
@TableField(exist = false)
private String ignore;
}
@Test
public void dynamicHeadWrite() {
//定义文件路径
String fileName = "E:\\DPlus\\Miscellaneous\\"+System.currentTimeMillis()+".xlsx";
EasyExcel.write(fileName)
// 动态头
.head(head()).sheet("模板")
.doWrite(data());
}
private List<List<String>> head() {
List<List<String>> list = new ArrayList<>();
List<String> head0 = new ArrayList<>();
head0.add("字符串");
List<String> head1 = new ArrayList<>();
head1.add("数字");
List<String> head2 = new ArrayList<>();
head2.add("日期");
list.add(head0);
list.add(head1);
list.add(head2);
return list;
}
实际工作中大部分导入导出Excel的需求都要求在Web端完成,此处是重点。同时,有了以上导出的基础,此处直接举例,读者应该易于理解:
/**
* @author: Sharry
* @createTime: 2023/3/7 11:48
* @version: Version-1.0
*/
@Slf4j
@RestController
@RequestMapping("/excel")
public class EasyExcelController {
@Autowired
private IDemoDataService demoDataService;
/**
* 导出Excel,代码参考自官方文档
*/
@GetMapping("/download")
public void download(HttpServletResponse response) throws IOException {
// 直接用浏览器或postman测试
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
// 防止中文乱码
String fileName
= URLEncoder.encode("测试", "UTF-8").replaceAll("\\+", "%20");
response.setHeader(
"Content-disposition",
"attachment;filename*=utf-8''"
+ fileName
+ ".xlsx");
EasyExcel.write(response.getOutputStream(), DemoData.class)
.sheet("模板")
.doWrite(demoDataService.list());
}
}
创建监听器类:
/**
* 读 Excel 监听器
* @author: Sharry
* @createTime: 2023/3/6 16:44
* @version: Version-1.0
*/
@Slf4j
public class DemoDataListener implements ReadListener<DemoData> {
/**
* 参照官方文档定义批量常量、缓存依据
*/
private static final int BATCH_COUNT = 100;
/**
* 缓存数据
*/
private List<DemoData> cacheDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
private IDemoDataService demoDataService;
/**
* 有参构造,参照官方文档,使用有Spring的方式
*/
public DemoDataListener(IDemoDataService demoDataService){
this.demoDataService = demoDataService;
}
/**
* 参考官方文档
* 每一条数据解析时都会调用
* @param demoData one row value. Is is same as {@link AnalysisContext#readRowHolder()}
* @param analysisContext 分析文本
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void invoke(DemoData demoData, AnalysisContext analysisContext) {
log.trace("解析到一条数据{}", JSONUtil.toJsonStr(demoData));
cacheDataList.add(demoData);
//达到 BATCH_COUNT , 执行持久化操作
if(cacheDataList.size() >= BATCH_COUNT){
log.info("{}条数据开始持久化!",cacheDataList.size());
boolean success = demoDataService.saveBatch(cacheDataList);
log.info("插入数据是否成功:{}",success);
}
}
/**
* 所有数据解析完成了,都会来调用
* @param analysisContext 分析文本
*/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
//最后遗留的数据也要存储到数据库
log.info("{}条数据开始持久化!",cacheDataList.size());
boolean success = demoDataService.saveOrUpdateBatch(cacheDataList);
log.info("插入数据是否成功:{}",success);
}
}
这个监听器类是根据官方文档创建的,在监听器内,我们重写了Invoke和 doAfterAllAnalysed方法并提供了持久化方法。最终读取结果可以在数据库里显示
准备数据
使用上文提到的数据准备方法,生成数据
测试方法
/**
* 测试简单的读操作,导入Excel
*/
@Test
public void testSimpleRead(){
//定义文件路径
String fileName = "E:\\DPlus\\Miscellaneous\\"+"1678149812045"+".xlsx";
//写法1 通过 listener 导入
try (ExcelReader excelReader = EasyExcel.read(
fileName,
DemoData.class,
new DemoDataListener(demoDataService)).build()){
ReadSheet readSheet = EasyExcel.readSheet(0).build();
excelReader.read(readSheet);
}
}
结果
其它写法举例
我们可以选用以下写法,或者匿名内部类来实现相同的功能。
EasyExcel.read(fileName, DemoData.class, new PageReadListener<DemoData>(
dataList -> dataList.forEach(
e ->log.info("读取到一条数据{}", JSONUtil.toJsonStr(e)))
)).sheet().doRead();
Sheet是指"工作表",excel里的概念,一个xlsx文件里可以有很多Sheet。多Sheet的情况相当于,一下把多张表的数据一起导入了,通过Sheet区分。我们实操看看:
@Test
public void testReadSheet(){
// 定义文件路径
String fileName = "E:\\DPlus\\Miscellaneous\\"+"1678149812045"+".xlsx";
// 以下分别执行测试
// 读取全部 Sheet
EasyExcel.read(fileName, DemoData.class, new DemoDataListener(demoDataService)).doReadAll();
// 读取指定 Sheet
try(ExcelReader excelReader = EasyExcel.read(fileName).build()){
ReadSheet sheet1 = EasyExcel
.readSheet(0)
.head(DemoData.class)
.registerReadListener(new DemoDataListener(demoDataService))
.build();
ReadSheet sheet2 = EasyExcel
.readSheet(1)
.head(DemoData.class)
.registerReadListener(new DemoDataListener(demoDataService))
.build();
excelReader.read(sheet1,sheet2);
}
}
作为JAVA后端码农,我们导入导出excel的大部分需求都是在Web环境下进行的,以下直接举例Web导入:
/**
* 上传Excel,代码参考自官方文档
*/
@PostMapping("/upload")
public JsonResult<Void> upload(MultipartFile file) throws IOException {
EasyExcel.read(file.getInputStream(),
DemoData.class,
new DemoDataListener(demoDataService))
.sheet().doRead();
return JsonResult.success();
}
通过 postman 测试结果:
填充 Excel 也是常见的需求之一,在给用户导出 Excel 时,有一种解决方案便是给定一个模板,然后填充模板,再给用户下载。
这里我们直接参考官方文档的例子:
实体类
/**
* @author Sharry
*/
@Data
public class FillData implements Serializable {
private static final long serialVersionUID = 1273825356454488497L;
private String name;
private double number;
private Date date;
}
准备模板
测试类
@Test
public void simpleFill() {
// 模板注意 用{} 来表示你要用的变量 如果本来就有"{","}" 特殊字符 用"\{","\}"代替
// 定义文件路径
String templateFileName = "E:\\DPlus\\Miscellaneous\\"+"1678149812045"+".xlsx";
// 方案1 根据对象填充
String fileName = "E:\\DPlus\\Miscellaneous\\"+System.currentTimeMillis()+".xlsx";
// 这里 会填充到第一个sheet, 然后文件流会自动关闭
FillData fillData = new FillData();
fillData.setName("张三");
fillData.setNumber(5.2);
fillData.setDate(new Date(System.currentTimeMillis()));
EasyExcel.write(fileName).withTemplate(templateFileName).sheet().doFill(fillData);
// 这里 会填充到第一个sheet, 然后文件流会自动关闭
Map<String, Object> map = MapUtils.newHashMap();
map.put("name", "张三");
map.put("number", 5.2);
fillData.setDate(new Date(System.currentTimeMillis()));
EasyExcel.write(fileName).withTemplate(templateFileName).sheet().doFill(map);
}
本文只为读者入门或快速复习EasyExcel,提供了最常用、最基本的例子。官方还提供了不同情况下的Excel导入导出Demo(详见官方文档),大多都大同小异。
在下面的小节做一个简单的补充说明。
通过上面章节的学习,我们已经了解了 EasyExcel 的一些基本使用,以下链接为官方提供的Demo代码链接,供读者参考:
官方读Excel示例代码:https://github.com/alibaba/easyexcel/blob/master/easyexcel-test/src/test/java/com/alibaba/easyexcel/test/demo/read/ReadTest.java
官方写Excel示例代码:https://github.com/alibaba/easyexcel/blob/master/easyexcel-test/src/test/java/com/alibaba/easyexcel/test/demo/write/WriteTest.java
官方填充Excel示例代码:https://github.com/alibaba/easyexcel/blob/master/easyexcel-test/src/test/java/com/alibaba/easyexcel/test/demo/fill/FillTest.java
我们在实际开发中,为了提升效率,我们通常会使用一些框架。在市面上的商用框架中,许多框架已经对EasyExcel进行了一步封装,此时,我们就要遵循框架封装过后的使用规则。举个例子:
@PostMapping("write-notice")
public R<Boolean> writeNotice(MultipartFile file) {
List<Notice> noticeList = new ArrayList<>();
List<NoticeExcel> list = ExcelUtil.read(file, NoticeExcel.class);
list.forEach(noticeExcel -> {
String category = DictCache.getKey("notice", noticeExcel.getCategoryName());
noticeExcel.setCategory(Func.toInt(category));
Notice notice = BeanUtil.copy(noticeExcel, Notice.class);
noticeList.add(notice);
});
return R.data(noticeService.saveBatch(noticeList));
}
上述代码提到的 “ExcelUtil” 就是框架已经封装好的一个Excel工具,不同的框架的封装方式可能不同,实际开发中注意使用。
在学习和使用EasyExcel过程中,我们不难发现,其实Excel相关的操作大部分情况都是基于流的。作为JAVA语言基础之一,我们应该对流多加学习、复习、练习。
最后,我们一起来梳理一下实际工作中可能会用到的EasyExcel情况及使用方法: