EasyExcel實現文件導出


官網:https://www.yuque.com/easyexcel/doc/easyexcel

導出

准備工作

引入依賴

<!--EasyExcel相關依賴-->
  <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>3.0.5</version>
  </dependency>

創建監聽器

@Data
public class ExcelDataListener<T> extends AnalysisEventListener<T> {
    private static Logger log4 = Logger.getLogger(AreaServiceImpl.class);

    /**
     * 導入出錯的信息(此處保存的為解析時錯誤的信息,某行某列+錯誤信息+;)
     */
    private List<String> errorMsg = new ArrayList<>();

    /**
     * 必要列為空的信息(某行+'列1,列2...'+的值為空)
     */
    private List<String> nullFields = new ArrayList<>();

    /**
     * 導入的表頭(解析到的表頭)
     */
    private Map<Integer, String> headMap = new HashMap<>();

    /**
     * 緩存的數據(成功解析的數據)
     */
    private List<T> dataList = new ArrayList<>();

    /**
     * 讀取報錯的數據(解析錯誤的數據,即無法映射到導出的實體類)
     */
    private List<Map<String, String>> errorDataList = ListUtils.newArrayList();

    /** 導出實體類的行索引字段名。
      *用於設置解析到的數據所在的行數(在進行導入的時候用於定位service中save出錯的數      
      *據,所在的行)
      */
    private String rowIndexField;

    /**
     * 假設這個是一個DAO,當然有業務邏輯這個也可以是一個service。當然如果不用存儲這個對象沒用。
     */
//    private DemoDAO demoDAO;

//    public ExcelDataListener(DemoDAO demoDAO) {
//        this.demoDAO = demoDAO;
//    }

    /**
     * 這個每一條數據解析都會來調用
     *
     * @param data    one row value
     * @param context
     */
    @Override
    public void invoke(T data, AnalysisContext context) {
        log4.info("解析到一條數據:" + JSON.toJSONString(data));
        // 由於會讀取空行(excel中刪除數據后的行)所以這里加判斷
        if (!"{}".equals(JSON.toJSONString(data))) {
        // 由於第一行是表頭,因此這里+1
           int rowIndex = context.readRowHolder().getRowIndex() + 1;
            Class<?> dataClazz = data.getClass();
            try {
                // 如果沒有設置實體類行索引的字段名,則默認為rowIndex
                String fieldName = StringUtils.isEmpty(rowIndexField) ? "rowIndex" : rowIndexField;
                Field rowIndexField = dataClazz.getDeclaredField(fieldName);
                rowIndexField.setAccessible(true);
                rowIndexField.set(data,rowIndex);
            } catch (NoSuchFieldException | IllegalAccessException e) {
                e.printStackTrace();
                log4.info("對應的實體中沒有行數");
            }
            // 校驗必要列是否為空
            List<String> checkField = ExcelUtil.checkField(data);
            if (checkField.size() > 0) {
                String errorMsg = "第" + rowIndex + "行," +
                        String.join("、",checkField) + "列的值為空";
                nullFields.add(errorMsg);
            } else {
                dataList.add(data);
            }
        }
        else
            context.readRowHolder().setRowType(RowTypeEnum.EMPTY);
    }

    /**
     * 讀取的表頭
     *
     * @param headMap  表頭
     * @param context
     */
    @Override
    public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
        // 此處由於會讀取空單元格表頭(刪除內容后的單元格也會被讀取)所以進行遍歷判斷
        Set<Map.Entry<Integer, String>> entries = headMap.entrySet();
        for (Map.Entry<Integer, String> head : entries) {
            if (!StringUtils.isEmpty(head.getValue()))
                this.headMap.put(head.getKey(),head.getValue());
        }
    }

    @Override
    public boolean hasNext(AnalysisContext context) {
        // 由於會讀取空行數據,因此此處加判斷遇到空行停止讀取;(invoke方法中遇到空行會設置RowType)
        if (RowTypeEnum.EMPTY.equals(context.readRowHolder().getRowType())) {
            doAfterAllAnalysed(context);
            return false;
        }
        return super.hasNext(context);
    }

    /**
     * 所有數據解析完成了 都會來調用
     *
     * @param context
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        // 這里也要保存數據,確保最后遺留的數據也存儲到數據庫
        log4.info("所有數據解析完成!");
    }

    /**
     * 在轉換異常 獲取其他異常下會調用本接口。拋出異常則停止讀取。如果這里不拋出異常則 繼續讀取下一行。
     *
     * @param exception
     * @param context
     * @throws Exception
     */
    @Override
    public void onException(Exception exception, AnalysisContext context) {
        // 如果是某一個單元格的轉換異常 能獲取到具體行號
        // 如果要獲取頭的信息 配合invokeHeadMap使用
        if (exception instanceof ExcelDataConvertException) {
            ExcelDataConvertException e = (ExcelDataConvertException)exception;
            String errorIndexInfo = "第" + (e.getRowIndex() + 1) + "行"
                    + "的”" +headMap.get(e.getColumnIndex()) +"“列,";
            errorMsg.add(errorIndexInfo + exception.getMessage());
            // 構造錯誤的數據
            ReadRowHolder readRowHolder = context.readRowHolder();
            HashMap<String, String> data = new HashMap<>();
            Set<Map.Entry<Integer, Cell>> entries = readRowHolder.getCellMap().entrySet();
            for (Map.Entry<Integer, Cell> entry : entries) {
                ReadCellData value = (ReadCellData)(entry.getValue());
                data.put(headMap.get(entry.getKey()),value.getStringValue());
            }
            errorDataList.add(data);
        }
    }

}

工具類校驗必要字段

/**
     * 校驗導入實體的必要字段
     * @param entity 導入的實體
     * @param <T>
     * @return
     */
    public static <T> List<String> checkField(T entity){
        Class<?> clazz = entity.getClass();
        Field[] fields = clazz.getDeclaredFields();
        List<String> nullFields = new ArrayList<>();
        for (Field field : fields) {
            NeedNotNull notNull = field.getDeclaredAnnotation(NeedNotNull.class);
            ExcelProperty property = field.getDeclaredAnnotation(ExcelProperty.class);
            field.setAccessible(true);
            // 該字段是否不能為空
            if (!ObjectUtils.isEmpty(notNull)){
                try {
                    Object value = field.get(entity);
                    // 如果必要字段的值為空則校驗失敗
                    if (ObjectUtils.isEmpty(value))
                        nullFields.add(StringUtils.join(property.value(),","));
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                    log4.error(field.getName() + "字段值獲取異常:"+e.getMessage());
                }
            }
        }
        return nullFields;
    }

自定義注解

/**
 * 自定義注解,用於判斷是否需要合並以及合並的主鍵
 */
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface CustomMerge {

    /**
     * 是否需要合並單元格
     */
    boolean needMerge() default false;

    /**
     * 是否是主鍵,即該字段相同的行合並
     */
    boolean isPk() default false;
}

/**
 * 自定義注解,用於判斷字段是否不能為空
 */
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface NeedNotNull {

    /**
     * 是否不能為空
     */
    boolean value() default true;
    /**
     * 字段名稱
     */
    String name() default "";
}

創建導出的實體類

@Data
public class AreaImportDto {

    /** 區域名稱 */
    @ExcelProperty("區域名稱")
    @CustomMerge(needMerge = true, isPk = true)
    @ColumnWidth(20)
    @NeedNotNull(name = "區域名稱")
    private String areaName;

    /** 區域的X坐標 */
    @ExcelProperty("父區域")
    @CustomMerge(needMerge = true)
    @ColumnWidth(10)
    private String parentName;

    /** 人員名稱 */
    @ExcelProperty("人員名稱")
    @CustomMerge(needMerge = true)
    @ColumnWidth(20)
    private String personName;

    /** 導入數據所在的行數 */
    private Integer rowIndex;
}

ps:
@ExcelProperty: 導出的列名(如果是多行表頭,則值為數組,詳見官網)
@CustomMerge(needMerge = true, isPk = true):要合並的列
@ColumnWidth(20):列寬
@NeedNotNull(name = "區域名稱"):是否為必要列

構造數據

  • 調用service查詢數據庫數據
  • 定義轉換方法,將實體類轉換為導出實體(此處可以省略,如果不另外定義導出實體的話耦合性較高)

創建導出工具類

private static final String FILE_SEPARATOR = "/";

/**
     * 數據導出到excel
     *
     * @param params          請求參數(自定義的請求參數實體,包含query(Map)、sort、page屬性)
     * @param serverPath      服務器文件導出路徑
     * @param defaultFileName 默認文件名
     * @param targetData      導出數據
     * @param clazz           導出數據的類對象
     * @param isMergeStrategy 是否要合並行(一對多導出時用到)
     * @return
     * @throws IOException
     */
    public static <T> Map<String, String> exportExcel(SearchParamDto params, String defaultFileName,
                                                      String serverPath, String urlPath, List<T> targetData,
                                                      Class<T> clazz, boolean isMergeStrategy) throws IOException {
        HashMap<String, String> result = new HashMap<String, String>() {{
            put("msg", "數據導出成功");
        }};
        // 請求中傳遞的需要導出的列
        JSONArray fields = JSONArray.parseArray(params.getQuery().get("fields").toString());
        // 設置文件名
        String fileName1 = ObjectUtils.isEmpty(params.getQuery().get("fileName")) ?
                defaultFileName :
                params.getQuery().get("fileName").toString();
//        String fileName = URLEncoder.encode(fileName1, "UTF-8").replaceAll("\\+", "%20") + ".xlsx";
        String fileName = fileName1 + ".xlsx";

        try {
            // 獲取當前時間
            String updTm = DateUtil.getAllTime();
            String filePath = serverPath + FILE_SEPARATOR + updTm + FILE_SEPARATOR + fileName;
            String destDirName = serverPath + FILE_SEPARATOR + updTm;
            createDir(destDirName);
            FileOutputStream fileOutputStream = new FileOutputStream(filePath);
            log4.info(fileName1 + "數據導出成功!");
            ExcelWriterBuilder head = EasyExcel.write(fileOutputStream).autoCloseStream(true)
                    .includeColumnFiledNames(fields.toJavaList(String.class))
                    .head(clazz);
            if (isMergeStrategy) {
                head.registerWriteHandler(new CustomMergeStrategy(clazz));
            }
            head.excelType(ExcelTypeEnum.XLSX)
                    .sheet("sheet1")
                    .doWrite(targetData);
            String url = urlPath + FILE_SEPARATOR + updTm + FILE_SEPARATOR + fileName;
             // 返回請求文件的url以及服務器報錯路徑
            result.put("url", url);
            result.put("savePath", serverPath + FILE_SEPARATOR + updTm);
        } catch (Exception e) {
            log4.error("數據導出失敗: {}", e);
            result.put("msg", "數據導出失敗");
            return result;
        }

        return result;
    }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM