本人使用SuperCsv进行Csv文件导出时,在进行表头写入时发生了以下异常。
java.lang.StringIndexOutOfBoundsException: null
at java.lang.AbstractStringBuilder.delete(AbstractStringBuilder.java:760)
at java.lang.StringBuilder.delete(StringBuilder.java:244)
at org.supercsv.encoder.DefaultCsvEncoder.encode(DefaultCsvEncoder.java:42)
at org.supercsv.io.AbstractCsvWriter.escapeString(AbstractCsvWriter.java:102)
at org.supercsv.io.AbstractCsvWriter.writeRow(AbstractCsvWriter.java:196)
at org.supercsv.io.AbstractCsvWriter.writeHeader(AbstractCsvWriter.java:228)
我之前是这么用的
...
ICsvListWriter csvWrite = new CsvListWriter(writer,CsvPreference.STANDARD_PREFERENCE)
...
csvWrite.writeHeader(xxx); //在此处抛了上述异常
...
经过分析发现,异常的抛出点是在 AbstractStringBuilder.delete()方法中,看到这个StringBuilder我们应该一下就能想到是它线程不安全的,那么我们继续往下看。
在声明CsvWriter对象时,我们传入的CsvPreference.STANDARD_PREFERENCE 其实是一个定义在CsvPreference中的一个final类型的静态常量,那么多线程环境下使用CsvWriter导出Csv文件时其实操作的都是同一个CsvPreference对象。
//这是CsvPreference类的代码
/*
* @author Kasper B. Graversen
* @author James Bassett
*/
public final class CsvPreference {
/**
* Ready to use configuration that should cover 99% of all usages.
*/
public static final CsvPreference STANDARD_PREFERENCE = new CsvPreference.Builder('"', ',', "\r\n").build();
/**
* Ready to use configuration for Windows Excel exported CSV files.
*/
public static final CsvPreference EXCEL_PREFERENCE = new CsvPreference.Builder('"', ',', "\n").build();
/**
* Ready to use configuration for north European excel CSV files (columns are separated by ";" instead of ",")
*/
public static final CsvPreference EXCEL_NORTH_EUROPE_PREFERENCE = new CsvPreference.Builder('"', ';', "\n").build();
...
}
这样一看似乎就有点眉目了,再看一下CsvWriter的构造方法
public CsvListWriter(final Writer writer, final CsvPreference preference) {
super(writer, preference);//调用了父类的构造方法
}
...
//看一下它的父抽象类的构造方法
public AbstractCsvWriter(final Writer writer, final CsvPreference preference) {
if( writer == null ) {
throw new NullPointerException("writer should not be null");
} else if( preference == null ) {
throw new NullPointerException("preference should not be null");
}
this.writer = new BufferedWriter(writer);
this.preference = preference;
this.encoder = preference.getEncoder();
}
CsvWriter 写入表头时会调用 encoder.encode()方法,那么CsvWriter使用的encoder其实就是定义在CsvPreference 内部的encoder,上面说过了多线程下操作的都是同一个CsvPreference ,即操作的是同一个encoder
我们继续来看一下这个encode方法
public class DefaultCsvEncoder implements CsvEncoder {
private final StringBuilder currentColumn = new StringBuilder();
/**
* Constructs a new <tt>DefaultCsvEncoder</tt>.
*/
public DefaultCsvEncoder() {
}
/**
* {@inheritDoc}
*/
public String encode(final String input, final CsvContext context, final CsvPreference preference) {
currentColumn.delete(0, currentColumn.length()); // reusing builder object
final int delimiter = preference.getDelimiterChar();
final char quote = (char) preference.getQuoteChar();
final String eolSymbols = preference.getEndOfLineSymbols();
final int lastCharIndex = input.length() - 1;
boolean quotesRequiredForSpecialChar = false;
boolean skipNewline = false;
...
}
}
encode方法的一开始就执行了 currentColumn.delete(),我们可以看到这个currentColumn 对象实际上就是定义在encoder内部的一个final类型的StringBuilder对象,也正是因为如此,在多线程的情况下,使用这种方法导出Csv文件是有线程安全问题的
解决方案就是,在创建CsvWriter时,传入我们自己构造出来的CsvPreference 对象即可,例如:
ICsvListWriter csvWrite = new ICsvListWriter (writer,new CsvPreference.Builder(CsvPreference.STANDARD_PREFERENCE).useEncoder(new DefaultCsvEncoder()).build())
好了,问题解决