解決POI多線程導出時數據錯亂問題


項目里有一個導出功能,但隨着數據量大量上漲,導出時間長到不可忍受,遂重寫此接口,多線程導出的代碼並不復雜,每頁有一條線程負責寫入,利用線程池去調度,用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));
                    }

                }

至此,問題解決.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM