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