大家好,我是鵬磊。
以 JSON配置 的方式去實作通用性和動態調整,當然,這個通用仍然存在一定的局限性,每個計畫的程式碼風格都不同,想要寫出一個適合所有計畫的通用性模組並不容易,這裏的通用局限於其所在計畫,所以該功能程式碼如果不適用於自己的計畫,希望可以以此為參考,稍作修改。
那麽現在來分析一下,我們會需要哪些JSON配置項。
匯出
基礎配置項
先從最簡單的匯出開始,被匯出數據應該支持透過業務層查出,如:Service.search(param),這是大前提,
然後為了支持顯示匯出進度,業務層還需要提供數量查詢方法,如:Service.count(param),否則無法實作匯出進度。
最後匯出檔名也可以客製,如: filename
由上可以得出配置項:
serviceClazz :業務類路徑,如:com.cc.service.UserService,必填
methodName :查詢方法名,如:listByCondition,必填
countMethodName :數量查詢方法名,可填,用於支持匯出進度
filename :匯出檔名
searchParams :查詢參數,陣列型別,字典元素。用陣列是為了支持查詢方法需要傳多參數的情況
至於查詢方法的參數類,不需要填,因為我們可以透過反射去獲取到該方法所需要傳入的參數型別
如果你近期準備面試跳槽,建議在ddkk.com線上刷題,涵蓋 一萬+ 道 Java 面試題,幾乎覆蓋了所有主流技術面試題,還有市面上最全的技術五百套,精品系列教程,免費提供。
註意,以下貼出的是關鍵程式碼,僅作參考理解:
class<?> service class = class.forName(param.getServiceClazz());
// param為請求參數類
Method searchMethod = ReflectUtil.findMethodByName(service class, param.getMethodName());
// 方法所需要傳入的參數列
class<?>[] parameterTypes = searchMethod.getParameterTypes();
/**
* 透過反射從指定類中獲取方法物件
*/
publicstatic Method findMethodByName( class<?> clazz, String name){
Method[] methods = clazz.getMethods();
if (StringUtils.isEmpty(name)) {
returnnull;
}
for (Method method : methods) {
if (method.getName().equals(name)) {
return method;
}
}
returnnull;
}
現在我們來想想,匯出都會有哪些場景:
1、
列表頁的分頁查詢,可能是當前頁數據匯出,也可能是所有數據匯出,這涉及到
分頁查詢
;
2、
數據總覽頁的查詢,通常是開發者自訂的復雜連表查詢,不需要分頁;
那麽本文針對以上兩種情況來實作第一版的通用匯出功能。
列表頁的分頁查詢
列表頁的數據匯出分 當前頁匯出 和 所有數據匯出 ,
假設查詢流程是這樣的:
1、
介面層接收參數:Controller.search(Paramparam);
2、
業務層呼叫查詢方法:Service.search(param);
3、
持久層存取資料庫:Mapper.search(param);
這種情況很簡單,但如果流程是這樣的:
1、
介面層接收參數:Controller.search(Paramparam);
2、
業務層呼叫查詢方法:Service.search(newCondition(param));
3、
持久層存取資料庫:Mapper.search(condition);
上面程式碼中,介面請求參數和持久層參數不一致,在業務層經過了包裝,那麽這種情況也要相容處理。
但是如果請求參數在業務層經過了包中包中包,那麽就算了。
接著是分頁參數,我們用 pageNum 和 pageSize 來表示頁碼和數量欄位,類似於:
{
"pageNum": 1,
"pageSize": 10,
"name": "老劉"// 此為查詢欄位,如查詢名字為老劉的數據
}
關於當前頁匯出和所有數據匯出,可以用一個bool來表示: onlyCurrentPage ,預設false,即匯出時會自動分頁查詢數據,直到所有數據查詢完畢,匯出所有數據分時頁查詢很有必要,能提高效能,避免記憶體溢位,當onlyCurrentPage為true時,則只匯出當前頁面數據。
如果你近期準備面試跳槽,建議在ddkk.com線上刷題,涵蓋 一萬+ 道 Java 面試題,幾乎覆蓋了所有主流技術面試題,還有市面上最全的技術五百套,精品系列教程,免費提供。
得出需要的配置項為:
searchParam :介面分頁請求參數,JSON型別,必填
conditionClazz :條件查詢類,也可以認為是包裝類,如:com.cc.codition.UserCondition,可填
onlyCurrentPage :僅當前頁匯出,預設false,可填
數據總覽頁的查詢
數據總覽數據沒有數量查詢方法,即Service.count(xxx),也沒有分頁查詢參數,類似於當前頁匯出,在也只考慮一層包裝類的情況下,沒有額外的配置項,上面的已經足夠了,要註意的就是程式碼裏面得把分頁參數剔除掉。
表頭配置
一級表頭
模擬一些數據來加深理解,現有一個介面是 查詢系統使用者列表 ,如:/user/search,返回結果是這樣的:
{
"code": 0,
"msg": "請求成功",
"data": [
{
"id": 1,
"username": "admin",
"nickname": "超管",
"phone": "18818881888",
"createTime": "2023-06-23 17:16:00"
},
{
"id": 2,
"username": "cc",
"nickname": "管理員",
"phone": "18818881888",
"createTime": "2023-06-23 17:16:00"
},
...
]
}
現在貼出EasyExcel的程式碼:
// 建立excel檔
try (ExcelWriter excelWriter = EasyExcel.write(path).build()) {
WriteSheet writeSheet = EasyExcel.writerSheet("sheet索引", "sheet名稱").head(getHeader()).build();
excelWriter.write(getDataList(), writeSheet);
}
// 模擬表頭
privatestatic List<List<String>> getHeader() {
List<List<String>> list = new ArrayList<>();
list.add(createHead("帳號"));
list.add(createHead("昵稱"));
list.add(createHead("聯系方式"));
list.add(createHead("註冊時間"));
return list;
}
publicstatic List<String> createHead(String... head){
returnnew ArrayList<>(Arrays.asList(head));
}
// 模擬數據
publicstatic List<List<Object>> getDataList() {
List<List<Object>> list = new ArrayList<>();
list.add(createData("admin", "超管", "18818881888", "2023-06-23 17:16:00"));
list.add(createData("cc", "管理員", "18818881888", "2023-06-23 17:16:00"));
return list;
}
publicstatic List<Object> createData(String... data){
returnnew ArrayList<>(Arrays.asList(data));
}s
然後匯出效果是這樣的:
現在先別在乎效果圖的excel樣式,我們後面都會進行動態配置,比如列寬、表頭背景色、字型居中等。
上面我們雖然是寫死了程式碼,但聰明的開發者一定懂得將資料庫查詢來的數據轉換成對應的格式,所以這段就跳過了。
現在我們就可以得出基礎的表頭配置:
"customHeads": [
{
"fieldName": "username",
"fieldNameZh": "帳號"
},
{
"fieldName": "nickname",
"fieldNameZh": "昵稱"
},
{
"fieldName": "phone",
"fieldNameZh": "聯系方式"
},
{
"fieldName": "createTime",
"fieldNameZh": "註冊時間"
}
]
也就是:
fieldName :內容名,這樣可以從返回結果的數據物件裏面透過反射找到該內容以及值
fieldNameZh :內容名肯定不適合作為表頭名,增加一個中文說明來代替內容名作為表頭
有了上面的基礎,我們就可以增加更多的項來實作功能的豐富性,比如
{
"fieldName": "username",
"fieldNameZh": "帳號",
"width": 20, // 列寬
"backgroundColor": 1, // 表頭背景色
"fontSize": 20, // 字型大小
"type": "date(yyyy-MM-dd)"// 欄位型別
...
}
註:欄位型別可以用作數據格式化,比如該內容是一個status狀態,1表示正常,2表示異常,那麽匯出這個1或2是沒有意義的,所以透過欄位型別辨識出這個狀態值對應的中文描述,這樣的匯出才正常。
一級表頭 已經可以滿足我們許多場景了,但是這並不足夠,我的經驗中,經常需要用到兩行表頭甚至是復雜表頭,好在EasyExcel是支持多級表頭的。
多級表頭
先貼出EasyExcel生成 二級表頭 的範例程式碼:
// 模擬表頭
privatestatic List<List<String>> getHeader() {
List<List<String>> list = new ArrayList<>();
list.add(createHead("使用者資訊", "帳號"));
list.add(createHead("使用者資訊", "昵稱"));
list.add(createHead("使用者資訊", "聯系方式"));
list.add(createHead("使用者資訊", "註冊時間"));
list.add(createHead("角色資訊", "超管"));
list.add(createHead("角色資訊", "管理員"));
return list;
}
publicstatic List<String> createHead(String... head){
returnnew ArrayList<>(Arrays.asList(head));
}
// 模擬數據
publicstatic List<List<Object>> getDataList() {
List<List<Object>> list = new ArrayList<>();
list.add(createData("admin", "超管", "18818881888", "2023-06-23 17:16:00", "是", "是"));
list.add(createData("cc", "管理員", "18818881888", "2023-06-23 17:16:00", "否", "是"));
return list;
}
publicstatic List<Object> createData(String... data){
returnnew ArrayList<>(Arrays.asList(data));
}
效果是這樣的:
可以看到,前面4列有一個共同表頭【使用者資訊】,後面兩列有一個共同表頭【角色資訊】,
從上面的範例程式碼我們知道,要使表頭合並,數據列表得按順序和相同表頭名,這樣會被EasyExcel辨識到然後才有合並效果,這點需要註意。
同理,當我們需要生成復雜表頭的時候,可以這樣:
// 模擬表頭
privatestatic List<List<String>> getHeader() {
List<List<String>> list = new ArrayList<>();
list.add(createHead("匯出使用者數據", "使用者資訊", "帳號"));
list.add(createHead("匯出使用者數據", "使用者資訊", "昵稱"));
list.add(createHead("匯出使用者數據", "使用者資訊", "聯系方式"));
list.add(createHead("匯出使用者數據", "使用者資訊", "註冊時間"));
list.add(createHead("匯出使用者數據", "角色資訊", "超管"));
list.add(createHead("匯出使用者數據", "角色資訊", "管理員"));
return list;
}
效果圖:
結論
以上是我對匯出功能的思考和實作思路,因為篇幅的關系,我沒有貼出完整的程式碼,但是相信以上內容已經足夠大家作為參考,缺少的內容,比如列寬、顏色字型等設定,請查閱EasyExcel官方文件來實作,主要方式就是根據前端傳過來的JSON配置資訊,來動態配置EasyExcel的匯出檔。
如果你近期準備面試跳槽,建議在ddkk.com線上刷題,涵蓋 一萬+ 道 Java 面試題,幾乎覆蓋了所有主流技術面試題,還有市面上最全的技術五百套,精品系列教程,免費提供。
匯入
匯入分兩個步驟:
1、
使用者下載匯入樣版;
2、
使用者填內容進匯入樣版,然後上傳樣版檔到系統,實作數據匯入操作;
下載匯入樣版
匯入樣版只需要上面的 customHeads 參數即可:
"customHeads": [
{
"fieldName": "username",
"fieldNameZh": "帳號"
},
{
"fieldName": "nickname",
"fieldNameZh": "昵稱"
},
{
"fieldName": "phone",
"fieldNameZh": "聯系方式"
},
{
"fieldName": "createTime",
"fieldNameZh": "註冊時間"
}
]
甚至fieldName都可以不要,生成一個只有表頭的excel檔。
匯入數據
匯入數據有兩種場景:
1、
單表數據匯入
,該場景很簡單;
2、
復雜數據匯入
,涉及多表,這種情況就稍微復雜點;
單表數據匯入
單表只需要考慮對應實體類的內容即可,我們可以透過反射來獲取實體類的內容,所以需要的配置項是:
modelClazz :實體類路徑,如:com.cc.entity.User
配置範例:
{
"modelClazz": "com.cc.entity.User",
"customHeads": [
{
"fieldName": "username",
"fieldNameZh": "帳號"
},
{
"fieldName": "nickname",
"fieldNameZh": "昵稱"
},
{
"fieldName": "phone",
"fieldNameZh": "聯系方式"
},
{
"fieldName": "createTime",
"fieldNameZh": "註冊時間"
}
]
}
這樣在匯入數據,被EasyExcel讀取每一行數據的時候,可以辨識到如:username項對應com.cc.entity.User類的username內容那麽就能做到類似這樣的事情:
User user = new User();
user.setUsername(fieldName列的值)
由此可以得到一個List userList陣列,再透過系統的UserService或UserMapper保存到資料庫,即可實作數據匯入操作。
復雜數據匯入
復雜數據比如這種場景:excel檔中每行的數據是這樣的:
帳號 | 昵稱 | 聯系方式 | 註冊時間 | 角色名 |
---|---|---|---|---|
admin | 超管 | 18818881888 | 2023-06-23 17:16:00 | 超級管理員 |
cc | 管理員 | 18818881888 | 2023-06-23 17:16:00 | 管理員 |
其中是否超管和是否管理員涉及關聯表:
使用者表:tb_user
角色表:tb_role
使用者角色關聯表:tb_user_role_relation
為了支持這種復雜數據匯入,系統內需要提供對應的保存方法:
1、 新建DTO類:;
第一種:
public classUserDto{
private String username;
private String nickname;
private String phone;
private Date createTime;
private Boolean superAdminFlag;
private Boolean adminFlag;
}
第二種:
public classUserDto{
private User user;
private Role role;
}
這兩種DTO的情況我們都應該考慮,第一種不用多說,上面的配置就可以應對,主要看第二種,第二種方式要考慮「 路徑 」這個問題,所以customHeads的寫法就要有所改變:
{
"modelClazz": "com.cc.model.UserDto",
"customHeads": [
{
"fieldName": "user.username",
"fieldNameZh": "帳號"
},
...
]
}
這樣配置帳號路徑為:user.username,內容的反射查詢就要有遞迴概念,先去尋找UserDto類的user內容,得到該內容的類,再去獲取其內的username內容,賦值方式就變成了:
UserDto dto = new UserDto();
User user = new User();
user.setUsername(fieldName列的值);
dto.setUser(user);
這樣得到一個List dtoList陣列。 2、 既然有復雜數據匯入的業務,那麽在Service業務層中,也應該編寫復雜數據的保存函式:;
publicinterfaceUserService{
// 單條插入
voidsaveUserDto(UserDto dto);
// 批次插入
voidsaveUserDtoBatch(List<UserDto> dtoList);
}
@Service
public classUserServiceImplimplementsUserService{
@Autowired
private UserMapper userMapper;
@Autowired
private RoleService roleService;
@Autowired
private UserRoleRelationService relationService;
// 事務
@Transactional(rollbackFor = Exception. class)
@Override
publicvoidsaveUserDto(UserDtodto) {
// 保存使用者
User user = userMapper.save(dto.getUser());
// 保存角色
Role role = roleService.save(dto.getRole);
// 保存關聯
UserRoleRelation relation = new UserRoleRelation();
relation.setUserId(user.getId());
relation.setRoleId(role.getId());
relationService.save(relation);
}
// 批次插入程式碼省略,原理同上
voidsaveUserDtoBatch(List<UserDto> dtoList);
}
3、 透過EasyExcel讀取到的每一行數據都能轉成UserDto物件,再透過單條或批次來保存數據,這期間有許多可以最佳化考慮的點,比如:;
批次比單條保存效率高、效能好,但是批次不容易辨識出部份失敗的行
批次保存的數量不能太多,要考慮系統和資料庫的效能,比如每次讀取500行就執行一次保存
如果你近期準備面試跳槽,建議在ddkk.com線上刷題,涵蓋 一萬+ 道 Java 面試題,幾乎覆蓋了所有主流技術面試題,還有市面上最全的技術五百套,精品系列教程,免費提供。
保存的進度顯示,先獲取excel總行數,再根據當前讀取行數來計算進度,並返回給前端
匯入時間過長,可以做成後台任務進行,至於前端提醒可以是輪詢也可以是WebSocket
所以需要指定查詢方法,這配置項上面已經給出來了。
配置項總結
最後給出一個總配置項出來參考:
匯出數據配置
{
"filename": "使用者數據匯出",
"serviceClazz": "com.cc.service.UserService",
"methodName": "listByCondition",
"countMethodName": "countByCondition",
"searchParams": [
{
"nickname": "cc"// 搜尋昵稱為cc的使用者
}
],
"customHeads": [
{
"fieldName": "username",
"fieldNameZh": "帳號",
"width": 20, // 列寬
"fontSize": 20// 字型大小
},
{
"fieldName": "createTime",
"fieldNameZh": "註冊時間",
"type": "date(yyyy-MM-dd)"// 內容型別聲明為date,並且轉換成指定格式匯出
}
]
}
匯入樣版配置
{
"filename": "使用者數據匯入",
"modelClazz": "com.cc.entity.User",
"customHeads": [
{
"fieldName": "username",
"fieldNameZh": "帳號",
"width": 20, // 列寬
"fontSize": 20// 字型大小
},
{
"fieldName": "createTime",
"fieldNameZh": "註冊時間",
"type": "date(yyyy-MM-dd)"// 內容型別聲明為date,並且轉換成指定格式匯出
}
]
}
匯入數據配置
{
"modelClazz": "com.cc.entity.User",
"serviceClazz": "com.cc.service.UserService",
"methodName": "save",
"customHeads": [
{
"fieldName": "username",
"fieldNameZh": "帳號",
},
{
"fieldName": "createTime",
"fieldNameZh": "註冊時間",
"type": "date(yyyy-MM-dd)"// 內容型別聲明為date,並且轉換成指定格式匯出
}
]
}
🔥 磊哥私藏精品 熱門推薦 🔥