Java FlatFileItemReader性能分析
FlatFileItemReader经过测试我发现,FlatFileItemReader的性能随着文件列数的增长会变得很差。
究其根源,DefaultFieldSet中的indexOf(String name)实现是主要元凶。
简单来讲,通常我们会用以下的Code来把一个文件里的内容,读成一个个对象。
//File Header
private static final String[] names = "a,b,c".split(",");
DelimitedLineTokenizer t = new DelimitedLineTokenizer();
t.setDelimiter("|".charAt(0));
t.setNames(names);
DefaultLineMapper<A> lm = new DefaultLineMapper<A>();
lm.setLineTokenizer(t);
FieldSetMapper<A> fm = new FieldSetMapper<A>(){
@Override
public A mapFieldSet(FieldSet fieldSet)
throws BindException {
A a = new A();
//fieldSet.readString 是造成性能问题的关键所在
a.setA(fieldSet.readString("a"));
a.setB(fieldSet.readString("b"));
a.setC(fieldSet.readString("c"));
return a;
}
};
lm.setFieldSetMapper(fm);
FlatFileItemReader<A> reader = new FlatFileItemReader<A>();
reader.setLineMapper(lm);
reader.setLinesToSkip(1);
reader.setResource(new FileSystemResource(file));
ExecutionContext ex = new ExecutionContext();
reader.open(ex);
A a = null;
while((a = reader.read())!=null){
}
众所周知,FlatFileReader主要有两部分构成,一部分是LineTokenizer,另外就是FieldSetMapper。
LineTokenizer主要负责返回一个FieldSet对象。以其默认实现DefaultFieldSet为例,该对象主要包含两个String数组,一个是该行内容构成的数组,一个是由程序传递给LineTokenizer的Head Names数组(ArrayList)。
而FieldSetMapper主要用于把FieldSet对象转换成你想要的POJO对象。
public interface LineTokenizer {
/**
* Yields the tokens resulting from the splitting of the supplied
* <code>line</code>.
*
* @param line the line to be tokenized (can be <code>null</code>)
*
* @return the resulting tokens
*/
FieldSet tokenize(String line);
}
public interface FieldSetMapper<T> {
/**
* Method used to map data obtained from a {@link FieldSet} into an object.
*
* @param fieldSet the {@link FieldSet} to map
* @throws BindException if there is a problem with the binding
*/
T mapFieldSet(FieldSet fieldSet) throws BindException;
}
public class DefaultLineMapper<T> implements LineMapper<T>, InitializingBean {
private LineTokenizer tokenizer;
private FieldSetMapper<T> fieldSetMapper;
public T mapLine(String line, int lineNumber) throws Exception {
return fieldSetMapper.mapFieldSet(tokenizer.tokenize(line));
}
....
}
然而,如果用我最开始的那种Code来使用FlatFileItemReader将FieldSet转换POJO对象就会导致性能问题。
这里关键在与FieldSet的默认实现DefaultFieldSet的readXXX(String name)方法实现。
当调用这个方法时,它会做两件事,首先调用indexOf(String name)方法在包含Header的ArrayList中根据name找到index,然后再调用readXXX(int index)方法在内容数组上返回tokens[index].
这个方法是o(n)的。这样读完一行就是o(n2)的效率了。
要处理这样的性能问题,目前我可以想到两种方法,一种是实现自己的LineTokenizer和FieldSet实现。
第二种就是把Names和其对应的Index关系在调用的一开始就维护一个map。
在mapFieldSet(FieldSet fieldSet)方法中直接调用fieldSet.readXXX(int index)方法,这样读取每一行就变为o(n)的操作了。