EasyExcel导入导出
在项目开发中往往需要使用到数据的导入和导出,导入就是从Excel中导入到DB中,而导出就是从DB中查询数据然后使用POI写到Excel上。所以今天就为大家带来一款基于阿里EasyExcel的导入导出功能,开放了一个demo,以下是gitee地址,有兴趣的可以去看看:EasyExcelDemo:基于EasyExcel实现导入导出的简易demo
该demo以及我下面所要分享的都是使用的mongodb来做的,使用mysql的小伙伴大同小异,自己改造一下,后续如果有时间的话我会在gitee以及这里更新mysql的方式
===================================================================================================
2023/07/03 更新:
百忙之中抽空更新了mysql的导入导出方式,代码已提交,放在gitee公开项目上,感兴趣的去拉取下来观看、改造、直接使用都可以,mysql的方式和mongo的方式都大同小异,就不在文章下面更新代码和使用方式了
====================================================================================================
1.引入依赖
<!--easyexcel-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.0.5</version>
</dependency>
<!-- 随机数据生成 -->
<dependency>
<groupId>com.apifan.common</groupId>
<artifactId>common-random</artifactId>
<version>1.0.16</version>
</dependency>
2.测试数据制造
可以看到,在上面的maven中,我提到了一个随机数据生成的依赖,这个依赖是我在网上找到的,简直就是解放了我们的双手(当时我是打算通过手敲方法,调用方法来生成的,下载了demo的可以在工具类中看到,我已经写了不少了手动封装数据的方法);详细的使用方式步骤,更多的数据生成方式可以参考这篇博客:Java-随机数据生成器
调用数据生成方法:
/**
* 随机生成一条用户数据
*
* @return
*/
public static Map createUserdata() {
//生成1个随机中文人名(性别随机)
String userName = PersonInfoSource.getInstance().randomChineseName();
//生成1个随机中国大陆手机号
String phone = PersonInfoSource.getInstance().randomChineseMobile();
//生成1个随机QQ号
String qqNumber = PersonInfoSource.getInstance().randomQQAccount();
//生成1个随机邮箱地址,后缀为163.com,邮箱用户名最大长度为10
String email = InternetSource.getInstance().randomEmail(10, "163.com");
//生成1个2000年的随机日期,日期格式为yyyy-MM-dd
String borthday = DateTimeSource.getInstance().randomDate(2000, "yyyy-MM-dd");
//生成过去36000秒范围内的随机时间
LocalDateTime lastLogintime = DateTimeSource.getInstance().randomPastTime(LocalDateTime.now(), 36000);
//生成1个随机强密码,长度为16,无特殊字符
String pwd = PersonInfoSource.getInstance().randomStrongPassword(16, false);
//生成1个随机中国大陆详细地址
String addr = AreaSource.getInstance().randomAddress();
//生成1个随机英文人名
String nikName = PersonInfoSource.getInstance().randomEnglishName();
//生成1个随机虚拟银联信用卡号码
String bankCard = PersonInfoSource.getInstance().randomCreditCardNo(CreditCardType.UnionPay);
Map data = new HashMap();
data.put("userName", userName);
data.put("phone", phone);
data.put("qqNumber", qqNumber);
data.put("email", email);
data.put("borthday", borthday);
data.put("pwd", pwd);
data.put("lastLogintime", lastLogintime);
data.put("addr", addr);
data.put("nikName", nikName);
data.put("bankCard", bankCard);
return data;
}
swagger接口调用生成100w条数据:
生成结果:
3.创建实体类
@NoArgsConstructor
@AllArgsConstructor
@Data
public class UserData {
@ExcelProperty(value = "姓名") // 导出后的表头名称
private String userName;
@ExcelProperty(value = "银行卡号")
private String bankCard;
@ExcelProperty(value = "电话号码")
private String phone;
@ExcelProperty(value = "家庭住址")
private String addr;
@ExcelProperty(value = "别名")
private String nikName;
@ExcelProperty(value = "QQ号")
private String qqNumber;
@ExcelProperty(value = "邮箱")
private String email;
@ExcelProperty(value = "生日")
private String borthday;
@ExcelProperty(value = "密码")
private String pwd;
@ExcelProperty(value = "最后登录时间")
private String lastLogintime;
@ExcelProperty(value = "创建人")
private String creatUser;
@ExcelProperty(value = "更新者")
private String updateUser;
}
注意:我这里创建的实体类是与我生成数据的字段所对应的,需求不同请酌情修改
4.导出
接口:
@PostMapping("/exportExcel")
@ApiOperation(value = "导出用户数据", notes = "导出用户数据详细信息")
@ApiImplicitParams({
@ApiImplicitParam(name = "num", value = "文件导出页数", required = true, dataType = "Long", defaultValue ="2"),
@ApiImplicitParam(name = "enable", value = "是否开启分页", required = true, dataType = "Boolean", defaultValue ="false")
})
public ResponseEntity exportExcel(@RequestParam(defaultValue = "2") Long num,@RequestParam(defaultValue = "false") Boolean enable) {
excelService.exportExcel(num, enable);
ResponseEntity<String> entity = new ResponseEntity<>("OK", HttpStatus.OK);
return entity;
}
核心代码:
/**
* 导出表格
*
* @param num 导出页数;若开启分页参数为false则此参数无效
* @param enable 是否开启分页
*/
@Override
public void exportExcel(Long num, Boolean enable) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
export(num, enable);
stopWatch.stop();
logger.info("共计耗时: " + stopWatch.getTotalTimeSeconds() + "S");
}
/**
* 导出所有用户数据
*
* @param num
* @param enable
*/
public void export(Long num, Boolean enable) {
String collectionName = "test.user.manyData";
OutputStream outputStream = null;
try {
// 获取当前线程的请求属性
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletResponse response = requestAttributes.getResponse();
outputStream = response.getOutputStream();
// 创建文件导出流
ExcelWriter excelWriter = EasyExcel.write(outputStream).build();
// 是否开启分页
if (enable) {
// 统计数据库中的数据总数
Integer totalCount = Math.toIntExact(mongoTemplate.count(new Query(), collectionName));
// 根据总数,计算每页需要多少条数据
Integer floor = (int) Math.floor(totalCount / num.intValue());
// 每一个Sheet页需要存放的数据条数
Integer sheetDataRows = floor;
// 每次写入的数据量
Integer writeDataRows = floor;
// 计算需要的Sheet数量
Integer sheetNum = num.intValue();
// 计算一般情况下每一个Sheet需要写入的次数(一般情况不包含最后一个sheet,因为最后一个sheet不确定会写入多少条数据)
Integer oneSheetWriteCount = sheetDataRows / writeDataRows;
// 计算最后一个sheet需要写入的次数
Integer lastSheetWriteCount = totalCount % sheetDataRows == 0 ? oneSheetWriteCount : (totalCount % sheetDataRows % writeDataRows == 0 ? (totalCount / sheetDataRows / writeDataRows) : (totalCount / sheetDataRows / writeDataRows + 1));
// 开始分批查询分次写入
for (int i = 0; i < sheetNum; i++) {
// 创建Sheet
WriteSheet sheet = new WriteSheet();
sheet.setSheetName("人员页数" + i);
sheet.setSheetNo(i);
// 循环写入次数: j是当目前不是最后一个Sheet的时候写入次数为正常的每个Sheet写入的次数,如果是最后一个就需要使用计算的次数lastSheetWriteCount
for (int j = 0; j < (i != sheetNum - 1 ? oneSheetWriteCount : lastSheetWriteCount); j++) {
Query query = new Query();
query.skip(i * floor); // 跳过前面的 pageNo-1 页(已查询的记录数)
query.limit(floor); // 返回 pageSize 条记录(每页显示的记录数)
List<UserData> userDataList = mongoTemplate.find(query, UserData.class, collectionName);
WriteSheet writeSheet = EasyExcel.writerSheet(i, "用户信息" + (i + 1)).head(UserData.class)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).build();
// 写数据
excelWriter.write(userDataList, writeSheet);
}
}
} else {
Query query = new Query();
// 全量查询所有数据
List<UserData> userDataList = mongoTemplate.find(query, UserData.class, collectionName);
WriteSheet writerSheet = EasyExcel.writerSheet(0, "用户信息").head(UserData.class).registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).build();
excelWriter.write(userDataList, writerSheet);
}
// 下载EXCEL
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止浏览器端导出excel文件名中文乱码 当然和easyexcel没有关系
String fileName = URLEncoder.encode("用户详细信息表格", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
// 关流
excelWriter.finish();
outputStream.flush();
} catch (IOException e) {
e.printStackTrace();
} catch (BeansException e) {
e.printStackTrace();
} finally {
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
导出测试:
100W数据导出耗时:
文件大小:
swagger填写参数(分10页):
结果查看:
可以看到,1分钟完成了百万级的数据导出,性能就不用多说了吧
5.导入
这里导入功能,我做了俩个接口,一个是获取导入的excel模板的接口,另一个则是导入excel到数据库的接口
接口:
@GetMapping("/exportTemplateExcel")
@ApiOperation(value = "导出用户数据模板", notes = "导出用户数据模板")
public ResponseEntity exportTemplateExcel() {
excelService.exportTemplateExcel();
ResponseEntity<String> entity = new ResponseEntity<>("OK", HttpStatus.OK);
return entity;
}
@PostMapping("/importExcel")
@ApiOperation(value = "导入用户数据", notes = "导入用户数据详细信息")
public ResponseEntity importExcel(@RequestParam("file") MultipartFile multipartFile) {
String importExcelPath = excelService.importExcel(multipartFile);
long startReadTime = System.currentTimeMillis();
System.out.println("------开始读取Excel的Sheet时间(包括导入数据过程):" + startReadTime + "ms------");
// 读取所有Sheet的数据.每次读完一个Sheet就会调用这个方法
EasyExcel.read(importExcelPath, new EasyExceGeneralDatalListener(excelService)).doReadAll();
long endReadTime = System.currentTimeMillis();
System.out.println("------读取Excel的Sheet时间(包括导入数据)共计耗时:" + (endReadTime-startReadTime) + "ms------");
ResponseEntity<String> entity = new ResponseEntity<>("OK", HttpStatus.OK);
return entity;
}
核心代码部分:
导出模板代码:
/**
* 导出表格模板文件
*/
@Override
public void exportTemplateExcel() {
OutputStream outputStream = null;
try {
// 获取当前线程的请求属性
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletResponse response = requestAttributes.getResponse();
// 创建一个输出流
outputStream = response.getOutputStream();
// build输出
ExcelWriter excelWriter = EasyExcel.write(outputStream).build();
// 填写sheet页
WriteSheet writerSheet = EasyExcel.writerSheet(0, "用户信息模板").head(UserData.class).registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).build();
// 写出模板文件
excelWriter.write(new ArrayList<>(), writerSheet);
// 下载EXCEL,设置响应参数
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
// 设置响应参数,这里URLEncoder.encode可以防止浏览器端导出excel文件名中文乱码 当然和easyexcel没有关系
String fileName = URLEncoder.encode("用户详细信息表格", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
// 关流
excelWriter.finish();
outputStream.flush();
} catch (IOException e) {
e.printStackTrace();
} catch (BeansException e) {
e.printStackTrace();
} finally {
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
导入数据代码:
/**
* 导入表格,保存到本地
*
* @param multipartFile 表格参数原件
* @return
*/
@Override
public String importExcel(MultipartFile multipartFile) {
try {
// 获取上传的文件信息
String fileName = multipartFile.getOriginalFilename();
// 保存上传的文件到本地磁盘
Path dest = Paths.get("C:\\Users\\IDLINK\\Desktop\\文档说明\\", fileName);
Files.copy(multipartFile.getInputStream(), dest);
return dest.toString();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 保存数据到mongo
*
* @param dataList
*/
@Override
public void importData(List<Map<Integer, String>> dataList) {
String collectionName = "test.user.manyData";
//结果集中数据为0时,结束方法.进行下一次调用
if (dataList.size() == 0) {
return;
}
try {
long startTime = System.currentTimeMillis();
logger.info(dataList.size() + "条,开始导入到数据库时间:" + startTime + "ms");
List list = new ArrayList();
for (int i = 1; i < dataList.size(); i++) {
Map<Integer, String> item = dataList.get(i);
if (StringUtils.isBlank(item.get(0))||StringUtils.isBlank(item.get(1))||StringUtils.isBlank(item.get(2))
||StringUtils.isBlank(item.get(3))||StringUtils.isBlank(item.get(7))||StringUtils.isBlank(item.get(8))){
throw new RuntimeException(String.format("必填字段不能为空!"));
}
UserData userData = new UserData();
userData.setUserName(item.get(0));
userData.setBankCard(item.get(1));
userData.setPhone(item.get(2));
userData.setAddr(item.get(3));
userData.setNikName(item.get(4));
userData.setQqNumber(item.get(5));
userData.setEmail(item.get(6));
userData.setBorthday(item.get(7));
userData.setPwd(item.get(8));
userData.setLastLogintime(item.get(9));
userData.setCreatUser(item.get(10));
userData.setUpdateUser(item.get(11));
list.add(userData);
}
mongoTemplate.insert(list, collectionName);
long endTime = System.currentTimeMillis();
logger.info(dataList.size() + "条,结束导入到数据库时间:" + endTime + "ms");
logger.info(dataList.size() + "条,导入用时:" + (endTime - startTime) + "ms");
} catch (Exception e) {
e.printStackTrace();
}
}
注意:该easyexcel导入需要配合导入监听类AnalysisEventListener来实现,以下就不过多介绍,我直接把监听类代码贴在下面,可以直接使用,如果不清楚的还是建议去我提供的gitee看看
/**
* 导入事件监听类,只要有文件传入就会调用read方法,就会触发该监听器
*/
public class EasyExceGeneralDatalListener extends AnalysisEventListener<Map<Integer, String>> {
/**
* 处理业务逻辑的Service,也可以是Mapper
*/
@Autowired
private ExcelService empService;
/**
* 用于存储读取的数据
*/
private List<Map<Integer, String>> dataList = new ArrayList<Map<Integer, String>>();
public EasyExceGeneralDatalListener() {
}
public EasyExceGeneralDatalListener(ExcelService empService) {
this.empService = empService;
}
@Override
public void invoke(Map<Integer, String> data, AnalysisContext context) {
//数据add进入集合
dataList.add(data);
//size是否为10000条:这里其实就是分批.当数据等于1w的时候执行一次插入
if (dataList.size() >= 10000) {
//存入数据库:数据小于1w条使用Mybatis的批量插入即可;
saveData();
//清理集合便于GC回收
dataList.clear();
}
}
/**
* 保存数据到DB
*
* @param
* @MethodName: saveData
* @return: void
*/
private void saveData() {
empService.importData(dataList);
dataList.clear();
}
/**
* Excel中所有数据解析完毕会调用此方法
*
* @param: context
* @MethodName: doAfterAllAnalysed
* @return: void
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
saveData();
dataList.clear();
}
}
导入测试:
首先删除集合
在swagger中上传文件:
注意:需要在yml中配置文件上传大小,否则可能会报错(大小超过限制) ,详细配置信息如下:
spring:
servlet:
multipart:
enabled: true
file-size-threshold: 2KB
max-file-size: 200MB
max-request-size: 200MB
导入耗时1分钟:
刷新mongo集合,查看数据量是否正确
百万条数据完美导入;耗时一分钟
6.更多使用方式可以查看官方文档: