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