解決POI多執行緒導出時數據錯亂問題

  • 2022 年 2 月 22 日
  • 筆記

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

  1. 獲取poi源碼,更改為執行緒安全後重新打包替換
  2. 在調用setCellValue的時候獲取到SharedStringTable對象,然後加鎖
    前兩種方法可以參考【這個鏈接](//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));
                    }

                }

至此,問題解決.

Tags: