Reading data from source like flat files and databases are common theme in batch projects. Spring Batch provides a lot of support for reading from these data sources out of the box. The FlatFileItemReader<T> provides a great example. This class extends the abstract AbstractItemCountingItemStreamItemReader<T>, which for one reason or another, I wanted to implement myself for reading a flat file. Extensions of this inteface must implement three methods:
- doClose()
- doOpen()
- doRead()
Since I was implementing the interface myself, I had to come up with a way to actually read the contents of the file myself. In the Google Guava project, I found a class to use, LineReader, that seemed like a good candidate to help alleviate the need for the usual tedious boilerplate code required for reading the contents of the file.
Before describing the use of the LineReader in a Spring Batch project, I wanted to demonstrate an example of using the LineReader outside of the context of a Spring Batch project. The Maven coordinates for the current version of Guava are:
- groupId: com.google.guava
- artifactId: guava
- version: 10.0.1
Below is a sample class that follows the same pattern that must be implemenented when extending the AbstractItemCountingItemStreamItemReader class using a LineReader. The class takes the name of a resource to read from and handles making the resource available to the LineReader in the doOpen() method. Reading from the resource and closing of the resource are handled by the doRead() and doClose() methods respectively.
package prystasj.sample.guava;
import com.google.common.io.LineReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
public class ResourceLineReader {
private InputStream inputStream;
private LineReader lineReader;
public String doRead() throws Exception {
return lineReader.readLine();
}
public void doOpen(String resourceName) throws Exception {
inputStream = getClass().getClassLoader().getResourceAsStream(resourceName);
Reader reader = new InputStreamReader(inputStream);
lineReader = new LineReader(reader);
}
public void doClose() throws Exception {
inputStream.close();
}
}
To test the above class, we'll use a file, tests.txt that contains three lines:
line1
line2
line3
As the doRead() method, by way of LineReader.readLine(), will return null when the data from the input resource has been exhausted, we can use that fact to know when to call the doClose() method. At the end of our test we have a check to verify that three lines were read from the file:
package prystasj.sample.guava;
import org.junit.Before;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
public class ResourceLineReaderTest {
ResourceLineReader resourceLineReader;
@Before
public void setup() {
resourceLineReader = new ResourceLineReader();
}
@Test
public void reads_lines_from_a_classpath_resource() {
List<String> itemsRead = new ArrayList<String>();
String itemRead;
try {
resourceLineReader.doOpen("test.txt");
while((itemRead = resourceLineReader.doRead()) != null) {
itemsRead.add(itemRead);
}
resourceLineReader.doClose();
}
catch(Exception e) {
fail(e.getMessage());
}
assertEquals(3, itemsRead.size());
}
}
We can use the same implementation pattern when extending the the AbstractItemCountingItemStreamItemReader<T> abstract class as mentioned earlier in this post:
package prystasj.sample.batch;
import com.google.common.io.LineReader;
import org.springframework.batch.item.file.ResourceAwareItemReaderItemStream;
import org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader;
import org.springframework.core.io.Resource;
import org.springframework.util.ClassUtils;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
class BatchFileReader extends AbstractItemCountingItemStreamItemReader<String> implements ResourceAwareItemReaderItemStream<String> {
private InputStream inputStream;
private LineReader lineReader;
private Resource fileResource;
/** Fulfills execution context requirement that {@code name} is set for the keys prefix. */
public BibNumbersFileReader() {
setName(ClassUtils.getShortName(BibNumbersFileReader.class));
}
@Override
protected String doRead() throws Exception {
return lineReader.readLine();
}
@Override
protected void doOpen() throws Exception {
inputStream = fileResource.getInputStream();
Reader reader = new InputStreamReader(inputStream);
lineReader = new LineReader(reader);
}
@Override
protected void doClose() throws Exception {
inputStream.close();
inputStream = null;
}
@Override
public void setResource(Resource resource) {
this.fileResource = resource;
}
}
The above reader can be configured with the below definition to be referenced from within a batch job:
<bean id="bibNumbersFileReader" class="prystasj.sample.batch.BatchFileReader" scope="step"
p:resource="file:#{jobParameters['file']}"/>
In the above configuration, the scope of step ensures the bean is reconfigured every time the reader is used within a job. Spring Expression Language is used to inject the the name of the file resource into the reader from the job's parameters.