解決POI多執行緒匯出時資料錯亂問題

煢祇發表於2022-02-22

專案裡有一個匯出功能,但隨著資料量大量上漲,匯出時間長到不可忍受,遂重寫此介面,多執行緒匯出的程式碼並不複雜,每頁有一條執行緒負責寫入,利用執行緒池去排程,用countdownLatch保證在所有資料寫完後再寫入檔案。修改後,匯出所有資料時間限制在了一分鐘以內。但是由於poi自身為了資源高效利用,同一個workbook裡的cell,setCellValue採用的是同一個SharedStringTable物件,由於多個執行緒同時使用而沒有加以限制,因此產生了執行緒不安全的問題。
有三種解決的辦法

  1. 獲取poi原始碼,更改為執行緒安全後重新打包替換
  2. 在呼叫setCellValue的時候獲取到SharedStringTable物件,然後加鎖
    前兩種方法可以參考【這個連結](https://blog.csdn.net/vatrenoludilo/article/details/121951681)
    第一個方法我從GitHub上把原始碼下下來後報了一堆莫名其妙的錯,就放棄了
    第二種方法對setCellValue加鎖,由於要匯出的excel列很多,而且很多列需要單獨處理,所以要麼加鎖粒度大,要麼加鎖程式碼負責,這都是我不想要的
    再來仔細分析一下問題
    是因為SharedStringTable類的addEntry()沒有加鎖導致的,既然不能修改原始碼,那麼能不能繼承這個類,然後在子類加鎖,最後把原來使用的物件換成子類的物件。
    子類的實現很簡單
/**
 * @author TestLove
 * @version 1.0
 * @date 2022/2/21 22:25
 * @Description: null
 */
public class CustomSharedStringsTable extends SharedStringsTable {
    @Override
    public synchronized int addSharedStringItem(RichTextString string){
        return super.addSharedStringItem(string);
    }

}

如何替換呢?利用反射,在workbook類中,SharedStringTable物件的名字叫sharedStringTable,

  Field field = workBook.getClass().getDeclaredField("sharedStringSource");
  field.setAccessible(true);
  field.set(workBook,customSharedStringsTable);

但是僅僅這樣替換是不夠的,雖然能匯出,但匯出的檔案無法開啟。
於是繼續看原始碼,sharedStringTable這個物件到底是怎麼來的
從workbook的構造方法開始看,一層層呼叫後最後落腳點在onWorkbookCreate這個私有方法

private void onWorkbookCreate() {
        workbook = CTWorkbook.Factory.newInstance();

        // don't EVER use the 1904 date system
        CTWorkbookPr workbookPr = workbook.addNewWorkbookPr();
        workbookPr.setDate1904(false);

        setBookViewsIfMissing();
        workbook.addNewSheets();

        POIXMLProperties.ExtendedProperties expProps = getProperties().getExtendedProperties();
        expProps.getUnderlyingProperties().setApplication(DOCUMENT_CREATOR);

        sharedStringSource = (SharedStringsTable)createRelationship(XSSFRelation.SHARED_STRINGS, this.xssfFactory);
        stylesSource = (StylesTable)createRelationship(XSSFRelation.STYLES, this.xssfFactory);
        stylesSource.setWorkbook(this);

        namedRanges = new ArrayList<>();
        namedRangesByName = new ArrayListValuedHashMap<>();
        sheets = new ArrayList<>();
        pivotTables = new ArrayList<>();
    }

createRelationship(XSSFRelation.SHARED_STRINGS, this.xssfFactory),這一句返回的是POIXMLDocumentPart物件,但SharedStringTable繼承了這個類,因此可以進行型別轉換.
觀察其他的方法名,我們可以發現有getRelationByID這一類的方法,點進去發現返回值從一個map中來,
於是猜想,需要把這個map裡儲存的value一併給替換掉,才能保證一致性,使檔案能夠正常開啟.但目前又不知道id究竟是什麼,於是繼續採用反射獲取到map,並列印出裡面的內容.注意,這裡的value並不是POIXMLDocumentPart而是POIXMLDocumentPart.RelationPart,所以說還要經過一步轉換才能獲取到想要的物件
但是隻是把customSharedStringtable設定到map裡會導致寫入檔案時報空指標,猜想是一些屬性沒有設定的緣故,於是利用反射,把原來的欄位複製到當前物件的欄位中.

for (Field declaredField1 : declaredFields1) {
                System.out.println(declaredField1.getName());
                for (Field declaredField : declaredFields) {
                    declaredField1.setAccessible(true);
                    declaredField.setAccessible(true);
                    if(declaredField1.getName().equals(declaredField.getName())
                            &&!declaredField.getName().equals("logger")){

                        declaredField.set(customSharedStringsTable,declaredField1.get(documentPart1));
                    }

                }

至此,問題解決.

相關文章