問題
在一般的后臺管理系統(tǒng)中都少不了導(dǎo)入導(dǎo)出功能录粱,而且很多都是以Excel的格式進行操作签财。在之前的項目中用的是前輩們提供的一個工具類伍茄〖浒Γ總結(jié)一下這個工具類可以幫你從Excel中解析出你要的數(shù)據(jù)绞灼,也可以把你的數(shù)據(jù)轉(zhuǎn)化成Excel,功能比較基礎(chǔ)和純粹呈野。但是從一個完成的導(dǎo)入導(dǎo)出邏輯來說低矮,我們要做的遠不止這些東西,比如在我們的項目中被冒,完整的導(dǎo)入導(dǎo)出應(yīng)該是這樣的:
- 導(dǎo)入:
- 用戶選擇文件军掂,點擊導(dǎo)入按鈕
- 前端頁面展示目前的導(dǎo)入進度轮蜕,總條數(shù),成功條數(shù)蝗锥,錯誤條數(shù)
- 導(dǎo)入完畢跃洛,展示包含錯誤數(shù)據(jù)的錯誤文件的下載鏈接
- 用戶下載錯誤文件,查看每條數(shù)據(jù)導(dǎo)入錯誤的原因终议,然后修改數(shù)據(jù)
- 重新導(dǎo)入
- 導(dǎo)入應(yīng)該支持Excel 03版本汇竭,07版本
- 導(dǎo)入性能應(yīng)該盡量高
- 導(dǎo)入最大應(yīng)該支持100萬數(shù)據(jù)的導(dǎo)入,不能出現(xiàn)OOM
- 導(dǎo)出:
- 用戶點擊導(dǎo)出按鈕
- 彈出對話框讓用戶選擇需要導(dǎo)出的字段
- 用戶點擊確定穴张,開始導(dǎo)出
- 前端頁面顯示導(dǎo)出進度细燎,總條數(shù),當(dāng)前條數(shù)
- 導(dǎo)出完畢陆馁,直接下載文件到本地電腦
- 導(dǎo)出應(yīng)該支持Excel 03版本找颓,07版本
- 導(dǎo)出性能應(yīng)該盡量高
- 導(dǎo)出最大應(yīng)該支持100萬數(shù)據(jù)的導(dǎo)出,不能出現(xiàn)OOM
基于上面這些問題叮贩,我們原來的一個簡單的工具類是沒辦法解決的,只能另尋他路佛析。
引入EasyExcel
幾經(jīng)尋找益老,找到了這個阿里巴巴開源的項目,看了一下文檔寸莫,下載下來把我需要的幾個功能都試了一下捺萌,感覺不錯。不管從可用性膘茎,穩(wěn)定性桃纯,性能以及學(xué)習(xí)成本上都比我們原來的工具類強很多,引入EasyExcel之后我們可以解決上面導(dǎo)入導(dǎo)出場景中的6披坏,7态坦,8問題,就是說針對Excel本身的這些操作我們可以不用操心了棒拂,完全交給EasyExcel就可以了伞梯,我們自己要去解決剩下的問題。
設(shè)計導(dǎo)入模式
其實我們大部分的導(dǎo)入場景的過程都是一樣的帚屉,基本上分為以下幾步:
- 從Excel解析出數(shù)據(jù)谜诫,轉(zhuǎn)換為Java對象
- 遍歷Java對象,處理每一個Java對象攻旦,通常就是簡單的插入數(shù)據(jù)庫
- 導(dǎo)入完畢喻旷,得到導(dǎo)入的結(jié)果:總條數(shù),成功條數(shù)牢屋,錯誤條數(shù)且预,錯誤數(shù)據(jù)列表
所以這個地方我們可以設(shè)計一個模板模式+策略模式牺陶,這樣可以將導(dǎo)入過程統(tǒng)一起來,把公共的步驟提供默認(rèn)實現(xiàn)辣之,每個業(yè)務(wù)只需要實現(xiàn)自己不同的地方掰伸,就是實現(xiàn)怎么去處理每一個Java對象。
簡單寫一下偽代碼:
導(dǎo)入模板接口:
interface ImportTemplateInterface<T>{
/**
*默認(rèn)方法怀估,實現(xiàn)導(dǎo)入模板過程
*/
default String import(InputStream ins){
//任務(wù)id狮鸭,返回給客戶端用戶查詢導(dǎo)入進度
String taskId = UUID.random().toString();
//異步執(zhí)行
new Thread(
new Runable(){
public void run(){
updateProgress(taskId,START);
//利用easyExcel讀取數(shù)據(jù)
List<T> modelList = easyExcel.read(ins);
//錯誤數(shù)據(jù)列表
List<ErrorModel> errorList = new ArrayList<>();
updateProgress(taskId,totalCount++);
modelList.foreach(model -> {
try{
//處理數(shù)據(jù)
importItem(model);
updateProgress(taskId,successCount++);
}catch(Exception e){
//加入到錯誤列表
errorList.add(toErrorModel(model));
updateProgress(taskId,errorCount++);
}
});
updateProgress(END);
}
}
).start()
return taskId;
}
/**
*處理數(shù)據(jù)方法,由具體實現(xiàn)類去實現(xiàn)
*/
void importItem(T model);
/**
*更新進度方法多搀,默認(rèn)實現(xiàn)類可以提供默認(rèn)實現(xiàn)歧蕉,具體實現(xiàn)類可以覆蓋
*/
void updateProgress(String taskId,int count);
}
模板類的默認(rèn)實現(xiàn):
abstract class DefaultImportTemplate<T> implements ImportTemplateInterface <T>{
@Resource
private Redis redis;
/**
*抽象方法,處理數(shù)據(jù)康铭,由子類提供實現(xiàn)
*/
abstract void importItem(T model);
/**
*默認(rèn)的更新進度實現(xiàn)
*/
void updateProgress(String taskId,int count){
//進度存儲到redis
redis.update(taskId,count);
}
/**
*默認(rèn)的獲取流程進度的方法
*/
void getProgress(String taskId){
redis.get(taskId);
}
/**
*默認(rèn)的獲取錯誤數(shù)據(jù)的方法
*/
List<ErrorModel> getErrorList(String taskId){
redis.getErrorList(taskId);
}
}
業(yè)務(wù)類的實現(xiàn):
class UserImportTemplate extends DefaultImportTemplate<User> {
@Resource
private UserDao userDao;
/**
*用戶導(dǎo)入實現(xiàn)處理數(shù)據(jù)的方法惯退,插入用戶
*/
void importItem(User model){
userDao.insert(model);
}
}
設(shè)計導(dǎo)出模式
其實導(dǎo)出跟導(dǎo)入一樣也可以使用模板模式+策略模式,在導(dǎo)出的過程里面从藤,主要需要業(yè)務(wù)自己實現(xiàn)的是獲取數(shù)據(jù)的方法和獲取表頭的方法催跪,其他的都可以在模板里面做好。這里可以沒有默認(rèn)實現(xiàn)夷野,因為獲取數(shù)據(jù)和獲取表頭都是和業(yè)務(wù)強相關(guān)的沒法提供默認(rèn)實現(xiàn)懊蒸。偽代碼思路基本上跟導(dǎo)入是一樣的,不寫了悯搔。
導(dǎo)出的動態(tài)表頭設(shè)計
我們系統(tǒng)有點特殊的地方骑丸,就是導(dǎo)出的字段是可選的,那這個該怎么辦呢妒貌?
設(shè)計一個注解
注解有index,value兩個基礎(chǔ)屬性通危,index表示導(dǎo)出時表頭字段的順序,value表示表頭的顯示名稱灌曙,把注解加在需要導(dǎo)出的實體類的字段上菊碟。
獲取所有可選表頭
前端傳一個參數(shù)表示是哪個模塊的導(dǎo)出操作,后端根據(jù)參數(shù)找到該模塊導(dǎo)出的實體類平匈,然后利用反射找出實體類中所有加了上面那個注解的字段框沟,然后封裝成表頭對象<index,columnKey,columnName>,columnKey是指字段的名字增炭,columnName是指表頭顯示名稱忍燥,就是上面注解的value。然后把這些可選表頭全部返回給前端隙姿,給用戶選擇梅垄。
根據(jù)選擇的表頭獲取數(shù)據(jù)
用戶在頁面上勾選的表頭時候提交到后端,后端獲取了表頭之后,使用EasyExcel的創(chuàng)建表頭功能創(chuàng)建表頭队丝,這一步比較簡單靡馁,就是把我們自己的表頭對象轉(zhuǎn)換成EasyExcel的格式,主要用到index和columnName兩個屬性机久。然后查詢出需要導(dǎo)出的數(shù)據(jù)臭墨,因為我們查詢出來的對象里面是所有屬性都有值的,如果直接使用EasyExcel導(dǎo)出的話膘盖,會把所有數(shù)據(jù)都會導(dǎo)出胧弛,這樣就會出現(xiàn),表頭是選擇的那么多侠畔,但是每一行后面的數(shù)據(jù)會多出那些沒有選擇的列结缚。因此,我們在從數(shù)據(jù)庫獲取數(shù)據(jù)之后软棺,我們還需要利用表頭中的columnKey的值進行反射獲取對象的屬性值红竭,我們只獲取用戶選擇的那些columnKey的值,然后組裝成EasyExcel中需要的那種格式喘落,然后就可以導(dǎo)出了茵宪。在這個過程中我們的表頭和我們的內(nèi)容順序要保持一致,要不然會出現(xiàn)內(nèi)容和表頭對不上揖盘,所以我們在創(chuàng)建表頭和內(nèi)容是都要先對提交上來的可選表頭按index排序眉厨,這樣就OK了。用文字描述可能有點抽象兽狭,其實自己去實現(xiàn)的話,沒有說的那么麻煩鹿蜀。如果你的導(dǎo)出表頭是固定的那就很簡單了箕慧,只需要使用EasyExcel的注解,然后直接查出數(shù)據(jù)茴恰,直接導(dǎo)出就可以了颠焦。EasyExcel提供了根據(jù)模型注解導(dǎo)出,也可以自己組裝數(shù)據(jù)和表頭往枣,所以我們利用后面這個特點就可以實現(xiàn)動態(tài)表頭的導(dǎo)出功能伐庭。
這個地方還有一點要注意的是,你要使用反射來自己獲取屬性值的話分冈,那日期類型的數(shù)據(jù)你要自己格式化一下圾另,要不然導(dǎo)出到Excel里面格式是不固定的,也許不是你想要的格式雕沉。
好了集乔,寫了這么多,感覺就是一個簡單的導(dǎo)入導(dǎo)出功能坡椒,如果是需要做到系統(tǒng)里面的導(dǎo)入導(dǎo)出比較統(tǒng)一實現(xiàn)的話扰路,可以參考一下尤溜,這樣其他的伙伴在開發(fā)的時候就比較簡單了,不用每個人都把這些做一遍汗唱。