EasyExcel實現(xiàn)讀取excel中的日期單元格并自動判定終止讀取
個人在工作中遇到一個需求,讀取第三方的對賬文件,但是對賬文件中的日期的單元格格式不是文本,這樣讀出來就會是一個數(shù)字,表示的是1990年1月1日距今的天數(shù)。而且這個excel中分成了兩個表格,第一個表格是我需要的,第二個表格不需要讀取,同時還有說明行,表頭不是第一行等約束。
最初樸素的想法是讀取出來數(shù)字,然后用Date接受,這樣就知道需要excel對應(yīng)的這串?dāng)?shù)字其實是日期,然后再自定義的listener里邊定義converter做日期轉(zhuǎn)換。大致代碼如下:
public class CustomModelBuildListener<T> extends AnalysisEventListener<Object> {
/**
* head 所在行,用于多head行的情況下, 只想要獲取某行數(shù)據(jù)作為head
*/
private final int headRow;
/**
* data 起始行, 可以跳過某些不想讀取的數(shù)據(jù),比如示例數(shù)據(jù)
*/
private final int dataRow;
/**
* 最終數(shù)據(jù)存放List
*/
@Getter
private final List<T> result = new ArrayList<>();
/**
* 用于將原始結(jié)構(gòu)轉(zhuǎn)為指定類型
*/
private final Class<T> clazz;
/**
* 用來對應(yīng)字段關(guān)系
*/
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 {
//走到這個邏輯的話,說明轉(zhuǎn)換的某些類型沒有做適配,補齊下就好
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);
}
}
}
關(guān)鍵就在于最后的field.getType() == Date.class表示了需要把數(shù)字轉(zhuǎn)為日期,但是如果excel里邊本來就是文本格式,你又用Date接受,也會走到這個邏輯里邊,然后就會報錯,因為轉(zhuǎn)換方法如下:
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被廢棄了,但是指向了一個新的注解,是用來解析時間轉(zhuǎn)換成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> {
/**
* 最終數(shù)據(jù)存放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;
}
// 將讀到的數(shù)據(jù)添加到列表中
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;
}
}
最重要的是判斷終止條件的地方,因為第一個表和第二個表不一樣,所以第一個表的列不在第二個表格中,讀取到第二個表對應(yīng)實體類屬性就是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 數(shù)據(jù)模型類的類型
* @param <T> 數(shù)據(jù)模型的泛型
* @param headRowNumber 表頭行一共有幾行,這里邊的行都是不會讀取的
* @param headRow 表頭行行號
* @param dataRow 數(shù)據(jù)行開始行數(shù)
* @return 數(shù)據(jù)列表
*/
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轉(zhuǎn)換對象
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 數(shù)據(jù)模型類的類型
* @param <T> 數(shù)據(jù)模型的泛型
* @param headRowNumber 表頭是第幾行,默認(rèn)下一行就是數(shù)據(jù),如果表頭和數(shù)據(jù)行中間還有不希望讀取的行,需要在listener自定義處理
* @return 數(shù)據(jù)列表
*/
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());
}
}第一種方法對應(yīng)的是parse方法,第二種方案對應(yīng)的是parseDefault方法。
到此這篇關(guān)于EasyExcel實現(xiàn)讀取excel中的日期單元格并自動判定終止讀取的文章就介紹到這了,更多相關(guān)EasyExcel讀取excel日期單元格內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
教你快速學(xué)會JPA中所有findBy語法規(guī)則
這篇文章主要介紹了教你快速學(xué)會JPA中所有findBy語法規(guī)則,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11
spring+springmvc+mybatis 開發(fā)JAVA單體應(yīng)用
這篇文章主要介紹了spring+springmvc+mybatis 開發(fā)JAVA單體應(yīng)用的相關(guān)知識,本文通過圖文實例代碼的形式給大家介紹的非常詳細(xì) ,需要的朋友可以參考下2018-11-11
基于Beanutils.copyProperties()的用法及重寫提高效率
這篇文章主要介紹了Beanutils.copyProperties( )的用法及重寫提高效率的操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-09-09
springBoot詳細(xì)講解使用mybaties案例
MyBatis本是apache的一個開源項目iBatis,2010年這個項目由apache software foundation遷移到了google code,并且改名為MyBatis。2013年11月遷移到Github。iBATIS一詞來源于“internet”和“abatis”的組合,是一個基于Java的持久層框架2022-05-05
SpringCloud配置客戶端ConfigClient接入服務(wù)端
這篇文章主要為大家介紹了SpringCloud配置客戶端ConfigClient接入服務(wù)端,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-08-08

