- 项目地址:github.com/alibaba/eas…
- 官方文档:www.yuque.com/easyexcel/d…
- EasyExcel是一款阿里开源的Excel导入导出工具,具有处理快速、占用内存小、使用方便的特点,在Github上已有
22k+
Star,可见其非常流行 - EasyExcel读取75M(46W行25列)的Excel,仅需使用64M内存,耗时20s,极速模式还可以更快!
常用注解:
- @ExcelProperty 指定当前字段对应excel中的那一列。可以根据名字或者Index去匹配。当然也可以不写,默认第一个字段就是index=0,以此类推。千万注意,要么全部不写,要么全部用index,要么全部用名字去匹配。千万别三个混着用,除非你非常了解源代码中三个混着用怎么去排序的。
- @ExcelIgnore 默认所有字段都会和excel去匹配,加了这个注解会忽略该字段
- @DateTimeFormat 日期转换,用String去接收excel日期格式的数据会调用这个注解。里面的value参照java.text.SimpleDateFormat
- @NumberFormat 数字转换,用String去接收excel数字格式的数据会调用这个注解。里面的value参照java.text.DecimalFormat
- @ExcelIgnoreUnannotated默认不加ExcelProperty 的注解的都会参与读写,加了不会参与
-
依赖:等下demo有用到lombok和fastjson,所以这里我加上了
<dependency> <groupId>com.alibabagroupId> <artifactId>easyexcelartifactId> <version>3.0.5version> dependency> <dependency> <groupId>com.alibabagroupId> <artifactId>fastjsonartifactId> <version>1.2.80version> dependency> <dependency> <groupId>org.projectlombokgroupId> <artifactId>lombokartifactId> <version>1.18.2version> <scope>compilescope> dependency>
-
实体类,转换类
public class SysExcelDemo implements Serializable { private static final long serialVersionUID = 1L; @ExcelIgnore @TableId(value = "id", type = IdType.AUTO) private Integer id; @ExcelProperty(value = "用户名") private String userName; @ExcelProperty(value = "手机号") private String mobile; @ExcelProperty(value = "性别", converter = SexExcelConverter.class) private Integer sex; } public class SexExcelConverter implements Converter<Integer> { @Override public Integer convertToJavaData(ReadConverterContext<?> context) throws Exception { //获取excel性别的值 String value = context.getReadCellData().getStringValue(); if ("男".equals(value)) { return 1; } else { return 0; } } }
-
监听器类,这个是重点
//有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去 @Slf4j public class DemoDataListener implements ReadListener<SysExcelDemo> { //每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收 private static final int BATCH_COUNT = 100; //缓存的数据 private List<SysExcelDemo> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT); //Service层 private ISysExcelDemoService demoService; public DemoDataListener(ISysExcelDemoService demoService){ this.demoService = demoService; } private static final String[] headers = {"用户名", "手机号", "性别"}; //验证模板 @Override public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) { if (context.readRowHolder().getRowIndex() == 0) { for (int i = 0; i < headers.length; i++) { if(!headers[i].equals(headMap.get(i).getStringValue())){ throw new ExcelAnalysisException("请导入正确的模板"); } } } } //这个每一条数据解析都会来调用, //这里也可以使用不创建对象的读invoke(Map
data, AnalysisContext context),当我们这里验证数据之后再封装进我们的实体 @Override public void invoke(SysExcelDemo data, AnalysisContext analysisContext) { log.info("解析到一条数据:{}", JSON.toJSONString(data)); cachedDataList.add(data); // 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM if (cachedDataList.size() >= BATCH_COUNT) { saveData(); // 存储完成清理 list cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT); } } //在转换异常 获取其他异常下会调用本接口。抛出异常则停止读取。如果这里不抛出异常则 继续读取下一行。 @Override public void onException(Exception exception, AnalysisContext context) { //数据转换异常 if (exception instanceof ExcelDataConvertException) { ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException)exception; log.error("第{}行,第{}列解析异常,数据为:{}", excelDataConvertException.getRowIndex(), excelDataConvertException.getColumnIndex(), excelDataConvertException.getCellData()); } //解析异常 if(exception instanceof ExcelAnalysisException){ ExcelAnalysisException excelAnalysisException = (ExcelAnalysisException) exception; String message = excelAnalysisException.getMessage(); log.error(message); //抛出异常,停止解析 throw new ExcelAnalysisStopException(message); } } //存储数据库 private void saveData() { log.info("{}条数据,开始存储数据库!", cachedDataList.size()); demoService.saveBatch(cachedDataList); log.info("存储数据库成功!"); } //所有数据解析完成了 都会来调用 @Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { // 这里也要保存数据,确保最后遗留的数据也存储到数据库 saveData(); log.info("所有数据解析完成!"); } } -
controller
@Controller @RequestMapping("/sysExcelDemo") public class SysExcelDemoController { @Autowired private ISysExcelDemoService demoService; @PostMapping("/excelRead") @ResponseBody public AjaxResult read(@RequestParam(name = "file", required = true) MultipartFile file) throws IOException { EasyExcel.read(file.getInputStream(), SysExcelDemo.class, new DemoDataListener(demoService)).sheet().doRead(); return AjaxResult.suc(); } }
测试,导入我们准备的xlsx
-
可以看到,数据库存储了excel中的数据
-
当我们导入的标题头跟监听器我们定义的headers不一致时(模板不一致),就不会继续解析下一行了,并会打印出错误日志
下面直接使用官方的demo,就是创建了多个监听器读取不同的sheet
/**
* 读多个或者全部sheet,这里注意一个sheet不能读取多次,多次读取需要重新读取文件
*
* 1. 创建excel对应的实体对象 参照{@link DemoData}
*
* 2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照{@link DemoDataListener}
*
* 3. 直接读即可
*/
@Test
public void repeatedRead() {
String fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
// 读取全部sheet,这里其实就是同一个监听器去接收全部Sheet
// 这里需要注意 DemoDataListener的doAfterAllAnalysed 会在每个sheet读取完毕后调用一次。然后所有sheet都会往同一个DemoDataListener里面写
EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).doReadAll();
// 读取部分sheet
fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
ExcelReader excelReader = null;
try {
excelReader = EasyExcel.read(fileName).build();
// 这里为了简单 所以注册了 同样的head 和Listener 自己使用功能必须不同的Listener
ReadSheet readSheet1 =
EasyExcel.readSheet(0).head(DemoData.class).registerReadListener(new DemoDataListener()).build();
ReadSheet readSheet2 =
EasyExcel.readSheet(1).head(DemoData.class).registerReadListener(new DemoDataListener()).build();
// 这里注意 一定要把sheet1 sheet2 一起传进去,不然有个问题就是03版的excel 会读取多次,浪费性能
excelReader.read(readSheet1, readSheet2);
} finally {
if (excelReader != null) {
// 这里千万别忘记关闭,读的时候会创建临时文件,到时磁盘会崩的
excelReader.finish();
}
}
}
写Excel
我们现在刚才导入数据库的数据导出到excel中,还原一下
-
还是刚才的实体
- 这里主要新加了一个注解@ColumnWidth(50),指定手机号列的宽度,还可以指定行高等
- 转换类重写了convertToExcelData方法,数据库到excel的转换
@TableName("sys_excel_demo") @ApiModel(value = "SysExcelDemo对象", description = "") public class SysExcelDemo implements Serializable { private static final long serialVersionUID = 1L; @ExcelIgnore @TableId(value = "id", type = IdType.AUTO) private Integer id; @ExcelProperty(value = "用户名") private String userName; @ColumnWidth(50) @ExcelProperty(value = "手机号") private String mobile; @ExcelProperty(value = "性别", converter = SexExcelConverter.class) private Integer sex; } public class SexExcelConverter implements Converter<Integer> { @Override public Integer convertToJavaData(ReadConverterContext<?> context) throws Exception { //获取excel性别的值 String value = context.getReadCellData().getStringValue(); if ("男".equals(value)) { return 1; } else { return 0; } } @Override public WriteCellData<?> convertToExcelData(WriteConverterContext<Integer> context) throws Exception { Integer sex = context.getValue(); if(sex == 1){ return new WriteCellData<>("男"); }else { return new WriteCellData<>("女"); } } }
-
controller
@Controller @RequestMapping("/sysExcelDemo") public class SysExcelDemoController { @Autowired private ISysExcelDemoService demoService; @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(), SysExcelDemo.class).sheet("Sheet1").doWrite(demoService.list()); } }
准备完成,开始测试,直接调用download接口,可以看到,跟刚才导入的数据一致
总结官方除了读写excel,还有填充excel,可以去官方文档看看,但是很多细节,还有出现的bug,看官方文档是远远不够的,需要自己去实践一下。其实还有挺多坑的
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)