# easyexcel-ie **Repository Path**: Hawkc/easyexcel-ie ## Basic Information - **Project Name**: easyexcel-ie - **Description**: 基于easyexcel定制化的数据导入/导出 - **Primary Language**: Java - **License**: GPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2023-06-10 - **Last Updated**: 2023-06-10 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # easyexcel-ie 基于 easyexcel 的定制化excel导入/导出的实现。 # 项目依赖 ```xml cn.com.zhaoweiping easyexcel-ie 1.0.0-SNAPSHOT ``` # 功能实现 #### Excel导出格式 ​ 支持Excel的导入和导出;支持多种样式的导出,包括冻结窗格、单元格下拉选择、单元格注释、一个Sheet中多表头、多Sheet的导出;同时也支持导出的这些内容的导入。 #### 转换函数 ​ 在`com.gitee.alpha.ie.handler`中主要定义了Excel导出所需用的类,在`com.gitee.alpha.ie.convert`中主要定义了导入/导出中所使用的转换函数,包括头转换函数和值转换函数。 ​ 支持实体类型的header/value转换函数(`EntityHeaderConverter/EntityDataConverter`)和多表头的转换函数(`ManyHeaderConverter/ManyTableDataConverter`)。 ​ 对于常量转换函数请继承`com.gitee.alpha.ie.convert.AbstractConstantConverter`,常量主要使用在导出的Excel中进行加拉选择使用,再导入的时候同样也能进行转换成相应的类型。 #### 转换函数注册 ​ 使用`com.gitee.alpha.ie.convert.ConverterRegister`进行全局注册;支持初始化报扫描方式进行注册。 #### 支持Web上传解析 ​ 支持将easyexcel-ie导入web项目中使用,上传Excel数据到后端进行解析后导入数据,同时能返回每条数据的读取状态或每个Sheet数据的读取状态给前端,能实时的获取到数据导入执行情况。 # 使用方法 ## Excel导出 ### 转换器注册 ```java @PostConstruct public void converterRegister() { ConverterRegister.list("cn.com.zhaoweiping", Converter.class); } ``` 使用`ConverterRegister`中的方法进行转换器的注册,可进行全局注册。 转换器可以根据类型来获取得到,使用`ConverterRegister.getConverter(Class clazz)` ### 一个Web下载数据导出的例子 抽象Controller中的导出通用实现(简单数据类型) ```java protected void doExport( HttpServletRequest request, HttpServletResponse response, Class entityClass) throws Exception { response.setContentType("application/vnd.ms-excel"); response.setCharacterEncoding("utf-8"); response.setHeader("Content-disposition", "attachment;filename=data.xlsx"); IService iService = getIService(); Assert.isFalse( iService == null, String.format( "需要导出文件的Controller没有实现 %s 的 %s 方法", this.getClass().getName(), "getIService()")); // 若选中数据,则导出选中的数据; 若没有选中数据,则导出所有的数据 // 构建请求参数 Map context = ?; // 可从request中获取 // 设置默认分页参数信息 if (context.containsKey(QueryOps.PAGE_INDEX) == false) { context.put(QueryOps.PAGE_INDEX, 1); } if (context.containsKey(QueryOps.PAGE_SIZE) == false) { context.put(QueryOps.PAGE_SIZE, 20); } // QueryBuilder来自Alpha-Framework QueryBuilder queryBuilder = new QueryBuilder(parameter.getEntity(), context); doExport(response, queryBuilder); } protected void doExport(HttpServletResponse response, QueryBuilder queryBuilder) throws Exception { } ``` 复杂数据类型的导出需要单独定制(一个复杂数据类型导出的例子) ```java @Override protected void doExport(HttpServletResponse response, QueryBuilder queryBuilder) throws Exception { ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).build(); ExcelHelper excelHelper = new ExcelHelper(); // A和B的关系为 public class A { private List bs; } // 设置表头 List aExcelFields = excelHelper.buildExcelFields(A.class); List bExcelFields = excelHelper.buildExcelFields(B.class); List> aHead = excelHelper.buildHead(A.class); List> bHead = excelHelper.buildHead(B.class); // 设置下拉选择内容 Map aDropDownValues = excelHelper.buildDropDownValues(aExcelFields); Map bDropDownValues = excelHelper.buildDropDownValues(bExcelFields); // 设置注释 List vos = excelHelper.buildCommentVos(aExcelFields, 0); if (CollectionUtil.isNotEmpty(vos)) { vos.addAll(excelHelper.buildCommentVos(bExcelFields, 2)); } else { vos = excelHelper.buildCommentVos(bExcelFields, 2); } // 转换函数 List aConverters = excelHelper.findConverters(aExcelFields); List bConverters = excelHelper.findConverters(bExcelFields); // body Page page = aService.findAllPage(queryBuilder); int index = 0; if (page != null && CollectionUtil.isNotEmpty(page.getContent())) { index = write( excelWriter, page.getContent(), index, vos, aHead, bHead, aDropDownValues, bDropDownValues, aConverters, bConverters); } while (page != null && CollectionUtil.isNotEmpty(page.getContent()) && !page.isLast()) { queryBuilder.pageIncrease(); page = getIService().findAllPage(queryBuilder); if (page != null && CollectionUtil.isNotEmpty(page.getContent())) { index = write( excelWriter, page.getContent(), index, vos, aHead, bHead, aDropDownValues, bDropDownValues, aConverters, bConverters); } } excelWriter.finish(); } private int write( ExcelWriter excelWriter, List as, int index, List vos, List> aHead, List> bHead, Map aDropDownValues, Map bDropDownValues, List aConverters, List bConverters) { if (CollectionUtil.isNotEmpty(as)) { as = aService.findAll( as.stream().map(a -> a.getAId()).collect(Collectors.toList())); for (int i = 0; i < as.size(); i++) { A a = as.get(i); index += i; // 准备Sheet WriteSheet sheet = EasyExcel.writerSheet(index, a.getAName()) .registerWriteHandler(new FreezePaneHandler(0, 3, 0, 3)) .registerWriteHandler(new SetColumnCommentHandler(vos)) .build(); // 写入A数据到ATable ExcelWriterTableBuilder aTableBuild = EasyExcel.writerTable(0) .head(aHead) // 设置自适应列 .registerWriteHandler(new SetColumnWidthHandler()) // 设置下拉选择 .registerWriteHandler(new SpinnerWriteHandler(aDropDownValues, 1, 1)) .needHead(true); aTableBuild = buildExcelWriterTableBuilderConverter(aTableBuild, aConverters); WriteTable aTable = aTableBuild.build(); excelWriter.write(Arrays.asList(a), sheet, aTable); // 写入BS到BTable ExcelWriterTableBuilder bTableBuild = EasyExcel.writerTable(1) .head(bHead) .registerWriteHandler(new SetColumnWidthHandler()) .registerWriteHandler(new SpinnerWriteHandler(bDropDownValues, 3)) .needHead(true); bTableBuild = buildExcelWriterTableBuilderConverter(bTableBuild, bConverters); WriteTable bTable = bTableBuild.build(); excelWriter.write(a.getBs(), sheet, bTable); } } return index; } ``` ## Excel导入 使用导出的Excel文件修改之后或者按照导出的Excel文件做为模板进行导入即可。 ### 一个Web上传数据导入的例子 文件上传实现 ```java @PostMapping(value = "/data/import") public AppResult upload(HttpServletRequest request, @RequestParam("file") MultipartFile file) { AppResult result = AppResult.ofSuccess(); try { // 前端使用返回的这个UUID来查询获取到数据的解析状态 String uuid = this.doUpload(request, file); result.setData(uuid); } catch (Exception e) { result = AppResult.ofFail(500, e.getMessage()); } return result; } protected String doUpload(HttpServletRequest request, MultipartFile multipartFile) throws Exception { Assert.isFalse(multipartFile.isEmpty(), "文件上传失败"); String uuid = IdUtil.fastSimpleUUID(); String originName = multipartFile.getOriginalFilename(); // 临时文件目录/使用application.yml中进行配置传入方式 File dir = new File(uploadDir); if (dir.exists() == false) { dir.mkdirs(); } // 防止文件名重复/在完成数据导入之后会删除掉临时Excel文件/需要注意的是可能文件名过长的问题 File file = new File(uploadDir + uuid + originName); multipartFile.transferTo(file); int sheetCounts = new ExcelHelper().getSheetCounts(file); SimpleReadContext readContext = new SimpleReadContext(sheetCounts); readContext.setFile(file); ReadContextHolder.set(uuid, readContext); new Thread(() -> this.doUploading(file, readContext)).start(); return uuid; } ``` 上传解析状态的查看,前端使用setTimeout/setInterval来定时请求获取数据在前端解析response展示即可 ```java @RestController @Api(value = "数据导入控制台监视信息") @RequestMapping("/console") public class DataImportConsoleMonitorInfo { @GetMapping("/info/{uuid}") public AppResult> info(@PathVariable(value = "uuid") String uuid) { AppResult> result = AppResult.ofSuccess(); ReadContext readContext = ReadContextHolder.get(uuid); if (readContext == null) { result.setAdditional("Excel解析、数据导入完成!"); } else { List list = readContext.read(); result.setData(list); Status readExcelStatus = readContext.readExcelStatus(); if (readExcelStatus == Status.FINISH) { // Excel读取完成 result.setAdditional("Excel解析、数据导入完成!"); ReadContextHolder.remove(uuid); } } return result; } } ``` 业务数据导入的实现示例(简单数据类型/JavaEntity) ```java public void doUploading(File file, SimpleReadContext readContext) { try { // 获取当前类的泛型 Class clazz = ?; // 需要实现获取抽象父类字类Controller中的数据类型泛型(即实体类型) // 默认使用实体头转换 HeaderConverter headerConverter = new EntityHeaderConverter(clazz); // 默认使用实体值转换 CellDataConverter cellDataConverter = new EntityDataConverter(clazz, readContext); // 读取所有的Sheet EasyExcel.read( file, new ExcelReadAnalysisListener(readContext, headerConverter, cellDataConverter, this)) .doReadAll(); } catch (Exception e) { log.error("上传失败", e); } } // 默认实现在抽象Controller中用于数据导入的通用处理方法、此处能处理的导入只有单类型数据(实体)模式,若需要导入多类型(多实体)等复杂类型的数据 // 需要重写importHandler方法 @Override protected void importHandler( Exception e, List list, ReadContext readContext, AnalysisContext context) { IService service = this.getIService(); Assert.isFalse( service == null, String.format( "继承%s实现数据的导入/导出,%s不能为空", this.getClass().getName(), IService.class.getName())); SimpleReadContext simpleReadContext = (SimpleReadContext) readContext; // 默认当没有异常时处理正常数据 if (e == null && CollectionUtil.isNotEmpty(list)) { for (RowWrapper wrapper : list) { try { Object data = wrapper.getRowData(); service.save((IEntity) data); simpleReadContext.setStatus(wrapper.getRowIndex(), true, "成功"); } catch (Exception ne) { if (ne instanceof DuplicateKeyException) { simpleReadContext.setStatus(wrapper.getRowIndex(), true, String.format("失败:存在重复记录")); } else { simpleReadContext.setStatus( wrapper.getRowIndex(), true, String.format("失败:%s", ne.getMessage())); } log.error("业务数据处理发生异常, 错误信息:", ne); } } } if (readContext.readStatus() == Status.FINISH) { // 此处调用父类的clear函数清空缓存数据 // 需要注意的是在字类Controller中重写importHandler方法是不能在调用importHandler了,需要使用super.clear(); super.importHandler(e, list, readContext, context); log.debug( String.format( "读取处理文件:%s,记录数:%d,失败条数:%d,错误信息:%s", simpleReadContext.getFileName(), simpleReadContext.getTotal(), simpleReadContext.getFailTotal(), simpleReadContext.getPartFails(5))); } } ``` 业务数据导入的实现示例(复杂数据类型/多JavaEntity/Map对象/Json对象) ```java // 此处示例中使用了两个项目中的实体类型A和B // Excel表格一个Sheet样式为: // 0行 A表头 // 1行 A数据 // 2行 B表头 // 3行 B数据 // 4行 B数据 // n行 ... // m行 B数据 @Override public void doUploading(File file, SimpleReadContext readContext) { try { CellDataConverter cellDataConverter = new ManyTableDataConverter( Sets.newHashSet(0, 2), readContext, new ConstantCellValueConverter()); ManyHeaderHelper manyHeaderHelper = ManyHeaderHelper.getInstance(); HeaderDescribe aHeaderDescribe = manyHeaderHelper.buildHeaderDescribe(A.class); HeaderDescribe bHeaderDescribe = manyHeaderHelper.buildHeaderDescribe(B.class); LinkedHashMap describes = Maps.newLinkedHashMap(); describes.put(0, aHeaderDescribe); describes.put(2, bHeaderDescribe); HeaderConverter headerConverter = new ManyHeaderConverter(describes); EasyExcel.read( file, new ExcelReadAnalysisListener(readContext, headerConverter, cellDataConverter, this)) .doReadAll(); } catch (Exception e) { log.error("上传失败", e); } } @Override protected void importHandler( Exception e, List list, ReadContext readContext, AnalysisContext context) { SimpleReadContext simpleReadContext = (SimpleReadContext) readContext; if (Status.FINISH == simpleReadContext.readStatus()) { try { // 获取一个Sheet中的数据 List as = get(A.class); List rowWrappers = get(B.class); // 数据转换 Map aMap = (Map) as.get(0).getRowData(); A a = BeanUtil.mapToBean(aMap, A.class, CopyOptions.create()); List bs = Lists.newArrayList(); for (RowWrapper wrapper : rowWrappers) { Map bMap = (Map) wrapper.getRowData(); B b = BeanUtil.mapToBean(bMap, B.class, CopyOptions.create()); bs.add(b); } a.setBs(bs); String aId = a.getAId(); A oA = aService.findById(aId); if (oA == null) { aService.save(a, false); } else { aService.update(a, false); } // 第一个参数为行号,这是看数据导入单位是按照行还是按照sheet为单位导入,若是按照sheet为单位则默认给-1 simpleReadContext.setStatus(-1, true, String.format("数据:%s 导入成功", a.getAName())); } catch (Exception ne) { if (ne instanceof DuplicateKeyException) { simpleReadContext.setStatus( -1, false, String.format("数据:%s 导入失败:存在重复记录", context.readSheetHolder().getSheetName())); } else { simpleReadContext.setStatus( // context.readSheetHolder().getSheetNo(), // 获取行号 -1, false, String.format( "数据:%s 导入失败,错误信息:%s", context.readSheetHolder().getSheetName(), ne.getMessage())); } log.error("业务数据处理发生异常, 错误信息:", ne); } finally { super.clear(); log.debug( String.format( "读取处理文件:%s,记录数:%d,失败条数:%d,错误信息:%s", simpleReadContext.getFileName(), simpleReadContext.getTotal(), simpleReadContext.getFailTotal(), simpleReadContext.getPartFails(5))); } } } ``` # ExcelHelper ​ 若使用Spring Data Jpa,则可用下面的方法重写ExcelHelper中的buildDropDownValues方法来构建Excel单元格下拉值列表 ```java // // ~ Spring Data Jpa // import com.gitee.alpha.Constant; // import cn.com.zhaoweiping.framework.repository.support.ColumnType; // import cn.hutool.core.util.ArrayUtil; // import cn.hutool.core.util.EnumUtil; // import com.google.common.collect.Maps; // import java.util.LinkedHashMap; // import javax.persistence.EnumType; // import javax.persistence.Enumerated; // @Override // protected Map buildDropDownValues(int colIndex, Field field) { // Map dropDownValues = Maps.newHashMap(); // Class clazz = field.getType(); // if (clazz.isEnum()) { // LinkedHashMap enums = EnumUtil.getEnumMap(clazz); // List list = Lists.newArrayList(); // Enumerated enumerated = field.getAnnotation(Enumerated.class); // if (CollectionUtil.isNotEmpty(enums)) { // enums.forEach( // (k, v) -> { // String val = String.valueOf(k); // if (v instanceof Constant && enumerated != null) { // Constant cv = (Constant) v; // if (enumerated.value() == EnumType.STRING) { // val = cv.getLabel(); // } else if (enumerated.value() == EnumType.ORDINAL) { // val = val; // } // } // else if () // 其他枚举类型 // // list.add(val); // }); // // String[] values = ArrayUtil.toArray(list, String.class); // if (ArrayUtil.isNotEmpty(values)) { // dropDownValues.put(colIndex, values); // } // } // } else if (ColumnType.BOOLEAN.getBasicType().equals(clazz.getName()) // || ColumnType.BOOLEAN.getJavaClass().getName().equals(clazz.getName())) { // dropDownValues.put(colIndex, new String[] {"TRUE", "FALSE"}); // } // // return dropDownValues; // } ```