EasyExcel实现读取excel中的日期单元格并自动判定终止读取
作者:weixin_45614626
个人在工作中遇到一个需求,读取第三方的对账文件,但是对账文件中的日期的单元格格式不是文本,这样读出来就会是一个数字,表示的是1990年1月1日距今的天数。而且这个excel中分成了两个表格,第一个表格是我需要的,第二个表格不需要读取,同时还有说明行,表头不是第一行等约束。
最初朴素的想法是读取出来数字,然后用Date接受,这样就知道需要excel对应的这串数字其实是日期,然后再自定义的listener里边定义converter做日期转换。大致代码如下:
public class CustomModelBuildListener<T> extends AnalysisEventListener<Object> { /** * head 所在行,用于多head行的情况下, 只想要获取某行数据作为head */ private final int headRow; /** * data 起始行, 可以跳过某些不想读取的数据,比如示例数据 */ private final int dataRow; /** * 最终数据存放List */ @Getter private final List<T> result = new ArrayList<>(); /** * 用于将原始结构转为指定类型 */ private final Class<T> clazz; /** * 用来对应字段关系 */ private Map<Integer, ReadCellData<?>> headMap; public CustomModelBuildListener(int headRow, int dataRow, Class<T> clazz) { this.headRow = headRow; this.dataRow = dataRow; this.clazz = clazz; } @Override public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) { if (getCurrentRow(context) != headRow) { return; } this.headMap = headMap; //log.info("Invoke head of listener: {}", JsonUtils.writeValue(headMap)); } @Override public void invoke(Object data, AnalysisContext context) { int currentRow = getCurrentRow(context); if (currentRow < dataRow) { return; } this.result.add(converter(data)); //log.info("Invoke data of listener, data:{}, row: {}", JsonUtils.writeValue(data), currentRow); } @Override public void doAfterAllAnalysed(AnalysisContext context) { log.info("All analysed done of listener, row: {}", getCurrentRow(context)); } private int getCurrentRow(AnalysisContext context) { return context.readRowHolder().getRowIndex() + 1; } @SuppressWarnings({"unchecked", "rawtypes"}) private T converter(Object data) { try { Map<Integer, ReadCellData> dataMap = (Map<Integer, ReadCellData>) data; Map<String, Object> name2Value = new HashMap<>(); headMap.forEach((key, value) -> { if (!dataMap.containsKey(key)) { return; } ReadCellData<?> cellData = dataMap.get(key); if (cellData.getType() == CellDataTypeEnum.NUMBER) { name2Value.put(value.getStringValue(), cellData.getNumberValue()); } else if (cellData.getType() == CellDataTypeEnum.STRING) { name2Value.put(value.getStringValue(), cellData.getStringValue()); } }); T instance = clazz.newInstance(); for (Field field : clazz.getDeclaredFields()) { if (field.isAnnotationPresent(ExcelProperty.class)) { ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class); String desc = excelProperty.value()[0]; if (name2Value.containsKey(desc)) { field.setAccessible(true); if (field.getType() == Integer.class) { Object value = name2Value.get(desc); if (value instanceof BigDecimal) { field.set(instance, ((BigDecimal) value).intValue()); } else if (value instanceof String) { field.set(instance, Integer.parseInt((String) value)); } else { log.warn("Unknown type for integer convert, type:{}", value.getClass()); throw new RuntimeException("Unknown type for converter, type:" + field.getType()); } } else if (field.getType() == Long.class) { Object value = name2Value.get(desc); if (value instanceof BigDecimal) { field.set(instance, ((BigDecimal)value).longValue()); } else if (value instanceof String) { field.set(instance, Long.parseLong((String)value)); } else { log.warn("Unknown type for long convert, type:{}", value.getClass()); throw new RuntimeException("Unknown type for converter, type:" + field.getType()); } } else if (field.getType() == Double.class) { Object value = name2Value.get(desc); if (value instanceof BigDecimal) { field.set(instance, ((BigDecimal)value).doubleValue()); } else if (value instanceof String) { field.set(instance, Double.parseDouble((String)value)); } else { log.warn("Unknown type for double convert, type:{}", value.getClass()); throw new RuntimeException("Unknown type for converter, type:" + field.getType()); } } else if (field.getType() == String.class) { field.set(instance, name2Value.get(desc)); } else if (field.getType() == Date.class) { field.set(instance, TimeUtils.formatExcelDate((Integer) name2Value.get(desc))); } else { //走到这个逻辑的话,说明转换的某些类型没有做适配,补齐下就好 log.warn("Unknown type for converter, type:{}", field.getType()); throw new RuntimeException("Unknown type for converter, type:" + field.getType()); } } } } return instance; } catch (Exception e) { log.error("Convert data error, data:{}", JsonUtils.writeValue(data), e); throw new RuntimeException(e); } } }
关键就在于最后的field.getType() == Date.class表示了需要把数字转为日期,但是如果excel里边本来就是文本格式,你又用Date接受,也会走到这个逻辑里边,然后就会报错,因为转换方法如下:
public static Date formatExcelDate(int day) { LocalDate localDate = LocalDate.ofYearDay(1990, 1); localDate = localDate.plusDays(day); return Date.from(localDate.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()); }
所以这种写法只适用于excel是非文本格式的日期,并且接受类属性是Date。
于是发现了第二种方法。
点进去@ExcelProperty注解,发现有以下的注释
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface ExcelProperty { /** * The name of the sheet header. * * <p> * write: It automatically merges when you have more than one head * <p> * read: When you have multiple heads, take the last one * * @return The name of the sheet header */ String[] value() default {""}; /** * Index of column * * Read or write it on the index of column, If it's equal to -1, it's sorted by Java class. * * priority: index > order > default sort * * @return Index of column */ int index() default -1; /** * Defines the sort order for an column. * * priority: index > order > default sort * * @return Order of column */ int order() default Integer.MAX_VALUE; /** * Force the current field to use this converter. * * @return Converter */ Class<? extends Converter<?>> converter() default AutoConverter.class; /** * * default @see com.alibaba.excel.util.TypeUtil if default is not meet you can set format * * @return Format string * @deprecated please use {@link com.alibaba.excel.annotation.format.DateTimeFormat} */ @Deprecated String format() default ""; }
最后一个format被废弃了,但是指向了一个新的注解,是用来解析时间转换成Date类型的,
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface DateTimeFormat { /** * * Specific format reference {@link java.text.SimpleDateFormat} * * @return Format pattern */ String value() default ""; /** * True if date uses 1904 windowing, or false if using 1900 date windowing. * * @return True if date uses 1904 windowing, or false if using 1900 date windowing. */ BooleanEnum use1904windowing() default BooleanEnum.DEFAULT; }
于是使用这个注解,指定format,例如-MM-dd。
但是因为需要指定表头行,并且手动中断读取进程,所以还是自定义了一个listener
public class DefaultModelBuildListener<T> extends AnalysisEventListener<T> { /** * 最终数据存放List */ @Getter private List<T> result = new ArrayList<>(); private boolean continueRead = true; @Override public void invoke(T data, AnalysisContext context) { if (Objects.isNull(data) || isFieldNull(data)) { // 停止读取 continueRead = false; return; } // 将读到的数据添加到列表中 result.add(data); } public static boolean isFieldNull(Object object) { if (object == null) { return true; } Field[] fields = object.getClass().getDeclaredFields(); for (Field field : fields) { try { field.setAccessible(true); if (field.get(object) == null) { return true; } else { return false; } } catch (IllegalAccessException e) { throw new RuntimeException(e); } } return false; } @Override public void doAfterAllAnalysed(AnalysisContext context) { log.info("All analysed done of listener"); } @Override public boolean hasNext(AnalysisContext context) { return continueRead; } }
最重要的是判断终止条件的地方,因为第一个表和第二个表不一样,所以第一个表的列不在第二个表格中,读取到第二个表对应实体类属性就是null,所以在invoke方法里加了条件。最初发现null用的是context.interupt。但是发现报异常,点进去发现又是一个废弃的方法,提示用hasNext来终止,进一步深入,发现读取的处理过程是:
for (ReadListener readListener : analysisContext.currentReadHolder().readListenerList()) { try { if (isData) { readListener.invoke(readRowHolder.getCurrentRowAnalysisResult(), analysisContext); } else { readListener.invokeHead(cellDataMap, analysisContext); } } catch (Exception e) { onException(analysisContext, e); break; } if (!readListener.hasNext(analysisContext)) { throw new ExcelAnalysisStopException(); } }
每次读取完一行,会用hasNext判断继不继续,所以重写hasNext方法来无异常终止即可。
测试下来没问题。
最终两种读取方法如下
/** * Excel解析方法, 自定义属性较多,建议优先使用parse(InputStream inputStream, Class<T> clazz) * @param inputStream 输入流 * @param clazz 数据模型类的类型 * @param <T> 数据模型的泛型 * @param headRowNumber 表头行一共有几行,这里边的行都是不会读取的 * @param headRow 表头行行号 * @param dataRow 数据行开始行数 * @return 数据列表 */ public static <T> List<T> parse(InputStream inputStream, Class<T> clazz, int headRowNumber, int headRow, int dataRow) { CustomModelBuildListener<T> dataListener = new CustomModelBuildListener<>(headRow,dataRow, clazz); try (InputStream in = inputStream) { ExcelReaderBuilder excelReaderBuilder = EasyExcel .read(in, clazz, dataListener) .useDefaultListener(false); //很重要,将不使用ModelBuildEventListener转换对象 ExcelReaderSheetBuilder sheetBuilder = excelReaderBuilder.sheet().headRowNumber(headRowNumber); sheetBuilder.doRead(); return dataListener.getResult(); } catch (Exception e) { log.error("Failed to parse excel file", e); throw new ServiceException(SystemCode.PARAM_VALID_ERROR, "Failed to parse Excel file "+ e.getMessage()); } } /** * Excel解析方法, 自定义属性较多,建议优先使用parse(InputStream inputStream, Class<T> clazz) * @param inputStream 输入流 * @param clazz 数据模型类的类型 * @param <T> 数据模型的泛型 * @param headRowNumber 表头是第几行,默认下一行就是数据,如果表头和数据行中间还有不希望读取的行,需要在listener自定义处理 * @return 数据列表 */ public static <T> List<T> parseDefault(InputStream inputStream, Class<T> clazz, int headRowNumber) { DefaultModelBuildListener excelDataListener = new DefaultModelBuildListener(); try (InputStream in = inputStream) { EasyExcel.read(in, clazz, excelDataListener) .sheet() .headRowNumber(headRowNumber) .doRead(); return excelDataListener.getResult(); } catch (Exception e) { log.error("Failed to parse excel file", e); throw new ServiceException(SystemCode.PARAM_VALID_ERROR, "Failed to parse Excel file "+ e.getMessage()); } }
第一种方法对应的是parse方法,第二种方案对应的是parseDefault方法。
到此这篇关于EasyExcel实现读取excel中的日期单元格并自动判定终止读取的文章就介绍到这了,更多相关EasyExcel读取excel日期单元格内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!