首页 > 技术文章 > 解决POI多线程导出时数据错乱问题

GodTestLove 2022-02-22 12:04 原文

项目里有一个导出功能,但随着数据量大量上涨,导出时间长到不可忍受,遂重写此接口,多线程导出的代码并不复杂,每页有一条线程负责写入,利用线程池去调度,用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));
                    }

                }

至此,问题解决.

推荐阅读