EasyExcel實現(xiàn)讀取excel中的日期單元格并自動判定終止讀取
個人在工作中遇到一個需求,讀取第三方的對賬文件,但是對賬文件中的日期的單元格格式不是文本,這樣讀出來就會是一個數字,表示的是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。
于是發(fā)現(xiàn)了第二種方法。
點進去@ExcelProperty注解,發(fā)現(xiàn)有以下的注釋
@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方法里加了條件。最初發(fā)現(xiàn)null用的是context.interupt。但是發(fā)現(xiàn)報異常,點進去發(fā)現(xiàn)又是一個廢棄的方法,提示用hasNext來終止,進一步深入,發(fā)現(xiàn)讀取的處理過程是:
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判斷繼不繼續(xù),所以重寫hasNext方法來無異常終止即可。
測試下來沒問題。
最終兩種讀取方法如下
/** * Excel解析方法, 自定義屬性較多,建議優(yōu)先使用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解析方法, 自定義屬性較多,建議優(yōu)先使用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實現(xiàn)讀取excel中的日期單元格并自動判定終止讀取的文章就介紹到這了,更多相關EasyExcel讀取excel日期單元格內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
spring+springmvc+mybatis 開發(fā)JAVA單體應用
這篇文章主要介紹了spring+springmvc+mybatis 開發(fā)JAVA單體應用的相關知識,本文通過圖文實例代碼的形式給大家介紹的非常詳細 ,需要的朋友可以參考下2018-11-11基于Beanutils.copyProperties()的用法及重寫提高效率
這篇文章主要介紹了Beanutils.copyProperties( )的用法及重寫提高效率的操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-09-09SpringCloud配置客戶端ConfigClient接入服務端
這篇文章主要為大家介紹了SpringCloud配置客戶端ConfigClient接入服務端,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-08-08