`ItemReader`s: files & XML/JSON
|
This section documents the current Spring Batch line — 6.0.x, on Spring Framework 7 and Spring Boot 4.1.x, with a Java 17+ baseline — as published at the Spring Batch reference documentation. No specific patch version is pinned. Some surfaces (Spring Cloud Task and Spring Cloud Data Flow orchestration, the deployer-based partition handler, and JSR-352) are linked, not documented in depth. This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. |
Most batch input arrives as a file. Spring Batch supplies readers for delimited and fixed-length flat files, XML and JSON, and a contract for writing your own.
The ItemReader contract
public interface ItemReader<T> {
T read() throws Exception; // returns the next item, or null when exhausted
}
read() returns one item per call and null exactly once, to signal the end of input — that null ends
the step. Returning null early therefore truncates the job, which is a common bug in hand-written readers.
ItemStreamReader<T> adds the ItemStream callbacks (open, update, close) that make a reader
restartable: update writes the current position into the step ExecutionContext at every commit point, and
open reads it back on restart. Every framework reader implements it. See
Steps, executions & the ExecutionContext for the
contract and for registering delegate streams.
FlatFileItemReader
Reading a flat file is a chain of four responsibilities:
one line of text"] --> LM["LineMapper"] LM --> LT["LineTokenizer
split the line"] LT --> FS["FieldSet
typed positional/named access"] FS --> FSM["FieldSetMapper
build the domain object"] FSM --> Item["item"]
FlatFileItemReaderBuilder assembles all four for the common cases:
@Bean
@StepScope
public FlatFileItemReader<TradeCsv> tradeReader(
@Value("#{jobParameters['input.file']}") Resource inputFile) {
return new FlatFileItemReaderBuilder<TradeCsv>()
.name("tradeReader") // required: the ExecutionContext key prefix
.resource(inputFile)
.linesToSkip(1) // a header row
.delimited()
.delimiter(",")
.quoteCharacter('"')
.names("externalId", "isin", "quantity", "price", "tradeDate")
.targetType(TradeCsv.class) // maps by property name
.strict(true) // fail if the resource is missing
.build();
}
.name(…) is mandatory for a restartable reader — it is the key under which the line count is saved.
A fixed-length file uses column ranges instead of a delimiter:
@Bean
public FlatFileItemReader<Customer> fixedWidthCustomerReader() {
return new FlatFileItemReaderBuilder<Customer>()
.name("fixedWidthCustomerReader")
.resource(new ClassPathResource("customers.txt"))
.fixedLength()
.columns(new Range(1, 10), new Range(11, 40), new Range(41, 42), new Range(43, 52))
.names("customerId", "name", "countryCode", "balance")
.targetType(Customer.class)
.build();
}
A custom FieldSetMapper
targetType(…) uses a BeanWrapperFieldSetMapper under the covers, which needs a no-argument constructor
and setters. For conversion, validation or an immutable type, write the mapper:
public class TradeFieldSetMapper implements FieldSetMapper<Trade> {
@Override
public Trade mapFieldSet(FieldSet fieldSet) {
return new Trade(
fieldSet.readString("isin"),
fieldSet.readLong("quantity"),
fieldSet.readBigDecimal("price"),
LocalDate.parse(fieldSet.readString("tradeDate")));
}
}
.delimited()
.names("isin", "quantity", "price", "tradeDate")
.fieldSetMapper(new TradeFieldSetMapper())
FieldSet is the typed accessor over the tokenized line: readString, readInt, readLong,
readBigDecimal, readBoolean, readDate, each by name or index.
Files with several record formats
A file that mixes header, detail and trailer records needs a LineMapper that chooses a tokenizer per line
prefix:
@Bean
public FlatFileItemReader<Object> mixedRecordReader() {
PatternMatchingCompositeLineMapper<Object> lineMapper =
new PatternMatchingCompositeLineMapper<>();
lineMapper.setTokenizers(Map.of(
"HDR*", headerTokenizer(),
"DTL*", detailTokenizer(),
"TRL*", trailerTokenizer()));
lineMapper.setFieldSetMappers(Map.of(
"HDR*", new HeaderFieldSetMapper(),
"DTL*", new DetailFieldSetMapper(),
"TRL*", new TrailerFieldSetMapper()));
return new FlatFileItemReaderBuilder<Object>()
.name("mixedRecordReader")
.resource(new FileSystemResource("/data/in/mixed.txt"))
.lineMapper(lineMapper)
.build();
}
The patterns use the same */? wildcards as flow transitions.
Multi-line records
When one logical item spans several physical lines, wrap the flat-file reader in a reader that assembles them. The typical shape is a delegate plus a loop that reads until the record’s terminator:
public class TradeGroupReader implements ItemStreamReader<TradeGroup> {
private final FlatFileItemReader<Object> delegate;
public TradeGroupReader(FlatFileItemReader<Object> delegate) {
this.delegate = delegate;
}
@Override
public TradeGroup read() throws Exception {
TradeGroup group = null;
for (Object line = delegate.read(); line != null; line = delegate.read()) {
if (line instanceof Header header) {
group = new TradeGroup(header);
} else if (line instanceof Detail detail && group != null) {
group.add(detail);
} else if (line instanceof Trailer && group != null) {
return group; // record complete
}
}
return group; // last record, or null at end of file
}
@Override public void open(ExecutionContext context) { delegate.open(context); }
@Override public void update(ExecutionContext context) { delegate.update(context); }
@Override public void close() { delegate.close(); }
}
Delegating open/update/close is what keeps the composite restartable.
Reading a directory of files
MultiResourceItemReader runs one delegate over many resources, tracking which file and which line it is on:
@Bean
@StepScope
public MultiResourceItemReader<TradeCsv> multiFileReader(
@Value("#{jobParameters['input.dir']}") String inputDir,
FlatFileItemReader<TradeCsv> tradeReader) throws IOException {
Resource[] resources = new PathMatchingResourcePatternResolver()
.getResources("file:" + inputDir + "/trades-*.csv");
MultiResourceItemReader<TradeCsv> reader = new MultiResourceItemReader<>();
reader.setName("multiFileReader");
reader.setResources(resources);
reader.setDelegate(tradeReader);
reader.setStrict(true);
return reader;
}
Sort the resources deterministically (the default comparator is by filename) so a restart sees the same order.
XML with StaxEventItemReader
StAX-based, so the document is streamed rather than loaded. The fragment root element names the item boundary,
and an Unmarshaller (JAXB here) turns each fragment into an object:
@Bean
public StaxEventItemReader<Trade> xmlTradeReader() {
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
marshaller.setClassesToBeBound(Trade.class);
return new StaxEventItemReaderBuilder<Trade>()
.name("xmlTradeReader")
.resource(new FileSystemResource("/data/in/trades.xml"))
.addFragmentRootElements("trade")
.unmarshaller(marshaller)
.build();
}
JSON with JsonItemReader
JsonItemReader streams a JSON array of objects through a JsonObjectReader. The Jackson-backed
implementation is JacksonJsonObjectReader, which in the current line uses Jackson 3:
@Bean
public JsonItemReader<Trade> jsonTradeReader() {
return new JsonItemReaderBuilder<Trade>()
.name("jsonTradeReader")
.resource(new FileSystemResource("/data/in/trades.json"))
.jsonObjectReader(new JacksonJsonObjectReader<>(Trade.class))
.build();
}
A newline-delimited JSON file is not a JSON array; read it with a FlatFileItemReader whose LineMapper
deserialises each line instead.
Custom readers
Implement ItemStreamReader when the source is neither a file nor a database — a REST endpoint, a queue
drain, a proprietary API. Two rules cover most mistakes: return null only at true end of input, and save
enough state in update to resume. The watermark example in
Steps, executions & the ExecutionContext is a
complete template.
Dealing with no input
A reader that returns null on the first call produces a step that completes with readCount = 0. Whether
that is success or failure is a business decision — attach a NoWorkFoundStepExecutionListener to fail it, or
return a custom exit code and branch on it. Both are covered in
Stopping, restart & recovery.