當前位置: 妍妍網 > 碼農

如何設計一個通用的 Excel 匯入匯出功能?

2023-06-23碼農

大家好,我是鵬磊。

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,並且轉換成指定格式匯出
    }
    ]
    }

    🔥 磊哥私藏精品 熱門推薦 🔥