文本简要概述了苍穹平台提供的标准导入功能及其二次开发中的定制化需求。文章由日常二次开发需求整理而来,提出了通用的Excel导入工具处理方法,包括自定义导入信息的注解配置、自动解析和自动映射。设计思路上,通过注解配置表头、标题行属性和对象属性与Excel单元格的映射,定义了数据行基础类,并开发了Excel导入的核心处理器实现模板验证、数据解析和读取功能。此外,还展示了相关的注解定义和核心处理类代码实现,以实现Excel导入的灵活性和可扩展性。
一、概述
苍穹平台提供了标准的引入引出功能实现了绝大部分的导入需求,但是在二次开发的过程中发现也存在部分特殊的功能需求需要定制化开发(例如需要动态表单上实现导入功能,同时导入模板在结构上与底层单据非直接对应需要特殊处理时)。本文由日常二次开发需求整理而来,对定制化的Excel导入提供通用的工具处理方法,实现对自定义导入信息的解析和自动映射。
注:
1、本【导入】模块的设计在基础配置和基础数据类型定义上与上一篇分享【自定义Excel导出工具】是通用的,也就是在设计上一开始就是综合考虑导出、导入的需要,实现导出的文件再导入的需要。为了便于读者了解完整的设计、实现过程,本文会完整介绍设计思路及其实现。
2、本文基于平台提供的导入包做二次封装,提供了对自定义导入信息的注解配置、自动解析和自动映射。
二、设计思路:
1、通过注解方式实现表头、标题行属性的配置。
2、通过注解方式实现java对象属性跟Excel单元的自动映射。
3、定义数据行基础类,提供通用的字段定义(例如:行号,导入校验结果、错误原因等)。
4、开发Excel导入的核心处理器ExcelExportHandler,实现Excel模板的验证及通用的Excel数据的解析、读取功能。
5、业务功能模块可根据业务需要直接使用核心处理器或者继承并重写行数据检查方法,实现自定义的数据校验逻辑。
三、实现代码
1、excel导入导出文件标题头部配置注解
/** * excel导入导出文件标题头部配置注解 */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface ExcelHeadProperty { /** * @return 起始行 */ int firstRow() default 0; /** * @return 截止行 */ int lastRow() default 0; /** * @return 起始列 */ int firstCol() default 0; /** * @return 截止列 */ int lastCol() default 0; /** * @return 标题行高 */ short height() default (short) (24 * 20); /** * @return 标题文本 */ String text() default ""; }
2、excel导入导出行标题配置注解
/** * excel导入导出行标题配置注解 */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface ExcelRowProperty { /** * @return 默认标题行高 */ short titleHeight() default (short) (16.5 * 20); /** * @return 默认数据行高 */ short height() default (short) (16.5 * 20); /** * @return 列宽自适应 */ boolean autoFitCol() default true; }
3、excel导入导出属性配置注解
/** * excel导入导出属性配置注解 */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface ExcelProperty { /** * @return 属性列索引位置,从0开始 */ int index() default -1; /** * @return 列标题,用于导出时生成列标题及导入时校验列标题 */ String title() default ""; /** * @return 默认的最新列宽度 */ int minWidth() default 10; }
4、Excel导入导出行数据基础信息类,用于定义导入导出的行数据类型
/** * Excel导入导出行数据基础信息类,用于定义导入导出的行数据类型 */ public class ExcelRowBaseData<T> implements Serializable { private static final long serialVersionUID = 8649711415082247228L; /** * 行号 */ private Integer rowNum; /** * 数据状态 */ private boolean success = true; /** * 失败原因 */ private String failMsg; /** * 行数据 */ private T rowData; public ExcelRowBaseData(T rowData) { this.rowData = rowData; } /** * 重写toString,打印属性信息 * @return */ @Override public String toString() { return "ExcelRowBaseData{" + "rowNum=" + rowNum + ", success=" + success + ", failMsg='" + failMsg + '\'' + ", rowData=" + rowData + '}'; } //此处省略getter setter方法 }
5、Excel导入的核心处理器ExcelExportHandler
本类实现中使用了kd.bos.impt.SheetHandler的基础读取能力,然后通过本类的封装实现了Excel模板的验证及通用的Excel数据的解析、读取功能。
public class ExcelImportHandler<E> extends SheetHandler { /** * 标题行 */ private int titleIndex = 0; /** * 是否读取隐藏行 */ private boolean readHideRow = false; /** * 数据类型 */ private Class<E> rowDataClass; /** * 成功的数据行信息 */ private List<ExcelRowBaseData<E>> successRows = new ArrayList<>(); /** * 错误的数据行信息 */ private List<ExcelRowBaseData<E>> failRows = new ArrayList<>(); /** * 构造函数, */ /** * 构造函数,外部传入数据标题行位置 * * @param titleIndex 标题行 * @param readHideRow 是否读取隐藏行 * @param dataClass 数据类型 */ public ExcelImportHandler(int titleIndex, boolean readHideRow, Class<E> dataClass) { this.titleIndex = titleIndex; this.readHideRow = readHideRow; this.rowDataClass = dataClass; } /** * 构造函数,从数据类型类定义中读取配置的信息,解析标题行 * * @param readHideRow 是否读取隐藏行 * @param dataClass 数据类型 */ public ExcelImportHandler(boolean readHideRow, Class<E> dataClass) { this.readHideRow = readHideRow; this.rowDataClass = dataClass; ExcelHeadProperty headProperty = dataClass.getAnnotation(ExcelHeadProperty.class); if (headProperty != null) { //文件标题下一行是数据标题行 this.titleIndex = headProperty.lastRow() + 1; } } /** * 重写行处理方法,进行数据的自动映射 * * @param parsedRow */ @Override public void handleRow(ParsedRow parsedRow) { //行号小于标题行,不进行处理 if (parsedRow.getRowNum() < titleIndex) { return; } //标题行处理,验证表头行数据是否被修改过 if (parsedRow.getRowNum() == titleIndex) { this.validateTitleRow(parsedRow); return; } //不读取隐藏行数据 if (!readHideRow && parsedRow.isHideRow()) { return; } //解析数据 ExcelRowBaseData<E> rowData = this.resolveRowData(parsedRow); //校验数据 boolean isSuccess = this.checkRowData(rowData); //根据校验结果添加到对应的列表 if (isSuccess) { this.successRows.add(rowData); } else { this.failRows.add(rowData); } } /** * 解析表头行数据,验证是否被修改过 * * @param parsedRow 行数据 */ private void validateTitleRow(ParsedRow parsedRow) { //获取声明的属性及其注解的列索引 Field[] fields = rowDataClass.getDeclaredFields(); for (Field field : fields) { ExcelProperty property = field.getAnnotation(ExcelProperty.class); if (property != null && property.index() >= 0 && !StringUtils.equals(property.title(), parsedRow.getData().get(property.index()))) { throw new KDBizException("Excel导入模板有误,请下载最新模板"); } } } /** * 解析行数据 * * @param parsedRow * @return 自定义行数据 */ private ExcelRowBaseData<E> resolveRowData(ParsedRow parsedRow) { try { //自定义行数据对象 ExcelRowBaseData<E> result = new ExcelRowBaseData<>(rowDataClass.newInstance()); //设置行号 result.setRowNum(parsedRow.getRowNum()+1); //获取声明的属性及其注解的列索引,并将读取的信息映射到自定义类对象的属性中 Field[] fields = result.getRowData().getClass().getDeclaredFields(); for (Field field : fields) { ExcelProperty property = field.getAnnotation(ExcelProperty.class); if (property != null && property.index() >= 0) { field.setAccessible(true); field.set(result.getRowData(), parsedRow.getData().get(property.index())); } } //返回解析后的对象 return result; } catch (ReflectiveOperationException e) { e.printStackTrace(); } return null; } /** * 自定义校验行数据 * * @param rowData * @return */ public boolean checkRowData(ExcelRowBaseData<E> rowData) { return true; } /** * 成功的数据行信息 * @return */ public List<ExcelRowBaseData<E>> getSuccessRows() { return successRows; } /** * 失败的数据行信息 * @return */ public List<ExcelRowBaseData<E>> getFailRows() { return failRows; } }
四、运用示例
本示例以单据表单上传文件的形式,通过解析文件的形式来演示文件解析。
1、实体定义
@ExcelHeadProperty(text = "学生名单", lastCol = 2) @ExcelRowProperty public class StudentDTO { @ExcelProperty(index = 0, title = "学号") private String no; @ExcelProperty(index = 1, title = "姓名") private String name; @ExcelProperty(index = 2, title = "年龄") private String age; /** * 默认的构造函数 */ public StudentDTO() { } /** * 构造函数 */ public StudentDTO(String no, String name, String age) { this.no = no; this.name = name; this.age = age; } /** * 重写toString,打印属性信息 * @return */ @Override public String toString() { return "StudentDTO{" + "no='" + no + '\'' + ", name='" + name + '\'' + ", age='" + age + '\'' + '}'; } }
2、导入关键代码
@Override public void upload(UploadEvent evt) { try { //如果不是指定导入的按钮,不执行操作 if (!evt.getCallbackKey().equals(BTN_IMPORT)) { return; } //获取文件链接 Object[] urls = evt.getUrls(); //文件解析处理 FileService fileService = FileServiceFactory.getAttachmentFileService(); for (Object url : urls) { //定义导入的处理器 ExcelImportHandler handler = new ExcelImportHandler(false, StudentDTO.class); //读取文件流 OutputStream os = new ByteArrayOutputStream(); fileService.download((String) url, os, "httpclient"); ByteArrayInputStream is = new ByteArrayInputStream(((ByteArrayOutputStream) os).toByteArray()); //创建读取器,并执行读取 ExcelReader reader = new ExcelReader(); reader.read(is, handler); //查看成功/失败的数据信息 System.out.println(String.format("成功的数据量:%s",handler.getSuccessRows())); System.out.println(String.format("失败的数据量:%s",handler.getFailRows())); //todo 根据具体业务需要,执行业务处理 } } catch (Exception e) { //todo 根据业务需要处理异常 }finally { //todo 关闭流 } }
3、导入文件示例及输出示例
成功的数据量:[ExcelRowBaseData{rowNum=3, success=true, failMsg='null', rowData=StudentDTO{no='1001', name='张三', age='18'}}, ExcelRowBaseData{rowNum=4, success=true, failMsg='null', rowData=StudentDTO{no='1002', name='李四', age='19'}}, ExcelRowBaseData{rowNum=5, success=true, failMsg='null', rowData=StudentDTO{no='1003', name='王五', age='20'}}] 失败的数据量:[]