Spring Batch: Using JAXB And StaxEventItemWriter To Generate XML

Jonny Hackett Java, Spring Batch, Tutorial Leave a Comment

While working with a client recently, my team was given the task to retrieve the held securities and account data from the system and export it to XML with the goal of importing it into another external system. The solution involved building a Spring Batch job that would read a set held security data, export that to XML data, and deliver the file to the external vendor securely.

In this blog, I’ll be giving a tutorial on how to execute this solution. These requirements form the basis for the example we’ll use.

Process Overview

I’m going to take a list of contact data that would typically be stored in a database and convert that data into XML data (which will be saved to a file) using Spring Batch. This is a fairly common requirement in business applications, and the Spring Batch framework provides the StaxEventItemWriter that makes it easy to output XML data.

If you’re familiar with Spring Batch (or my earlier blog series, Intro to Spring Batch), a simple and typical job generally contains a step that has a reader and a writer. For this particular example, however, our job is going to have one step that implements a reader that provides the data, a processor to convert the data, and a writer to output the XML file.

Step 1: The Reader

To keep this example as simple as possible and focused on creating the XML output, the reader is a very simple ItemReader that creates a list of contacts in the beforeStep method and uses an IteratorItemReader to produce the results from the reader.

Here’s is the code for that reader.

public class ContactItemReader implements ItemReader<Contact> {

	private IteratorItemReader<Contact> delegateReader;

	@BeforeStep
	private void beforeStep(StepExecution stepExecution) {
		List<Contact> contacts = new ArrayList<>();
		contacts.add(createContact("John", "Smith", "[email protected]", "111-333-4444"));
		contacts.add(createContact("John", "Doe", "[email protected]", "111-543-1234"));
		contacts.add(createContact("Jane", "Doe", "[email protected]", "111-463-8583"));
		this.delegateReader = new IteratorItemReader<>(contacts);
	}

	@Override
	public Contact read() throws Exception {
		return this.delegateReader.read();
	}

	private Contact createContact(String firstName, String lastName, String emailAddress, String cellPhone) {
		return new Contact(firstName, lastName, emailAddress, cellPhone);

	}
}

Step 2: The Processor

Now that we have our reader created, we need to create a processor that will convert the Contact object into the object that will represent the XML output. To do this, we will create an object that uses the JAXB2.0 XML binding annotations to tell the marshaling engine how to map out the XML and values.

Here’s the code for that object. I’ll explain what the annotations mean afterward.

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType( propOrder = { "firstName", "lastName", "emailAddress", "cellPhone" })
@XmlRootElement(name = "Contact")
public class ContactXml {

	@XmlElement(name = "FirstName", required = true)
	protected String firstName;
	
	@XmlElement(name = "LastName", required = true)
	protected String lastName;
	
	@XmlElement(name = "EmailAddress", required = true)
	protected String emailAddress;

	@XmlElement(name = "CellPhone", required = true)
	protected String cellPhone;

	// Getters and setters removed for brevity.  Will be included in full code listing at the end of the article.

}

The Annotations Explained

  • @XmlAccessorType: This defines whether the fields and properties of a class will be serialized. In this example, I’ve set the value to XmlAccessType.FIELD, which means that every non-static, nontransient field will be automatically bound to XML – unless annotated by @XmlTransient. The names for the XML elements will be derived, by default, from the field names.
  • @XmlType: This allows us to define additional properties such as mapping the class to a specific schema, namespace, and specific order of the children. In this specific case, we’re only using it to define the particular order of elements.
  • @XmlRootElement: This is used to map the class to a specific root element. By default, it derives the root element tag from the class name. For this example, we’re specifying a different name.
  • @XmlElement: This maps a JavaBean property to an XML element derived from the property name by default.

For our purpose in this case, we’re specifying a specific name and requiring that the element is present. If we weren’t specifying a different name, this would not be required due to the @XmlAccessorType being set to FIELD.

Based on ContactXml object and it’s annotations, this is what the resulting XML will look like.

<?xml version="1.0" encoding="UTF-8"?>
<ContactList>
    <Contact>
        <FirstName>John</FirstName>
        <LastName>Smith</LastName>
        <EmailAddress>[email protected]</EmailAddress>
        <CellPhone>111-333-4444</CellPhone>
    </Contact>
</ContactList>

Step 3: The Writer

Now that we’ve created the ContactItemReader and ContactItemProcessor, the final step is to configure the ItemWriter that will produce the XML file as output. To do this, we’ll create an ItemWriter bean in the batch job configuration. The StaxEventItemWriter uses StaX and a Marshaller to serialize an object to XML.

We’ve already done most of the work for this batch job. There’s only a few little things we really need to configure. The first is to set up the Marshaller to be bound to the ContactXml class we created with the JAXB2.0 annotations. Next, we need to build the StaxEventItemWriter using it’s associated builder class to set some of the options. We’ll set the root tag, the file resource for the output, and assign the marshaller we created.

Here’s what the configured bean looks like in the job configuration:

	@Bean
	ItemWriter<ContactXml> writer() {
		String exportFile = System.getProperty("java.io.tmpdir") + "/contact-data.xml";
		
		Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
		marshaller.setClassesToBeBound(ContactXml.class);
		
		return new StaxEventItemWriterBuilder<ContactXml>()
				.name("contactItemWriter")
				.version("1.0")
				.rootTagName("ContactList")
				.resource(new FileSystemResource(exportFile))
				.marshaller(marshaller)
				.build();		
	}

Based on the data contained within the reader, when this job is executed, the following XML will be outputed to the file named contact-data.xml in the system temp directory.

<?xml version="1.0" encoding="UTF-8"?>
<ContactList>
    <Contact>
        <FirstName>John</FirstName>
        <LastName>Smith</LastName>
        <EmailAddress>[email protected]</EmailAddress>
        <CellPhone>111-333-4444</CellPhone>
    </Contact>
    <Contact>
        <FirstName>John</FirstName>
        <LastName>Doe</LastName>
        <EmailAddress>[email protected]</EmailAddress>
        <CellPhone>111-543-1234</CellPhone>
    </Contact>
    <Contact>
        <FirstName>Jane</FirstName>
        <LastName>Doe</LastName>
        <EmailAddress>[email protected]</EmailAddress>
        <CellPhone>111-463-8583</CellPhone>
    </Contact>
</ContactList>

As you can see, the amount of code to generate this simple XML output is very minimal and easy to create.

You can create considerably more complex XML through the use of additional objects and JAXB2.0 annotations during the mapping process in the ContactItemProcessor. There would be no change to the writer configuration for the additional XML mapping complexity.

You also have the additional flexibility of implementing the StaxWriterCallback interface to inject custom header and footer XML elements if needed.

Wrapping Up

We’ve had previous tutorials that demonstrate how to read and write flat files, excel spreadsheets, and now XML. Using JAXB2.0 annotations combined with the StaxEventItemWriter makes it simple to output and, with the StaxEventItemReader, input XML Hopefully you’ll find this useful for your upcoming project. As always, feel free to ask any questions about the tutorial or the code.

Complete Code Listing

Just so you have it all in one place for convenient access, here is the complete code listing for the example. I hope you find it useful!

Job Configuration

package com.example.demo.batch.xml;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.xml.builder.StaxEventItemWriterBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.FileSystemResource;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;

@Configuration
@EnableBatchProcessing
public class ContactExtractJobConfig {

	public static final String JOB_NAME = "Contract-Extract-Job";
	
	@Autowired
	private JobBuilderFactory jobBuilderFactory;
	
	@Autowired
	private StepBuilderFactory stepBuilderFactory;

	@Bean
	public Step exportContactListStep() {
		return this.stepBuilderFactory.get("exportContactListStep").<Contact, ContactXml>chunk(100).reader(reader())
				.processor(processor()).writer(writer()).build();
	}

	@Bean
	public Job contactExportJob() {
		return this.jobBuilderFactory.get(JOB_NAME).start(exportContactListStep()).build();
	}

	@Bean
	public ContactItemReader reader() {
		return new ContactItemReader();
	}

	@Bean
	public ContactItemProcessor processor() {
		return new ContactItemProcessor();
	}

	@Bean
	ItemWriter<ContactXml> writer() {
		String exportFile = System.getProperty("java.io.tmpdir"	) + "/contact-data.xml";
		
		Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
		marshaller.setClassesToBeBound(ContactXml.class);
		
		return new StaxEventItemWriterBuilder<ContactXml>()
				.name("contactItemWriter")
				.version("1.0")
				.rootTagName("ContactList")
				.resource(new FileSystemResource(exportFile))
				.marshaller(marshaller)
				.build();
		
	}
}

ContactItemReader

package com.example.demo.batch.xml;

import java.util.ArrayList;
import java.util.List;

import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.annotation.BeforeStep;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.support.IteratorItemReader;

public class ContactItemReader implements ItemReader<Contact> {

	private IteratorItemReader<Contact> delegateReader;

	@BeforeStep
	private void beforeStep(StepExecution stepExecution) {
		List<Contact> contacts = new ArrayList<>();
		contacts.add(createContact("John", "Smith", "jsmi[email protected]", "111-333-4444"));
		contacts.add(createContact("John", "Doe", "[email protected]", "111-543-1234"));
		contacts.add(createContact("Jane", "Doe", "[email protected]", "111-463-8583"));
		this.delegateReader = new IteratorItemReader<>(contacts);
	}

	@Override
	public Contact read() throws Exception {
		return this.delegateReader.read();
	}

	private Contact createContact(String firstName, String lastName, String emailAddress, String cellPhone) {
		return new Contact(firstName, lastName, emailAddress, cellPhone);
	}
}

Contact

package com.example.demo.batch.xml;

public class Contact {

	public Contact(String firstName, String lastName, String emailAddress, String cellPhone) {
		super();
		this.firstName = firstName;
		this.lastName = lastName;
		this.emailAddress = emailAddress;
		this.cellPhone = cellPhone;
	}

	private String firstName;
	private String lastName;
	private String emailAddress;
	private String cellPhone;

	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

	public String getEmailAddress() {
		return emailAddress;
	}

	public void setEmailAddress(String emailAddress) {
		this.emailAddress = emailAddress;
	}

	public String getCellPhone() {
		return cellPhone;
	}

	public void setCellPhone(String cellPhone) {
		this.cellPhone = cellPhone;
	}

}

ContactXml

package com.example.demo.batch.xml;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(propOrder = { "firstName", "lastName", "emailAddress", "cellPhone" })
@XmlRootElement(name = "Contact")
public class ContactXml {

	@XmlElement(name = "FirstName", required = true)
	protected String firstName;
	
	@XmlElement(name = "LastName", required = true)
	protected String lastName;
	
	@XmlElement(name = "EmailAddress", required = true)
	protected String emailAddress;

	@XmlElement(name = "CellPhone", required = true)
	protected String cellPhone;
	
	public String getCellPhone() {
		return cellPhone;
	}

	public String getEmailAddress() {
		return emailAddress;
	}

	public String getFirstName() {
		return firstName;
	}

	public String getLastName() {
		return lastName;
	}

	public void setCellPhone(String cellPhone) {
		this.cellPhone = cellPhone;
	}

	public void setEmailAddress(String emailAddress) {
		this.emailAddress = emailAddress;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

	@Override
	public String toString() {
		return "ContactXml [firstName=" + firstName + ", lastName=" + lastName + ", emailAddress=" + emailAddress
				+ ", cellPhone=" + cellPhone + "]";
	}

}

ContactItemProcessor

package com.example.demo.batch.xml;

import org.springframework.batch.item.ItemProcessor;

public class ContactItemProcessor implements ItemProcessor<Contact, ContactXml> {

	@Override
	public ContactXml process(Contact item) throws Exception {
		ContactXml xml = new ContactXml();
		xml.setFirstName(item.getFirstName());
		xml.setLastName(item.getLastName());
		xml.setEmailAddress(item.getEmailAddress());
		xml.setCellPhone(item.getCellPhone());
		return xml;
	}

}
0 0 vote
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments