`ItemWriter`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. |
An ItemWriter receives the whole chunk at once, which is what lets it write efficiently — one batched
statement, one flush, one file append — instead of one operation per item.
The contract
public interface ItemWriter<T> {
void write(Chunk<? extends T> chunk) throws Exception;
}
Chunk<T> is the list of items the reader and processor produced for this transaction. Two consequences:
-
the writer must handle a chunk of any size, including one — during a skip scan a fault-tolerant step writes items one at a time (Fault tolerance: skip & retry);
-
the writer is the right home for side effects, because it runs once per chunk inside the chunk’s transaction (ItemProcessors).
ItemStreamWriter<T> adds the ItemStream callbacks. Every file writer implements it, because the current
file position has to be saved at each commit point for a restart to append rather than overwrite.
FlatFileItemWriter
Writing a flat file is the mirror image of reading one:
object to Object[]"] FE --> LA["LineAggregator
Object[] to one line of text"] LA --> File["Resource
one line appended"]
@Bean
@StepScope
public FlatFileItemWriter<Trade> tradeCsvWriter(
@Value("#{jobParameters['output.file']}") WritableResource outputFile) {
return new FlatFileItemWriterBuilder<Trade>()
.name("tradeCsvWriter")
.resource(outputFile)
.delimited()
.delimiter(",")
.names("externalId", "isin", "quantity", "price", "notional")
.headerCallback(writer -> writer.write("externalId,isin,quantity,price,notional"))
.shouldDeleteIfExists(true)
.build();
}
.delimited() builds a DelimitedLineAggregator over a BeanWrapperFieldExtractor that reads the named
properties. For fixed-width output use .formatted() with a java.util.Formatter pattern:
@Bean
public FlatFileItemWriter<Trade> tradeReportWriter() {
return new FlatFileItemWriterBuilder<Trade>()
.name("tradeReportWriter")
.resource(new FileSystemResource("/data/out/trades.txt"))
.formatted()
.format("%-12s%-14s%10d%12.2f")
.names("externalId", "isin", "quantity", "price")
.build();
}
A custom FieldExtractor takes over when the output columns are computed rather than read straight off the
object:
public class TradeFieldExtractor implements FieldExtractor<Trade> {
@Override
public Object[] extract(Trade trade) {
return new Object[] {
trade.getExternalId(),
trade.getInstrument().getIsin(),
trade.getQuantity(),
trade.getPrice(),
trade.getQuantity().multiply(trade.getPrice()) // computed
};
}
}
And a custom LineAggregator takes over when the line format is not "fields joined by something".
Headers and footers
FlatFileHeaderCallback writes before the first item; FlatFileFooterCallback writes after the last one, and
is where control totals belong. A footer usually needs state gathered during the step, so it also implements
StepExecutionListener:
public class TotalsFooterCallback implements FlatFileFooterCallback, StepExecutionListener {
private StepExecution stepExecution;
@Override
public void beforeStep(StepExecution stepExecution) {
this.stepExecution = stepExecution;
}
@Override
public void writeFooter(Writer writer) throws IOException {
writer.write("TRL" + String.format("%09d", stepExecution.getWriteCount()));
}
}
Register the callback both on the writer (.footerCallback(…)) and as a step listener, so beforeStep is
called.
On restart, the header callback would otherwise run again and write a second header. FlatFileItemWriter
handles this by tracking the written position in the ExecutionContext and appending — which is exactly why
.name(…) and stream registration matter.
File-management flags
| Flag | Effect |
|---|---|
|
Delete an existing output file when the step starts fresh. Prevents stale data from a previous run. |
|
Remove the file at the end if nothing was written — no empty files for downstream systems to misinterpret. |
|
Append to an existing file instead of deleting it; implies not writing a header. Mutually exclusive with |
|
Buffer the chunk’s lines and flush them only on commit, so a rolled-back chunk leaves nothing behind. Set |
|
|
|
Do not record the position; the writer becomes non-restartable but slightly cheaper (see Profiling & tuning). |
XML with StaxEventItemWriter
@Bean
public StaxEventItemWriter<Trade> xmlTradeWriter() {
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
marshaller.setClassesToBeBound(Trade.class);
return new StaxEventItemWriterBuilder<Trade>()
.name("xmlTradeWriter")
.resource(new FileSystemResource("/data/out/trades.xml"))
.marshaller(marshaller)
.rootTagName("trades")
.overwriteOutput(true)
.build();
}
The root tag is opened once and closed at the end, so the file is well-formed even though it was streamed.
JSON with JsonFileItemWriter
@Bean
public JsonFileItemWriter<Trade> jsonTradeWriter() {
return new JsonFileItemWriterBuilder<Trade>()
.name("jsonTradeWriter")
.resource(new FileSystemResource("/data/out/trades.json"))
.jsonObjectMarshaller(new JacksonJsonObjectMarshaller<>())
.build();
}
The output is a JSON array: an opening bracket, comma-separated objects, a closing bracket.
Rolling output over several files
MultiResourceItemWriter starts a new file every itemCountLimitPerResource items, using a
ResourceSuffixCreator for the numbering. It keeps a single output file from growing past what downstream
systems can ingest:
@Bean
public MultiResourceItemWriter<Trade> rollingTradeWriter(FlatFileItemWriter<Trade> tradeCsvWriter) {
MultiResourceItemWriter<Trade> writer = new MultiResourceItemWriter<>();
writer.setName("rollingTradeWriter");
writer.setResource(new FileSystemResource("/data/out/trades")); // trades1, trades2, ...
writer.setDelegate(tradeCsvWriter);
writer.setItemCountLimitPerResource(100_000);
writer.setResourceSuffixCreator(index -> "-" + index + ".csv");
return writer;
}
Composing writers
CompositeItemWriter sends every chunk to each delegate in turn — write to the database and an audit file:
@Bean
public CompositeItemWriter<Trade> auditingWriter(JdbcBatchItemWriter<Trade> databaseWriter,
FlatFileItemWriter<Trade> auditFileWriter) {
CompositeItemWriter<Trade> composite = new CompositeItemWriter<>();
composite.setDelegates(List.of(databaseWriter, auditFileWriter));
return composite;
}
ClassifierCompositeItemWriter routes each item to one delegate instead:
@Bean
public ClassifierCompositeItemWriter<Trade> routingWriter(ItemWriter<Trade> domesticWriter,
ItemWriter<Trade> foreignWriter) {
ClassifierCompositeItemWriter<Trade> writer = new ClassifierCompositeItemWriter<>();
writer.setClassifier(trade -> trade.isDomestic() ? domesticWriter : foreignWriter);
return writer;
}
In both cases the delegates are hidden from the step, so any delegate that implements ItemStream must be
registered with .stream(…) — see
Chunk-oriented processing.