筆記:使用EasyExcel導入csv文件出現編碼問題,導致導入數據全為null的解決方法
通常情況下,我們使用excel導入,但是部分情況下或者領導要求,我們需要使用csv導入文件,但是csv文件模板下載之后會變成系統當前編碼,而不是固定為UTF-8,如果用excel的方式打開,寫入數據之后,會提示你另存為xlsx,這也說明了csv和xlsx本質是完全不同的文件。
如果這種時候你使用EasyExcel默認的編碼讀取csv文件,你會發現讀取的數據全部都是null,因為通常情況下,windows系統如果不特意設置編碼格式,默認都為GBK,所以會導致導入數據無法讀取的情況,當然excel并不會出現這種問題,所以如果不是特殊需求,導入還是推薦使用Excel文件。
這里分享一個導入方法,通過人為干預導入數據的方式,去判斷是否讀取成功,這里只用中國常用的UTF8和GBK作為讀取的編碼,通常情況下是夠用的,如果有需要,可以在charsets集合自行增加其他編碼。
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.hexin.fms.common.excel.*;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.springframework.web.multipart.MultipartFile;import java.io.InputStream;
import java.lang.reflect.Field;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;public class ExcelUtil {/*** 多編碼轉換導入數據* 目前使用常用編碼utf8和gbk解析文件* 如使用csv導入出現編碼問題的時候* @param file 導入文件* @param clazz 類對象* @param excelTypeEnum 導入文件類型* @return 文件數據*/public static <T> ExcelResult<T> importExcelByAnyCharset(MultipartFile file, Class<T> clazz, ExcelTypeEnum excelTypeEnum) throws Exception {DefaultExcelListener<T> listener = new DefaultExcelListener<>(false);return importExcelByAnyCharset(file, clazz, excelTypeEnum, listener);}/*** 多編碼轉換導入數據* 目前使用常用編碼utf8和gbk解析文件* 如使用csv導入出現編碼問題的時候* @param file 導入文件* @param clazz 類對象* @param excelTypeEnum 導入文件類型* @param listener 監聽器* @return 文件數據*/public static <T> ExcelResult<T> importExcelByAnyCharset(MultipartFile file, Class<T> clazz, ExcelTypeEnum excelTypeEnum,ExcelListener<T> listener) throws Exception {//需要校驗的全部編碼List<Charset> charsets = new ArrayList<>();charsets.add(StandardCharsets.UTF_8);charsets.add(Charset.forName("GBK"));//開始解析文件ExcelResult<T> result = new DefaultExcelResult<>();for(Charset charset : charsets){result = importExcelByCharset(file.getInputStream(), clazz, excelTypeEnum, listener, charset);//校驗實體數據是否全部為nullif(result != null && result.getList() != null){List<T> resultList = result.getList();int size = resultList.size();int nullNums = 0;for(T t : resultList){if(t == null){nullNums++;continue;}Class<?> tClass = t.getClass();Field[] fields = tClass.getDeclaredFields();int len = fields.length;int fieldNullNums = 0;for(Field field : fields){field.setAccessible(true);if(field.get(t) == null){fieldNullNums++;}}if(len == fieldNullNums){nullNums++;}else{break;}}if(nullNums != size){break;}}}return result;}/*** 獲取導入數據* 只走utf8,不校驗獲取數據是否為null* 如果不能保證導入文件為utf8,請使用importExcelByAnyCharset方法* @param is 導入文件的文件流* @param clazz 類對象* @param excelTypeEnum 導入文件類型* @return 文件數據*/public static <T> ExcelResult<T> importExcelByUTF8(InputStream is, Class<T> clazz, ExcelTypeEnum excelTypeEnum) {DefaultExcelListener<T> listener = new DefaultExcelListener<>(false);return importExcelByCharset(is, clazz, excelTypeEnum, listener, StandardCharsets.UTF_8);}/*** 獲取導入數據* @param is 導入文件的文件流* @param clazz 類對象* @param excelTypeEnum 導入文件類型* @param listener 監聽器* @param charset 用于導入文件數據轉換的編碼* @return 文件數據*/public static <T> ExcelResult<T> importExcelByCharset(InputStream is, Class<T> clazz, ExcelTypeEnum excelTypeEnum,ExcelListener<T> listener, Charset charset) {EasyExcel.read(is, clazz, listener).excelType(excelTypeEnum).charset(charset).sheet().doRead();return listener.getExcelResult();}
}
下面是一個默認的監聽器,其中isValidate可以去掉,個人不建議在監聽器里面作校驗,這樣導入數據之間的唯一性或者其他一些關系可能無法校驗或者校驗起來很麻煩。
個人建議,還是先獲取到表格數據,然后再進行校驗。
import cn.hutool.core.util.StrUtil;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.exception.ExcelAnalysisException;
import com.alibaba.excel.exception.ExcelDataConvertException;
import com.hexin.fms.common.utils.JsonUtils;
import com.hexin.fms.common.utils.StreamUtils;
import com.hexin.fms.common.utils.ValidatorUtils;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.Map;
import java.util.Set;@Slf4j
@NoArgsConstructor
public class DefaultExcelListener<T> extends AnalysisEventListener<T> implements ExcelListener<T> {/*** 是否Validator檢驗,默認為是*/private Boolean isValidate = Boolean.TRUE;/*** excel 表頭數據*/private Map<Integer, String> headMap;/*** 導入回執*/private ExcelResult<T> excelResult;public DefaultExcelListener(boolean isValidate) {this.excelResult = new DefaultExcelResult<>();this.isValidate = isValidate;}/*** 處理異常** @param exception ExcelDataConvertException* @param context Excel 上下文*/@Overridepublic void onException(Exception exception, AnalysisContext context) throws Exception {String errMsg = null;if (exception instanceof ExcelDataConvertException) {// 如果是某一個單元格的轉換異常 能獲取到具體行號ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException) exception;Integer rowIndex = excelDataConvertException.getRowIndex();Integer columnIndex = excelDataConvertException.getColumnIndex();errMsg = StrUtil.format("第{}行-第{}列-表頭{}: 解析異常<br/>",rowIndex + 1, columnIndex + 1, headMap.get(columnIndex));if (log.isDebugEnabled()) {log.error(errMsg);}}if (exception instanceof ConstraintViolationException) {ConstraintViolationException constraintViolationException = (ConstraintViolationException) exception;Set<ConstraintViolation<?>> constraintViolations = constraintViolationException.getConstraintViolations();String constraintViolationsMsg = StreamUtils.join(constraintViolations, ConstraintViolation::getMessage, ", ");errMsg = StrUtil.format("第{}行數據校驗異常: {}", context.readRowHolder().getRowIndex() + 1, constraintViolationsMsg);if (log.isDebugEnabled()) {log.error(errMsg);}}excelResult.getErrorList().add(errMsg);throw new ExcelAnalysisException(errMsg);}@Overridepublic void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {this.headMap = headMap;log.debug("解析到一條表頭數據: {}", JsonUtils.toJsonString(headMap));}@Overridepublic void invoke(T data, AnalysisContext context) {if (isValidate) {ValidatorUtils.validate(data);}excelResult.getList().add(data);}@Overridepublic void doAfterAllAnalysed(AnalysisContext context) {log.debug("所有數據解析完成!");}@Overridepublic ExcelResult<T> getExcelResult() {return excelResult;}}
用法如下:
@PostMapping(value = "/importExcel", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)public R<Void> importExcel(@RequestPart("file") MultipartFile file) throws Exception {ExcelResult<ImportVo> result = ExcelUtil.importExcelByAnyCharset(file, ImportVo.class, ExcelTypeEnum.CSV);List<ImportVo> excelList = result.getList();return importService.importExcel(excelList);}
importService.importExcel(excelList)就是你校驗和保存更新刪除數據的方法。