| | |
| | | package com.doumee.core.annotation.excel; |
| | | |
| | | import com.doumee.core.constants.ResponseStatus; |
| | | import com.doumee.core.exception.BusinessException; |
| | | import lombok.AllArgsConstructor; |
| | | import lombok.Data; |
| | | import org.apache.commons.lang3.StringUtils; |
| | | import org.apache.poi.hssf.usermodel.HSSFWorkbook; |
| | | import org.apache.poi.openxml4j.exceptions.InvalidFormatException; |
| | | import org.apache.poi.ss.usermodel.*; |
| | | import org.apache.poi.xssf.usermodel.XSSFWorkbook; |
| | | import org.springframework.core.annotation.AnnotationConfigurationException; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | import org.springframework.web.multipart.MultipartFile; |
| | | |
| | | import java.io.File; |
| | | import java.io.FileInputStream; |
| | | import java.io.IOException; |
| | | import java.io.InputStream; |
| | | import java.lang.reflect.Field; |
| | | import java.lang.reflect.Method; |
| | | import java.lang.reflect.Type; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.TreeMap; |
| | | import java.util.*; |
| | | |
| | | /** |
| | | * Excel导入实现 |
| | | * @author dm |
| | | * @since 2025/03/31 16:44 |
| | | * 导入Excel文件(支持“XLS”和“XLSX”格式) |
| | | */ |
| | | public class ExcelImporter<T> { |
| | | public class ExcelImporter { |
| | | |
| | | private Class<T> modelClass; |
| | | |
| | | private ExcelImporter () {} |
| | | private static Logger log = LoggerFactory.getLogger(ExcelImporter.class); |
| | | |
| | | /** |
| | | * 构造ExcelImporter对象 |
| | | * |
| | | * @param modelClass 实体Class对象 |
| | | * @return ExcelImporter实例 |
| | | * 工作薄对象 |
| | | */ |
| | | public static <T> ExcelImporter<T> build(Class<T> modelClass) { |
| | | ExcelImporter<T> excelImporter = new ExcelImporter<>(); |
| | | excelImporter.modelClass = modelClass; |
| | | return excelImporter; |
| | | private Workbook wb; |
| | | |
| | | /** |
| | | * 工作表对象 |
| | | */ |
| | | private Sheet sheet; |
| | | |
| | | /** |
| | | * 标题行号 |
| | | */ |
| | | private int headerNum; |
| | | /** |
| | | * 标题行号 |
| | | */ |
| | | private CellType changeType; |
| | | |
| | | /** |
| | | * 构造函数 |
| | | * @param--path 导入文件,读取第一个工作表 |
| | | * @param headerNum 标题行号,数据行号=标题行号+1 |
| | | * @throws InvalidFormatException |
| | | * @throws IOException |
| | | */ |
| | | public ExcelImporter(String fileName, int headerNum) |
| | | throws InvalidFormatException, IOException { |
| | | this(new File(fileName), headerNum); |
| | | } |
| | | |
| | | /** |
| | | * 导入数据 |
| | | * |
| | | * @param is 输入流 |
| | | * @param callback 回调 |
| | | * @param sync 是否同步已存在数据 |
| | | * @return 导入成功数 |
| | | * 构造函数 |
| | | * @param--path 导入文件对象,读取第一个工作表 |
| | | * @param headerNum 标题行号,数据行号=标题行号+1 (eg: 1) |
| | | * @throws InvalidFormatException |
| | | * @throws IOException |
| | | */ |
| | | public int importData (InputStream is, ExcelImportCallback<T> callback, boolean sync) { |
| | | return this.importData(is, 0, callback, sync); |
| | | public ExcelImporter(File file, int headerNum) |
| | | throws InvalidFormatException, IOException { |
| | | this(file, headerNum, 0); |
| | | } |
| | | |
| | | /** |
| | | * 导入数据 |
| | | * |
| | | * @param is 输入流 |
| | | * @param sheetIndex sheet坐标 |
| | | * @param callback 回调 |
| | | * @param sync 是否同步已存在数据 |
| | | * @return 导入成功数 |
| | | * 构造函数 |
| | | * @param--path 导入文件 |
| | | * @param headerNum 标题行号,数据行号=标题行号+1 |
| | | * @param sheetIndex 工作表编号 |
| | | * @throws InvalidFormatException |
| | | * @throws IOException |
| | | */ |
| | | public int importData (InputStream is, int sheetIndex, ExcelImportCallback<T> callback, boolean sync) { |
| | | try { |
| | | Workbook workbook = new XSSFWorkbook(is); |
| | | Sheet sheet = workbook.getSheetAt(sheetIndex); |
| | | // 获取列信息 |
| | | List<ColumnInfo> columns = this.getColumns(); |
| | | List<T> data = new ArrayList<>(); |
| | | // 循环获取excel行记录 |
| | | for (int i = 1; i <= sheet.getLastRowNum(); i++) { |
| | | // 构造数据实例对象 |
| | | T instance = modelClass.newInstance(); |
| | | Row row = sheet.getRow(i); |
| | | // 循环获取单元格信息 |
| | | for (int columnIndex = 0; columnIndex < columns.size(); columnIndex++) { |
| | | Cell cell = row.getCell(columnIndex); |
| | | if (cell == null) { |
| | | continue; |
| | | } |
| | | if (StringUtils.isBlank(cell.toString())) { |
| | | continue; |
| | | } |
| | | ColumnInfo columnInfo = columns.get(columnIndex); |
| | | if (columnInfo == null) { |
| | | break; |
| | | } |
| | | // 写入对象属性 |
| | | columnInfo.getField().setAccessible(Boolean.TRUE); |
| | | columnInfo.getField().set(instance, this.getCellValue(cell, columnInfo)); |
| | | columnInfo.getField().setAccessible(Boolean.FALSE); |
| | | } |
| | | // 如果是空行则结束行读取 |
| | | if (this.isEmptyRow(instance)) { |
| | | break; |
| | | } |
| | | data.add(instance); |
| | | } |
| | | // 执行回调函数 |
| | | return callback.callback(data, sync); |
| | | } catch (Exception e) { |
| | | throw new BusinessException(ResponseStatus.IMPORT_EXCEL_ERROR, e); |
| | | } finally { |
| | | if (is != null) { |
| | | try { |
| | | is.close(); |
| | | } catch (IOException e) { |
| | | e.printStackTrace(); |
| | | } |
| | | } |
| | | public ExcelImporter(String fileName, int headerNum, int sheetIndex) |
| | | throws InvalidFormatException, IOException { |
| | | this(new File(fileName), headerNum, sheetIndex); |
| | | } |
| | | |
| | | /** |
| | | * 构造函数 |
| | | * @param--path 导入文件对象 |
| | | * @param headerNum 标题行号,数据行号=标题行号+1 |
| | | * @param sheetIndex 工作表编号 |
| | | * @throws InvalidFormatException |
| | | * @throws IOException |
| | | */ |
| | | public ExcelImporter(File file, int headerNum, int sheetIndex) |
| | | throws InvalidFormatException, IOException { |
| | | this(file.getName(), new FileInputStream(file), headerNum, sheetIndex); |
| | | } |
| | | |
| | | /** |
| | | * 构造函数 (重要) |
| | | * @param--file 导入文件对象 |
| | | * @param headerNum 标题行号,数据行号=标题行号+1 |
| | | * @param sheetIndex 工作表编号 |
| | | * @throws InvalidFormatException |
| | | * @throws IOException |
| | | */ |
| | | public ExcelImporter(MultipartFile multipartFile, int headerNum, int sheetIndex) |
| | | throws InvalidFormatException, IOException { |
| | | this(multipartFile.getOriginalFilename(), multipartFile.getInputStream(), headerNum, sheetIndex); |
| | | } |
| | | /** |
| | | * 构造函数 (重要) |
| | | * @param--file 导入文件对象 |
| | | * @param headerNum 标题行号,数据行号=标题行号+1 |
| | | * @param sheetIndex 工作表编号 |
| | | * @throws InvalidFormatException |
| | | * @throws IOException |
| | | */ |
| | | public ExcelImporter(MultipartFile multipartFile, int headerNum, int sheetIndex, CellType cellType) |
| | | throws InvalidFormatException, IOException { |
| | | this(multipartFile.getOriginalFilename(), multipartFile.getInputStream(), headerNum, sheetIndex,cellType); |
| | | } |
| | | |
| | | /** |
| | | * 构造函数 () |
| | | * @param--path 导入文件对象 |
| | | * @param headerNum 标题行号,数据行号=标题行号+1 |
| | | * @param sheetIndex 工作表编号 (eg: 0) |
| | | * @throws InvalidFormatException |
| | | * @throws IOException |
| | | */ |
| | | public ExcelImporter(String fileName, InputStream in, int headerNum, int sheetIndex) |
| | | throws InvalidFormatException, IOException { |
| | | if (StringUtils.isBlank(fileName)){ |
| | | throw new RuntimeException("导入文档为空!"); |
| | | }else if(fileName.toLowerCase().endsWith("xls")){ |
| | | this.wb = new HSSFWorkbook(in); |
| | | }else if(fileName.toLowerCase().endsWith("xlsx")){ |
| | | this.wb = new XSSFWorkbook(in); |
| | | }else{ |
| | | throw new RuntimeException("文档格式不正确!"); |
| | | } |
| | | if (this.wb.getNumberOfSheets()<sheetIndex){ |
| | | throw new RuntimeException("文档中没有工作表!"); |
| | | } |
| | | this.sheet = this.wb.getSheetAt(sheetIndex); |
| | | this.headerNum = headerNum; |
| | | log.debug("Initialize success."); |
| | | } |
| | | public ExcelImporter(String fileName, InputStream in, int headerNum, int sheetIndex,CellType cellType) |
| | | throws InvalidFormatException, IOException { |
| | | if (StringUtils.isBlank(fileName)){ |
| | | throw new RuntimeException("导入文档为空!"); |
| | | }else if(fileName.toLowerCase().endsWith("xls")){ |
| | | this.wb = new HSSFWorkbook(in); |
| | | }else if(fileName.toLowerCase().endsWith("xlsx")){ |
| | | this.wb = new XSSFWorkbook(in); |
| | | }else{ |
| | | throw new RuntimeException("文档格式不正确!"); |
| | | } |
| | | if (this.wb.getNumberOfSheets()<sheetIndex){ |
| | | throw new RuntimeException("文档中没有工作表!"); |
| | | } |
| | | this.sheet = this.wb.getSheetAt(sheetIndex); |
| | | this.headerNum = headerNum; |
| | | this.changeType = cellType; |
| | | log.debug("Initialize success."); |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * 获取行对象 |
| | | * @param rownum |
| | | * @return |
| | | */ |
| | | public Row getRow(int rownum){ |
| | | return this.sheet.getRow(rownum); |
| | | } |
| | | |
| | | /** |
| | | * 获取Cell值 |
| | | * |
| | | * @param cell 单元格对象 |
| | | * @param columnInfo 列配置 |
| | | * 获取数据行号 |
| | | * @return |
| | | */ |
| | | public int getDataRowNum(){ |
| | | return headerNum+1; |
| | | } |
| | | |
| | | /** |
| | | * 获取最后一个数据行号 |
| | | * @return |
| | | */ |
| | | public int getLastDataRowNum(){ |
| | | return this.sheet.getLastRowNum()+headerNum; |
| | | } |
| | | |
| | | /** |
| | | * 获取最后一个列号 |
| | | * @return |
| | | */ |
| | | public int getLastCellNum(){ |
| | | return this.getRow(headerNum).getLastCellNum(); |
| | | } |
| | | |
| | | /** |
| | | * 获取单元格值 |
| | | * @param row 获取的行 |
| | | * @param column 获取单元格列号 |
| | | * @return 单元格值 |
| | | */ |
| | | private Object getCellValue (Cell cell, ColumnInfo columnInfo) { |
| | | CellType cellType = cell.getCellType(); |
| | | Type fieldType = columnInfo.getField().getGenericType(); |
| | | Object value = null; |
| | | if (fieldType.getTypeName().equals("java.util.Date")) { |
| | | value = cell.getDateCellValue(); |
| | | } else if (cellType.equals(CellType.NUMERIC)) { |
| | | value = cell.getNumericCellValue(); |
| | | } else if (cellType.equals(CellType.STRING)) { |
| | | value = cell.getStringCellValue(); |
| | | } else if (cellType.equals(CellType.BOOLEAN)) { |
| | | value = cell.getBooleanCellValue(); |
| | | } else if (cellType.equals(CellType.BLANK)) { |
| | | value = ""; |
| | | } else if (cellType.equals(CellType.ERROR)) { |
| | | value = cell.getErrorCellValue(); |
| | | } |
| | | // 调用转换器 |
| | | if (!columnInfo.columnConfig.converter().equals(ExcelDataConverterAdapter.class)) { |
| | | try { |
| | | Object instance = columnInfo.columnConfig.converter().newInstance(); |
| | | Method convertMethod = columnInfo.columnConfig.converter().getMethod("convert", Object[].class); |
| | | List<Object> args = new ArrayList<>(); |
| | | args.add(value); |
| | | for (String arg : columnInfo.columnConfig.args()) { |
| | | args.add(arg); |
| | | public Object getCellValue(Row row, int column){ |
| | | Object val = ""; |
| | | try{ |
| | | Cell cell = row.getCell(column); |
| | | if (cell != null){ |
| | | if (cell.getCellType() == CellType.NUMERIC){ |
| | | cell.setCellType(CellType.STRING); // 确保单元格类型为字符串 |
| | | val = cell.getStringCellValue(); |
| | | }else if (cell.getCellType() == CellType.STRING){ |
| | | val = cell.getStringCellValue(); |
| | | }else if (cell.getCellType() == CellType.FORMULA){ |
| | | val = cell.getCellFormula(); |
| | | }else if (cell.getCellType() == CellType.BOOLEAN){ |
| | | val = cell.getBooleanCellValue(); |
| | | }else if (cell.getCellType() == CellType.ERROR){ |
| | | val = cell.getErrorCellValue(); |
| | | } |
| | | value = convertMethod.invoke(instance, new Object[]{args.toArray()}); |
| | | } catch (Exception e) { |
| | | throw new IllegalStateException("EVA: can not convert data by " + columnInfo.columnConfig.converter(), e); |
| | | } |
| | | }catch (Exception e) { |
| | | return val; |
| | | } |
| | | return value; |
| | | return val; |
| | | } |
| | | |
| | | /** |
| | | * 获取列集合 |
| | | * 获取导入数据列表 |
| | | * @param cls 导入对象类型 |
| | | * @param groups 导入分组 可为空 |
| | | */ |
| | | private List<ColumnInfo> getColumns () { |
| | | Map<Integer, ColumnInfo> sortedFields = new TreeMap<>(); |
| | | Field[] fields = modelClass.getDeclaredFields(); |
| | | int index = 0; |
| | | for (Field field : fields) { |
| | | ExcelImportColumn excelColumn = field.getAnnotation(ExcelImportColumn.class); |
| | | if (excelColumn == null) { |
| | | public <E> List<E> getDataList(Class<E> cls, int... groups) throws InstantiationException, IllegalAccessException{ |
| | | List<Object[]> annotationList = new ArrayList<>(); |
| | | // Get annotation field |
| | | Field[] fs = cls.getDeclaredFields(); |
| | | for (Field f : fs){ |
| | | ExcelColumn ef = f.getAnnotation(ExcelColumn.class); |
| | | if (ef != null && (ef.type()==0 || ef.type()==2)){ |
| | | if (groups!=null && groups.length>0){ |
| | | boolean inGroup = false; |
| | | for (int g : groups){ |
| | | if (inGroup){ |
| | | break; |
| | | } |
| | | for (int efg : ef.groups()){ |
| | | if (g == efg){ |
| | | inGroup = true; |
| | | annotationList.add(new Object[]{ef, f}); |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | }else{ |
| | | annotationList.add(new Object[]{ef, f}); |
| | | } |
| | | } |
| | | } |
| | | // Get annotation method |
| | | Method[] ms = cls.getDeclaredMethods(); |
| | | for (Method m : ms){ |
| | | ExcelColumn ef = m.getAnnotation(ExcelColumn.class); |
| | | if (ef != null && (ef.type()==0 || ef.type()==2)){ |
| | | if (groups!=null && groups.length>0){ |
| | | boolean inGroup = false; |
| | | for (int g : groups){ |
| | | if (inGroup){ |
| | | break; |
| | | } |
| | | for (int efg : ef.groups()){ |
| | | if (g == efg){ |
| | | inGroup = true; |
| | | annotationList.add(new Object[]{ef, m}); |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | }else{ |
| | | annotationList.add(new Object[]{ef, m}); |
| | | } |
| | | } |
| | | } |
| | | // Field sorting |
| | | Collections.sort(annotationList, (o1, o2) -> { |
| | | int index1 =((ExcelColumn)o1[0]).index(), index2=((ExcelColumn)o2[0]).index(); |
| | | if(index1 == -1){ |
| | | index1 = annotationList.size()+99999; |
| | | } |
| | | if(index2 == -1){ |
| | | index2 = annotationList.size()+99999; |
| | | } |
| | | return new Integer(index1).compareTo(new Integer(index2)); |
| | | }); |
| | | //log.debug("Import column count:"+annotationList.size()); |
| | | // Get excel data |
| | | List<E> dataList = new ArrayList<>(); |
| | | System.out.println("起始数据行:"+getDataRowNum()); |
| | | System.out.println("结束数据行:"+getLastDataRowNum()); |
| | | for (int i = this.getDataRowNum(); i <= this.getLastDataRowNum(); i++) { |
| | | E e = (E)cls.newInstance(); |
| | | int column = 0; |
| | | Row row = this.getRow(i); |
| | | if (Objects.isNull(row)){ |
| | | continue; |
| | | } |
| | | if (sortedFields.get(excelColumn.index()) != null) { |
| | | throw new AnnotationConfigurationException("EVA: Excel column contains the same index."); |
| | | StringBuilder sb = new StringBuilder(); |
| | | for (Object[] os : annotationList){ |
| | | Object val = this.getCellValue(row, column++); |
| | | if (val != null){ |
| | | ExcelColumn ef = (ExcelColumn)os[0]; |
| | | // Get param type and type cast |
| | | Class<?> valType = Class.class; |
| | | if (os[1] instanceof Field){ |
| | | valType = ((Field)os[1]).getType(); |
| | | }else if(os[1] instanceof Method){ |
| | | Method method = ((Method)os[1]); |
| | | if ("get".equals(method.getName().substring(0, 3))){ |
| | | valType = method.getReturnType(); |
| | | }else if("set".equals(method.getName().substring(0, 3))){ |
| | | valType = ((Method)os[1]).getParameterTypes()[0]; |
| | | } |
| | | } |
| | | //log.debug("Import value type: ["+i+","+column+"] " + valType); |
| | | try { |
| | | if (valType == String.class){ |
| | | String s = String.valueOf(val.toString()); |
| | | if(StringUtils.endsWith(s, ".0")){ |
| | | val = StringUtils.substringBefore(s, ".0"); |
| | | }else{ |
| | | val = String.valueOf(val.toString()).trim(); |
| | | } |
| | | } |
| | | else if (valType == Integer.class){ |
| | | val = Double.valueOf(val.toString()).intValue(); |
| | | }else if (valType == Long.class){ |
| | | val = Double.valueOf(val.toString()).longValue(); |
| | | }else if (valType == Double.class){ |
| | | val = Double.valueOf(val.toString()); |
| | | }else if (valType == Float.class){ |
| | | val = Float.valueOf(val.toString()); |
| | | }else if (valType == Date.class){ |
| | | val = DateUtil.getJavaDate((Double)val); |
| | | }else{ |
| | | if (ef.fieldType() != Class.class){ |
| | | val = ef.fieldType().getMethod("getValue", String.class).invoke(null, val.toString()); |
| | | }else{ |
| | | val = Class.forName(this.getClass().getName().replaceAll(this.getClass().getSimpleName(), |
| | | "fieldtype."+valType.getSimpleName()+"Type")).getMethod("getValue", String.class).invoke(null, val.toString()); |
| | | } |
| | | } |
| | | } catch (Exception ex) { |
| | | log.info("Get cell value ["+i+","+column+"] error: " + ex.toString()); |
| | | val = null; |
| | | } |
| | | // set entity value |
| | | if (os[1] instanceof Field){ |
| | | Reflections.invokeSetter(e, ((Field)os[1]).getName(), val); |
| | | }else if (os[1] instanceof Method){ |
| | | String mthodName = ((Method)os[1]).getName(); |
| | | if ("get".equals(mthodName.substring(0, 3))){ |
| | | mthodName = "set"+StringUtils.substringAfter(mthodName, "get"); |
| | | } |
| | | Reflections.invokeMethod(e, mthodName, new Class[] {valType}, new Object[] {val}); |
| | | } |
| | | } |
| | | sb.append(val+", "); |
| | | } |
| | | sortedFields.put(excelColumn.index() == -1 ? index : excelColumn.index(), new ColumnInfo(excelColumn, field)); |
| | | index++; |
| | | dataList.add(e); |
| | | log.debug("Read success: ["+i+"] "+sb.toString()); |
| | | } |
| | | return new ArrayList<>(sortedFields.values()); |
| | | return dataList; |
| | | } |
| | | |
| | | /** |
| | | * 判断是否为空行 |
| | | * |
| | | * @param row 行对象 |
| | | * @return Boolean |
| | | * 关闭流 清理临时文件 |
| | | */ |
| | | private boolean isEmptyRow(Object row) throws IllegalAccessException{ |
| | | Field[] fields = row.getClass().getDeclaredFields(); |
| | | for (Field field : fields) { |
| | | field.setAccessible(Boolean.TRUE); |
| | | if (field.get(row) != null) { |
| | | field.setAccessible(Boolean.FALSE); |
| | | return Boolean.FALSE; |
| | | public void dispose(){ |
| | | try { |
| | | if(wb!=null){ |
| | | wb.close(); |
| | | } |
| | | field.setAccessible(Boolean.FALSE); |
| | | } catch (IOException e) { |
| | | e.printStackTrace(); |
| | | throw new RuntimeException(e); |
| | | } |
| | | return Boolean.TRUE; |
| | | |
| | | } |
| | | |
| | | /** |
| | | * 列信息 |
| | | */ |
| | | @Data |
| | | @AllArgsConstructor |
| | | private static class ColumnInfo { |
| | | |
| | | // 列配置 |
| | | private ExcelImportColumn columnConfig; |
| | | |
| | | // 字段信息 |
| | | private Field field; |
| | | } |
| | | } |
| | | } |