土製Excel導入導出及相關問題探討

轉載請註明出處http://www.javashuo.com/article/p-nnslmdhz-gk.html

新的一年,又一個開始,不見收穫,卻見年齡,好一個豬年,待我先來一首里爾克的詩:html

《沉重的時刻》(里爾克)java

此刻有誰在世上某處哭,平白無故在世上哭,在哭我。
    此刻有誰在夜間某處笑,平白無故在夜間笑,在笑我。
    此刻有誰在世上某處走,平白無故在世上走,走向我。
    此刻有誰在世上某處死,平白無故在世上死,望着我。

ok,此次說說項目中常常用到的Excel導出問題,目前就用到的能夠操做Excel的技術(在java中)大體有兩類:

  • JXL
    • 僅僅支持對xls的文件讀寫,
    • 僅包含Excel基礎api,比較老,好久不更新
    • 讀寫速度還行,對於要求低同時兼容性較好的推薦
  • POI
    • 可支持xls、xlsx兩種格式的Excel文件讀寫
      • HSSF:操做Excel 97(.xls)格式
      • XSSF:操做Excel 2007 OOXML (.xlsx)格式,操做EXCEL內存佔用高於HSSF
      • SXSSF: 從POI3.8 beta3開始支持,基於XSSF,低內存佔用。
    • 技術較新,保留了最大兼容性,可對Excel作複雜對數據極樣式處理
    • 讀寫速度上 SXSSF快於XSSF ,HSSF速度同略遜於JXL

以上對於這兩種技術作了簡要對描述,在開發中,咱們通常將POI做爲首選,同時以上還可能存在一個問題是:大數據量導出。大數據導出,通常咱們須要解決兩個問題:

  • 大數據量讀寫容易形成內存不足問題
  • 長時讀寫容易形成客戶端請求超時,形成導出失敗問題
  • 大數據量處理耗時問題

對於以上幾個問題,解決思路大體有下:

  • 數據庫數據查詢階段建議使用fetch分批次查詢,減小數據庫壓力
  • 單個文件讀寫建議使用SXSSF,以減小內存佔用
  • 對於單個sheet超過十萬的建議分sheet作多線程寫入,這裏分享一個網友寫的Demo
  • 對於POI寫入效率的問題官方給了個Demo,這個例子大體是使用xml文檔拼接的方式+xml文件壓縮的方式

ok,對於以上核心問題我的都嘗試過,憚於目前項目進度較爲緊張,我的作了個限制導出處理,以免(客戶端請求)超時問題,說白了就是將問題扔給下一期去集中解決。

不考慮以上所說的問題,我的花了些許時間寫了兩個Excel 工具類:ExcelReadUtil以及ExcelWriteUtil (代碼地址見篇尾),集中處理了包含單不只限於如下問題:git

  • 對象列表("List ")類型數據導出問題
  • 數據分sheet問題
  • 表頭字體極單元格內換行問題
  • 數據單元格多樣式問題
  • 數據單元格多數據類型及格式化問題
  • 單元格列寬度調整問題
  • 大數據拆分問題
  • 通用對象導出問題(使用泛型)

固然,解決這些問題也查閱了很多官方資料,同時也作了大量的測試才得以投入正式項目使用,在這中間也跳過一些坑,接下來我就講一講我在開發所跳過的坑,這些坑均是相對於上一個版本而言的:

  • 使用通用泛型接收參數對象問題
    一開始(在上一個項目)作了個初稿,在調用導出方法時直接傳入固定類型對象,一開始這樣:
toXlsxByMap(List<Map<String,Object>> dataList,
                                  String[] headerNames,
                                  String[] cellNames,
                                  CellFmt[] cellFmts)

致使的問題是傳入的dataList內部的Map的value必須是Object類型,以後鑽研了下java泛型,使用這種方式輕鬆解決問題!github

public static <V extends Object> File toXlsxByMap(List<Map<String,V>> dataList,
                                  String[] headerNames,
                                  String[] cellNames,
                                  CellFmt[] cellFmts)
  • 分數據分sheet問題
    這個問題其實很簡單,就是先數據分組,而後循環每組數據時再createSheet,代碼片斷:
//數據分組
          List<List<Map<String, V>>> mData = splitMapList(dataList);
                    //循環每組數據 並建立sheet>寫單元格
                    for (List<Map<String, V>> subList : mData) {
                        //第一個sheet 參數(sheet名稱,sheet的序號)
                        sheet = workbook.createSheet(String.format("%s~%s",
                                (dataList.size() > DATA_SPLIT_GROP_SIZE ?
                                        mData.indexOf(subList) * DATA_SPLIT_GROP_SIZE + 1
                                        : 0) + "",
                                (dataList.size() > DATA_SPLIT_GROP_SIZE ?
                                        (mData.size() == (mData.indexOf(subList) + 1) ? dataList.size() : DATA_SPLIT_GROP_SIZE * (mData.indexOf(subList) + 1))
                                        : dataList.size()) + "")
                        );
                        LOGGER.info(">>>sheet name : {}",sheet.getSheetName());
                        PoiCellProcess.writeHeaderCell(sheet,headerCellStyle,headerNames);
                        PoiCellProcess.writeBodyCellByMap(sheet,bodyCellStyle,cellNames,subList, cellFmts);
                    }
  • 單元格內換行問題
    其實這是個小問題,只需給CellStyle設置一個setWrapText(true),大體邏輯這樣:
public static CellStyle headerCellStyle(SXSSFWorkbook wb){
        CellStyle headerStyle = wb.createCellStyle();
        //...some code
        //容許單元格內換行
        headerStyle.setWrapText(true);
        return  headerStyle;
    }
  • 單元格類型及格式處理問題數據庫

    這個問題其實分爲多個,並且密切相關,大體有這幾個:
    - 單元格樣式類
    - 單元格樣式類
    - 單元格數據類型
    - 單元格寫入數據格式apache

    可是,處理了這幾個問題其實還不夠完美
    至於不完美的緣由是什麼呢,一個是Excel數據格式與java數據格式不一致(這個體如今日期,長數字,小數的處理上),好比你要格式化的日期後爲「yyyy-mm-dd」 這種類型,
    可是在Excel中相近的格式類型只有這樣「yyyy/M/d」,若是強制單元格樣式類型爲「yyyy-mm-dd HH24:mi:ss」 其實也是能夠的,只不過會變成自定義格式,並且是Excel的自定義格式,
    具體以下圖:編程

另一個問題是單元格類型與編程語言的數據類型相異同時與poi所能提供的數據類型也相異,如圖:api


  • 列寬調整問題
    記得在初版的時候列寬問題其實並不重要,遂就作個了固定長度
    在第二版的時候爲了保證能夠動態調整列寬,就剔除了初版的固定長度處理,將長度數據做爲一個Integer數組傳入
    因爲第二版先期已經投入開發中了,再在方法裏面加入長度數組實感受不合適,因而,想了個用代碼作動態列寬,這裏實現的思路大體有下:
    • 因爲表頭也是做爲一個參數傳入的,因此將表頭字符個數做爲字段倍數長度,數據行過長時將表頭字段添加適當個數的空格便可(數據傳入的時候)
    • 實際顯示的時候因爲存在單元格內換行問題,因此在代碼處理的時候先判斷換行,因此:
      • 有換行時 單元格列寬=基準長度(本身定義的單字符長度)*字符個數/2
      • 無換行時 單元格列寬=基準長度(本身定義的單字符長度)*字符個數
        這是最終的代碼:
      public static void writeHeaderCell(SXSSFSheet sheet, CellStyle headerCellStyle, String[] headerNames) {
        SXSSFRow row = sheet.createRow(0);
        row.setHeight((short) 30);
        row.setHeightInPoints((short) 30);
        SXSSFCell headerCell;
        for (int i = 0; i < headerNames.length; i++) {
            headerCell = row.createCell(i);
            headerCell.setCellStyle(headerCellStyle);
            headerCell.setCellValue(headerNames[i]);
            sheet.setColumnWidth(i,
                    null == headerNames[i] ? CELL_BASE_LENGTH
                            : (headerNames[i].contains("\r\n") ? CELL_CHARSET_LENGTH * headerNames[i].length() / 2
                            : CELL_CHARSET_LENGTH * headerNames[i].length()));
        }
        }
      這是最終處理的結果:
  • 對象導出問題
    這個問題耗時較多,因爲個人同事所處理的源數據是這樣子 "List " ,這樣作其實有個很大的問題就是 java代碼無法動態針對不一樣對象作getter和setter處理,遂每個導出功能就須要單獨寫poi的導出邏輯,緩慢並且耽擱開發進度,這個時候在寫第二版的時候完全優化了,這裏的思路和注意事項大體有下:
    • 利用反射動態獲取字段數據,這裏是不得已而爲之(其實jvm作頻繁反射處理時並不慢)
    • 反射處理時必須要將最終數據排序,否則循環 Field[] 獲取到的數據結果並不必定與表頭字段數據一致
      這裏是最終代碼:
    public static Object[] fieldValues(final Object obj, final String[] fieldNames,Object[] valueList) {
          for (Class<?> superClass = obj.getClass(); superClass != Object.class; superClass = superClass.getSuperclass()) {
              try {
                  Field[] fields = superClass.getDeclaredFields();
                  for(int k=0;k<fields.length;k++){
                      if ((!Modifier.isPublic(fields[k].getModifiers()) ||
                              !Modifier.isPublic(fields[k].getDeclaringClass().getModifiers()) ||
                              Modifier.isFinal(fields[k].getModifiers())) &&
                              !fields[k].isAccessible()) {
                          fields[k].setAccessible(true);
                      }
                      /**
                       * 須要排序,不然順序不一致
                       */
                      for(int j=0;j<fieldNames.length;j++){
                          if(fields[k].getName().equals(fieldNames[j])){
                              valueList[j] = fields[k].get(obj);
                              break;
                          }
                      }
                  }
                  return valueList;
              } catch (Exception e) {
                  e.printStackTrace();
              }
          }
          //這裏新增一個,不然數組越界
          return new Object[fieldNames.length];
      }

    &最後

    先展現導出的效果:

    因爲導入並無作嚴格要求,因此將讀取的數據所有放入這種對象裏面 "List<Map<String,String>>",詳細請看代碼,這裏就不作詳細介紹了數組

    這裏共享下個人 「土製Excel導入導出」:
    代碼地址 : https://github.com/funnyzpc/excel-process
    筆記寫的略微簡單,建議使用前使用看下這兩個測試樣例:
  • 樣例 example

以上寫的過於粗糙,各位有更好的想法請分享下哈~多線程

如今是 2019-02-18 星期一,各位中午好~

相關文章
相關標籤/搜索