# SpringBoot集成Excel **Repository Path**: jianml/excel ## Basic Information - **Project Name**: SpringBoot集成Excel - **Description**: SpringBoot集成Excel - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2020-01-06 - **Last Updated**: 2021-03-06 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # SpringBoot集成Excel ## 简介 EasyExcel是阿里开源的解析excel的工具,可以把它看作对poi的优化版 Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。easyexcel重写了poi对07版Excel的解析,能够原本一个3M的excel用POI sax依然需要100M左右内存降低到KB级别,并且再大的excel不会出现内存溢出,03版依赖POI的sax模式。在上层做了模型转换的封装,让使用者更加简单方便 > 参考:https://github.com/alibaba/easyexcel ## 引入依赖 ```xml com.alibaba easyexcel 2.1.6 ``` ## 创建实体类 注意:实体类使用Lombok生成构造方法时,添加了有参构造注解之后一定要添加无参构建注解,否则会抛出异常 ```java @Data @NoArgsConstructor @AllArgsConstructor public class User { @ExcelProperty("姓名") private String username; @ExcelProperty("密码") private String password; @ExcelProperty(value = "性别", converter = SexConverter.class) private Integer sex; @ExcelProperty("出生日期") private Date birthday; @ExcelIgnore private String address; } ``` ### 注解 - `ExcelProperty` 指定当前字段对应excel中的那一列。可以根据名字或者Index去匹配。当然也可以不写,默认第一个字段就是index=0,以此类推。千万注意,要么全部不写,要么全部用index,要么全部用名字去匹配。千万别三个混着用,除非你非常了解源代码中三个混着用怎么去排序的。 - `ExcelIgnore` 默认所有字段都会和excel去匹配,加了这个注解会忽略该字段 - `DateTimeFormat` 日期转换,用`String`去接收excel日期格式的数据会调用这个注解。里面的`value`参照`java.text.SimpleDateFormat` - `NumberFormat` 数字转换,用`String`去接收excel数字格式的数据会调用这个注解。里面的`value`参照`java.text.DecimalFormat` PS:对于` @ExcelProperty(value = "性别", converter = SexConverter.class)`有疑惑的话后面有一节会专门讲转换器的使用,请仔细往下继续看 ## Excel导入 ### 编写excel的监听器 有个很重要的点` ImportExcelListener `不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去 ```java @Slf4j public class ImportExcelListener extends AnalysisEventListener { private UserService userService; private List list = new ArrayList<>(); /** * 每隔5条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收 */ private static final int BATCH_COUNT = 5; public ImportExcelListener(UserService userService){ this.userService = userService; } /** * 这个每一条数据解析都会来调用 */ @Override public void invoke(User data, AnalysisContext analysisContext) { log.info("解析到一条数据:{}", JSON.toJSONString(data)); list.add(data); if(list.size() >= BATCH_COUNT){ saveData(); // 清理掉已存储的数据 list.clear(); } } /** * 所有数据解析完成了 都会来调用 */ @Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { if(list.size() > 0) saveData(); log.info("所有数据解析完成"); } private void saveData(){ log.info("{}条数据,开始存储数据库", list.size()); userService.saveData(list); log.info("存储数据库成功"); } } ``` ### 导入excel代码 ```java /** * 导入Excel数据(默认异步读取excel) */ @PostMapping("/import") public String importExcels(@RequestParam("file") MultipartFile file) throws IOException { EasyExcel.read(file.getInputStream(), User.class, new ImportExcelListener(userService)).sheet().doRead(); return "导入成功"; } ``` ## Excel导出 ### 获取数据 由于我们没有连接数据库,所以我们直接用代码直接生成数据 ```java @Override public List getData() { // 模拟从数据库中取数据 DateFormat format = new SimpleDateFormat("yyyy-MM-dd"); List list = new ArrayList<>(); try{ list.add(new User("wujian", "123456", 0, format.parse("1994-10-13"), "湖南省")); list.add(new User("李四", "111111", 1, format.parse("1992-11-16"), "江苏省")); list.add(new User("王五", "222222", 2, format.parse("1991-04-03"), "北京市")); }catch (Exception e){ e.printStackTrace(); } return list; } ``` ### 导出excel代码 ```java /** * 导出 Excel */ @PostMapping("/export") public void export(HttpServletResponse response) throws IOException{ response.setContentType("application/vnd.ms-excel"); response.setCharacterEncoding("utf-8"); String fileName = URLEncoder.encode("测试", "UTF-8"); response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx"); EasyExcel.write(response.getOutputStream(), User.class).sheet("模板").doWrite(userService.getData()); } ``` ## 自定义转换器 这是最基本的数据读写,我们的业务数据通常不可能这么简单,有时甚至需要将其转换为程序可读的数据 比如 Excel 中新增「性别」列,其性别为男/女/未知,我们需要将 Excel 中的性别信息转换成程序信息: 「0: 男;1:女;2:未知」 ### 首先在 User 实体中添加成员变量 `sex` ```java @ExcelProperty(value = "性别", converter = SexConverter.class) private Integer sex; ``` EasyExcel 支持我们自定义 converter,将 excel 的内容转换为我们程序需要的信息,这里新建 `SexConverter`,用来转换性别信息 ```java public class SexConverter implements Converter { private static final String MALE = "男"; private static final String FEMALE = "女"; private static final String UNKNOW = "未知"; @Override public Class supportJavaTypeKey() { return Integer.class; } @Override public CellDataTypeEnum supportExcelTypeKey() { return CellDataTypeEnum.STRING; } /** * 这里读的时候会调用 */ @Override public Integer convertToJavaData(CellData cellData, ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception { String value = cellData.getStringValue(); if(MALE.equals(value)) return 0; else if(FEMALE.equals(value)) return 1; else return 2; } /** * 这里是写的时候会调用 */ @Override public CellData convertToExcelData(Integer integer, ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception { String value = UNKNOW; if(0 == integer) value = MALE; else if(1 == integer) value = FEMALE; return new CellData(value); } } ``` ## 封装优化 ### 封装 解析监听类 (也可以不封装,直接继承AnalysisEventListener 方法) ```java @Slf4j public abstract class ExcelListener extends AnalysisEventListener { public static final int BATCH_COUNT = 1000; private List datas = new ArrayList(); public void setDatas(List datas) { this.datas = datas; } public List getDatas() { return datas; } /** * 每解析一行会执行一次invoke */ @Override public void invoke(Object o, AnalysisContext analysisContext) { log.info("解析到一条数据:{}", JSON.toJSONString(o)); addListBefore(o); datas.add(o); doListAfter(o); } /** * 所有数据解析完成了 都会来调用 */ @Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { doAllListAfter(); } /** * 在添加到List集合之前 */ public abstract void addListBefore(Object object); /** * 在添加到List集合之后 */ public abstract void doListAfter(Object object); /** * 所有数据都解析完后 */ public abstract void doAllListAfter(); } ``` ### 继承封装好的监听类 ```java @Slf4j public class UserExcelListener extends ExcelListener { private UserService userService; public UserExcelListener(UserService userService){ this.userService = userService; } @Override public void addListBefore(Object object) { // doSomething... } @Override public void doListAfter(Object object) { // 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM if(this.getDatas().size() >= BATCH_COUNT){ saveDatas(); // 清理掉已存储的数据 this.getDatas().clear(); } } @Override public void doAllListAfter() { // 这里也要保存数据,确保最后遗留的数据也存储到数据库 if(this.getDatas().size() > 0) saveDatas(); log.info("所有数据解析完成"); } private void saveDatas(){ log.info("{}条数据,开始存储数据库", this.getDatas().size()); userService.saveDatas(this.getDatas()); log.info("存储数据库成功"); } } ``` ### 测试导入excel ```java /** * 导入Excel数据(优化后) */ @PostMapping("/commonImport") public String commonImportExcels(@RequestParam("file") MultipartFile file) throws IOException { EasyExcel.read(file.getInputStream(), User.class, new UserExcelListener(userService)).sheet().doRead(); return "导入成功"; } ``` > 源码地址:https://gitee.com/jianml/excel