官網: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;
}