java的web开发需要excel的导入导出工具,所以需要一定的工具类实现,如果是使用easypoi、hutool导入导出excel,会非常的损耗内存,因此可以尝试使用easyexcel解决大数据量的数据的导入导出,且可以通过java8的函数式编程解决该问题。
使用easyexcel,虽然不太会出现oom的问题,但是如果是大数据量的情况下也会有一定量的内存溢出的风险,所以我打算从以下几个方面优化这个问题:
- 使用java8的函数式编程实现低代码量的数据导入
- 使用反射等特性实现单个接口导入任意excel
- 使用线程池实现大数据量的excel导入
- 通过泛型实现数据导出
maven导入
<!--easyexcel相关依赖-->
<dependency>
<groupid>com.alibaba</groupid>
<artifactid>easyexcel</artifactid>
<version>3.0.5</version>
</dependency>使用泛型实现对象的单个sheet导入
先实现一个类,用来指代导入的特定的对象
@data
@noargsconstructor
@allargsconstructor
@tablename("stu_info")
@apimodel("学生信息")
//@excelignoreunannotated 没有注解的字段都不转换
public class stuinfo {
private static final long serialversionuid = 1l;
/**
* 姓名
*/
// 设置字体,此处代表使用斜体
// @contentfontstyle(italic = booleanenum.true)
// 设置列宽度的注解,注解中只有一个参数value,value的单位是字符长度,最大可以设置255个字符
@columnwidth(10)
// @excelproperty 注解中有三个参数value,index,converter分别代表表名,列序号,数据转换方式
@apimodelproperty("姓名")
@excelproperty(value = "姓名",order = 0)
@exportheader(value = "姓名",index = 1)
private string name;
/**
* 年龄
*/
// @excelignore不将该字段转换成excel
@excelproperty(value = "年龄",order = 1)
@apimodelproperty("年龄")
@exportheader(value = "年龄",index = 2)
private integer age;
/**
* 身高
*/
//自定义格式-位数
// @numberformat("#.##%")
@excelproperty(value = "身高",order = 2)
@apimodelproperty("身高")
@exportheader(value = "身高",index = 4)
private double tall;
/**
* 自我介绍
*/
@excelproperty(value = "自我介绍",order = 3)
@apimodelproperty("自我介绍")
@exportheader(value = "自我介绍",index = 3,ignore = true)
private string selfintroduce;
/**
* 图片信息
*/
@excelproperty(value = "图片信息",order = 4)
@apimodelproperty("图片信息")
@exportheader(value = "图片信息",ignore = true)
private blob picture;
/**
* 性别
*/
@excelproperty(value = "性别",order = 5)
@apimodelproperty("性别")
private integer gender;
/**
* 入学时间
*/
//自定义格式-时间格式
@datetimeformat("yyyy-mm-dd hh:mm:ss:")
@excelproperty(value = "入学时间",order = 6)
@apimodelproperty("入学时间")
private string intake;
/**
* 出生日期
*/
@excelproperty(value = "出生日期",order = 7)
@apimodelproperty("出生日期")
private string birthday;
}重写readlistener接口
@slf4j
public class uploaddatalistener<t> implements readlistener<t> {
/**
* 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收
*/
private static final int batch_count = 100;
/**
* 缓存的数据
*/
private list<t> cacheddatalist = listutils.newarraylistwithexpectedsize(batch_count);
/**
* predicate用于过滤数据
*/
private predicate<t> predicate;
/**
* 调用持久层批量保存
*/
private consumer<collection<t>> consumer;
public uploaddatalistener(predicate<t> predicate, consumer<collection<t>> consumer) {
this.predicate = predicate;
this.consumer = consumer;
}
public uploaddatalistener(consumer<collection<t>> consumer) {
this.consumer = consumer;
}
/**
* 如果使用了spring,请使用这个构造方法。每次创建listener的时候需要把spring管理的类传进来
*
* @param demodao
*/
/**
* 这个每一条数据解析都会来调用
*
* @param data one row value. is is same as {@link analysiscontext#readrowholder()}
* @param context
*/
@override
public void invoke(t data, analysiscontext context) {
if (predicate != null && !predicate.test(data)) {
return;
}
cacheddatalist.add(data);
// 达到batch_count了,需要去存储一次数据库,防止数据几万条数据在内存,容易oom
if (cacheddatalist.size() >= batch_count) {
try {
// 执行具体消费逻辑
consumer.accept(cacheddatalist);
} catch (exception e) {
log.error("failed to upload data!data={}", cacheddatalist);
throw new bizexception("导入失败");
}
// 存储完成清理 list
cacheddatalist = listutils.newarraylistwithexpectedsize(batch_count);
}
}
/**
* 所有数据解析完成了 都会来调用
*
* @param context
*/
@override
public void doafterallanalysed(analysiscontext context) {
// 这里也要保存数据,确保最后遗留的数据也存储到数据库
if (collutil.isnotempty(cacheddatalist)) {
try {
// 执行具体消费逻辑
consumer.accept(cacheddatalist);
log.info("所有数据解析完成!");
} catch (exception e) {
log.error("failed to upload data!data={}", cacheddatalist);
// 抛出自定义的提示信息
if (e instanceof bizexception) {
throw e;
}
throw new bizexception("导入失败");
}
}
}
}controller层的实现
@apioperation("只需要一个readlistener,解决全部的问题")
@postmapping("/update")
@responsebody
public r<string> alistener4allexcel(multipartfile file) throws ioexception {
try {
easyexcel.read(file.getinputstream(),
stuinfo.class,
new uploaddatalistener<stuinfo>(
list -> {
// 校验数据
// validationutils.validate(list);
// dao 保存···
//最好是手写一个,不要使用mybatis-plus的一条条新增的逻辑
service.savebatch(list);
log.info("从excel导入数据一共 {} 行 ", list.size());
}))
.sheet()
.doread();
} catch (ioexception e) {
log.error("导入失败", e);
throw new bizexception("导入失败");
}
return r.success("success");
}但是这种方式只能实现已存对象的功能实现,如果要新增一种数据的导入,那我们需要怎么做呢?
可以通过读取成map,根据顺序导入到数据库中。
通过实现单个sheet中任意一种数据的导入
controller层的实现
@apioperation("只需要一个readlistener,解决全部的问题")
@postmapping("/listenmapdara")
@responsebody
public r<string> listenmapdara(@apiparam(value = "表编码", required = true)
@notblank(message = "表编码不能为空")
@requestparam("tablecode") string tablecode,
@apiparam(value = "上传的文件", required = true)
@notnull(message = "上传文件不能为空") multipartfile file) throws ioexception {
try {
//根据tablecode获取这张表的字段,可以作为insert与剧中的信息
easyexcel.read(file.getinputstream(),
new nonclazzorientedlistener(
list -> {
// 校验数据
// validationutils.validate(list);
// dao 保存···
log.info("从excel导入数据一共 {} 行 ", list.size());
}))
.sheet()
.doread();
} catch (ioexception e) {
log.error("导入失败", e);
throw new bizexception("导入失败");
}
return r.success("success");
}重写readlistener接口
@slf4j
public class nonclazzorientedlistener implements readlistener<map<integer, string>> {
/**
* 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收
*/
private static final int batch_count = 100;
private list<list<object>> rowslist = listutils.newarraylistwithexpectedsize(batch_count);
private list<object> rowlist = new arraylist<>();
/**
* predicate用于过滤数据
*/
private predicate<map<integer, string>> predicate;
/**
* 调用持久层批量保存
*/
private consumer<list> consumer;
public nonclazzorientedlistener(predicate<map<integer, string>> predicate, consumer<list> consumer) {
this.predicate = predicate;
this.consumer = consumer;
}
public nonclazzorientedlistener(consumer<list> consumer) {
this.consumer = consumer;
}
/**
* 添加devicename标识
*/
private boolean flag = false;
@override
public void invoke(map<integer, string> row, analysiscontext analysiscontext) {
consumer.accept(rowslist);
rowlist.clear();
row.foreach((k, v) -> {
log.debug("key is {},value is {}", k, v);
rowlist.add(v == null ? "" : v);
});
rowslist.add(rowlist);
if (rowslist.size() > batch_count) {
log.debug("执行存储程序");
log.info("rowslist is {}", rowslist);
rowslist.clear();
}
}
@override
public void doafterallanalysed(analysiscontext analysiscontext) {
consumer.accept(rowslist);
if (collutil.isnotempty(rowslist)) {
try {
log.debug("执行最后的程序");
log.info("rowslist is {}", rowslist);
} catch (exception e) {
log.error("failed to upload data!data={}", rowslist);
// 抛出自定义的提示信息
if (e instanceof bizexception) {
throw e;
}
throw new bizexception("导入失败");
} finally {
rowslist.clear();
}
}
}这种方式可以通过把表中的字段顺序存储起来,通过配置数据和字段的位置实现数据的新增,那么如果出现了导出数据模板/手写excel的时候顺序和导入的时候顺序不一样怎么办?
可以通过读取header进行实现,通过表头读取到的字段,和数据库中表的字段进行比对,只取其中存在的数据进行排序添加
/**
* 这里会一行行的返回头
*
* @param headmap
* @param context
*/
@override
public void invokehead(map<integer, readcelldata<?>> headmap, analysiscontext context) {
//该方法必然会在读取数据之前进行
map<integer, string> colummap = converterutils.converttostringmap(headmap, context);
//通过数据交互拿到这个表的表头
// map<string,string> columnlist=dao.xxxx();
map<string, string> columnlist = new hashmap();
colummap.foreach((key, value) -> {
if (columnlist.containskey(value)) {
filterlist.add(key);
}
});
//过滤到了只存在表里面的数据,顺序就不用担心了,可以直接把filterlist的数据用于排序,可以根据mybatis做一个动态sql进行应用
log.info("解析到一条头数据:{}", json.tojsonstring(colummap));
// 如果想转成成 map<integer,string>
// 方案1: 不要implements readlistener 而是 extends analysiseventlistener
// 方案2: 调用 converterutils.converttostringmap(headmap, context) 自动会转换
}那么这些问题都解决了,如果出现大数据量的情况,如果要极大的使用到cpu,该怎么做呢?
可以尝试使用线程池进行实现
使用线程池进行多线程导入大量数据
java中线程池的开发与使用与原理我可以单独写一篇文章进行讲解,但是在这边为了进行好的开发我先给出一套固定一点的方法。
由于readlistener不能被注册到ioc容器里面,所以需要在外面开启
详情可见spring boot通过easyexcel异步多线程实现大数据量excel导入,百万数据30秒
通过泛型实现对象类型的导出
public <t> void commonexport(string filename, list<t> data, class<t> clazz, httpservletresponse response) throws ioexception {
if (collectionutil.isempty(data)) {
data = new arraylist<>();
}
//设置标题
filename = urlencoder.encode(filename, "utf-8");
response.setcontenttype("application/vnd.ms-excel");
response.setcharacterencoding("utf-8");
response.setheader("content-disposition", "attachment;filename=" + filename + ".xlsx");
easyexcel.write(response.getoutputstream()).head(clazz).sheet("sheet1").dowrite(data);
}直接使用该方法可以作为公共的数据的导出接口
如果想要动态的下载任意一组数据怎么办呢?可以使用这个方法
public void exportfreely(string filename, list<list<object>> data, list<list<string>> head, httpservletresponse response) throws ioexception {
if (collectionutil.isempty(data)) {
data = new arraylist<>();
}
//设置标题
filename = urlencoder.encode(filename, "utf-8");
response.setcontenttype("application/vnd.ms-excel");
response.setcharacterencoding("utf-8");
response.setheader("content-disposition", "attachment;filename=" + filename + ".xlsx");
easyexcel.write(response.getoutputstream()).head(head).sheet("sheet1").dowrite(data);
}什么?不仅想一个接口展示全部的数据与信息,还要增加筛选条件?这个后期我可以单独写一篇文章解决这个问题。
到此这篇关于springboot整合easyexcel实现一个接口任意表的excel导入导出的文章就介绍到这了,更多相关springboot excel导入导出内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论