Spring Batch – Replacing XML Job Configuration With JavaConfig

Jonny Hackett Java, Spring, Spring Batch, Technology Snapshot 5 Comments

I recently assisted a client in getting up and running with a Spring Batch implementation. The team had decided to move forward with a JavaConfig-based configuration for their batch jobs instead of the traditional XML-based configuration. As this is becoming a more common approach to configuring Java applications, I felt it was time to update Keyhole’s Spring Batch series to show you how to convert an existing XML-based Spring Batch configuration to the new JavaConfig annotation-based configuration.

This tutorial will use the simple batch job found in the second of our Spring Batch tutorials (https://keyholesoftware.com/2012/06/25/getting-started-with-spring-batch-part-two/).

House Cleaning

Before we can begin the conversion process, there is a little bit of house cleaning we need to do to the project.

1. Upgrade your Java build and Spring environment to Java 7, if you haven’t already.

2. Add the Spring Boot dependency to the Maven pom.xml:

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-batch</artifactId>
			<version>1.2.4.RELEASE</version>
		</dependency>

3. Modify the Spring Batch version to 3.0.4.RELEASE and Spring Framework version to 4.1.6.RELEASE

	<properties>      				  		<spring.framework.version>4.1.6.RELEASE</spring.framework.version>
		<spring.batch.version>3.0.4.RELEASE</spring.batch.version>
	</properties>

4. Comment out the job definitions in the original batch configuration file named module-context.xml.

5. Comment out the Spring app context configuration elements in the configuration file named launch-context.xml.

6. Comment out the @Component annotation on the Reader, Processor, and Writer elements. Do not comment out the @Service annotation on the CurrencyConversionServiceImpl class.

Building the JavaConfig-based Configuration

Now that we’ve removed or disabled the existing XML-based configuration, we can start building the JavaConfig-based configuration. In order to do that we need to create a new class with some annotations that set up the basis for the configuration.

package com.keyhole.example.config;

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.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableBatchProcessing
public class TickerPriceConversionConfig {

	@Autowired
	private JobBuilderFactory jobs;

	@Autowired
	private StepBuilderFactory steps;

}

The @Configuration annotation lets the Spring container know that this class will contain one or more @Bean annotated methods that will be processed to generate bean definitions and service requests at runtime.

The @EnableBatchProcessing annotation provides a base configuration for building batch job configurations. Spring Batch uses this annotation to set up a default JobRepository, JobLauncher, JobRegistry, PlatformTransactionManager, JobBuilderFactory, and StepBuilderFactory.

Now it’s time to add our @Bean annotated methods for our components that make up the batch job. For reference, I’ve included the corresponding XML configuration for each bean.

ItemReader Configuration

	<bean name="tickerReader"
    class="org.springframework.batch.item.file.FlatFileItemReader">
<property name="resource"
value="http://finance.yahoo.com/d/quotes.csv?s=XOM+IBM+JNJ+MSFT&f=snd1ol1p2" />
    <property name="lineMapper" ref="tickerLineMapper" />
</bean>
 
<bean name="tickerLineMapper"
class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
<property name="fieldSetMapper" ref="tickerMapper" />
    <property name="lineTokenizer" ref="tickerLineTokenizer" />
</bean>
 
<bean name="tickerLineTokenizer"
class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer" />
    @Bean
    public ItemReader<TickerData> reader() throws MalformedURLException {
        FlatFileItemReader<TickerData> reader = new FlatFileItemReader<TickerData>();
        reader.setResource(new UrlResource("http://finance.yahoo.com/d/quotes.csv?s=XOM+IBM+JNJ+MSFT&amp;f=snd1ol1p2"));
        reader.setLineMapper(new DefaultLineMapper<TickerData>() {{
            setLineTokenizer(new DelimitedLineTokenizer());
            setFieldSetMapper(new TickerFieldSetMapper());
        }});
        return reader;
    }

The ItemProcessor and ItemWriter previously used the @Component annotation for the Spring container to pick up and load the bean into the app context.

	@Bean
	public ItemProcessor<TickerData, TickerData> processor() {
		return new TickerPriceProcessor();
	}

	@Bean
	public ItemWriter<TickerData> writer() {
		return new LogItemWriter();
	}

Now that we’ve defined our Spring beans, we can create the @Bean annotated methods that represent the step and job. For reference I have included the corresponding XML configuration.

	<batch:job id="TickerPriceConversion">
		<batch:step id="convertPrice">
			<batch:tasklet transaction-manager="transactionManager">
				<batch:chunk reader="tickerReader"
              				processor="tickerPriceProcessor"
                		writer="tickerWriter" commit-interval="10" />
        		</batch:tasklet>
    		</batch:step>
	</batch:job>
	@Bean
	public Job TickerPriceConversion() throws MalformedURLException {
		return jobs.get("TickerPriceConversion").start(convertPrice()).build();
	}

	@Bean
    	public Step convertPrice() throws MalformedURLException {
        return steps.get("convertPrice")
                .<TickerData, TickerData> chunk(5)
                .reader(reader())
                .processor(processor())
                .writer(writer())
                .build();
    	}

I’ll include the complete code for the TickerPriceConversionConfig class at the end of the article for reference, but basically that is all there is to it!

Once you have defined your Spring beans and have used the JobBuilderFactory and StepBuilderFactory to create the bean configuration for the batch job and step, you’re ready to run the job and test the configuration. To run the job we will utilize Spring Boot to test the execution of the newly converted job configuration. For that we will create a new class in the test package called TickerPriceConversionJobRunner.

The source code looks like this:

package com.keyhole.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class TickerPriceConversionJobRunner {

	public static void main(String[] args) {
		SpringApplication.run(TickerPriceConversionJobRunner.class, args);
	}

}

The @SpringBootApplication annotation is essentially a convenience annotation that provides the functions you would normally get from using @Configuration, @EnableAutoConfiguration, and @ComponentScan. The TickerPriceConversionJobRunner is a simple Java application that delegates the main method processing to Spring Boot’s SpringApplication class for running the application.

You can now export this project as a jar and run the TickerPriceConversionJobRunner from the command line or, if you would like to run it within Spring STS, you can right-click on the class and choose Run As → Spring Boot Application.

Final Thoughts & Code Listings

As you can see, there isn’t a lot of work required to create Spring Batch job configurations, but if you decide to convert all of your existing jobs from an XML-based configuration to the newer JavaConfig-based configuration, you have quite a bit of work ahead of you. Most of that work is going to be tied up in amount of time required to adequately regression test the batch jobs that you’ve converted.

Would it be worth it if you have an extensive library of Spring Batch jobs? Probably not, but if you’re just starting out or have a manageable library of batch jobs this is definitely the approach I would take going forward.

Code Listing for TickerPriceConversionConfig

package com.keyhole.example.config;

import java.net.MalformedURLException;

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.ItemProcessor;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.mapping.DefaultLineMapper;
import org.springframework.batch.item.file.transform.DelimitedLineTokenizer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.UrlResource;

import com.keyhole.example.LogItemWriter;
import com.keyhole.example.TickerData;
import com.keyhole.example.TickerFieldSetMapper;
import com.keyhole.example.TickerPriceProcessor;

@Configuration
@EnableBatchProcessing
public class TickerPriceConversionConfig {

	@Autowired
	private JobBuilderFactory jobs;

	@Autowired
	private StepBuilderFactory steps;

	@Bean
    public ItemReader<TickerData> reader() throws MalformedURLException {
        FlatFileItemReader<TickerData> reader = new FlatFileItemReader<TickerData>();
        reader.setResource(new UrlResource("http://finance.yahoo.com/d/quotes.csv?s=XOM+IBM+JNJ+MSFT&amp;f=snd1ol1p2"));
        reader.setLineMapper(new DefaultLineMapper<TickerData>() {{
            setLineTokenizer(new DelimitedLineTokenizer());
            setFieldSetMapper(new TickerFieldSetMapper());
        }});
        return reader;
    }

	@Bean
	public ItemProcessor<TickerData, TickerData> processor() {
		return new TickerPriceProcessor();
	}

	@Bean
	public ItemWriter<TickerData> writer() {
		return new LogItemWriter();
	}

	@Bean
	public Job TickerPriceConversion() throws MalformedURLException {
		return jobs.get("TickerPriceConversion").start(convertPrice()).build();
	}

	@Bean
    public Step convertPrice() throws MalformedURLException {
        return steps.get("convertPrice")
                .<TickerData, TickerData> chunk(5)
                .reader(reader())
                .processor(processor())
                .writer(writer())
                .build();
    }
}

Code Listing for TickerPriceConversionJobRunner

package com.keyhole.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class TickerPriceConversionJobRunner {

	public static void main(String[] args) {
		SpringApplication.run(TickerPriceConversionJobRunner.class, args);
	}

}

— Jonny Hackett, asktheteam@keyholesoftware.com

Spring Batch Blog Series

Part One: Introducing Spring Batch

Part Two:  Getting Started With Spring Batch

Part Three: Generating Large Excel Files Using Spring Batch

Scaling Spring Batch – Step Partitioning

Spring Batch Unit Testing and Mockito

Spring Batch – Replacing XML Job Configuration With JavaConfig


About the Author
Jonny Hackett

Jonny Hackett

Twitter

Jonny is a Senior Software Engineer and Mentor with 15+ years of experience in IT. As a Java Developer, avid SportingKC fan, and photographer (check him out on www.Facebook.com/no9photography.) , Jonny is also our resident Spring Batch expert.


Share this Post

Comments 5

  1. Vincenzo Candela

    Hi Jonny, first of all many thanks for this inspiring and really useful article. I’d like to do same “house cleaning” as well in our huge multi maven’s modules project but due the large number of batches I cannot do it in only one go. Is there a nice way to mix schema-based and annotations configurations and do the job incrementally?
    Thanks

    1. Jonny Hackett

      Hi Vincenzo,

      I am happy that you have found this post helpful for you. To be honest, I haven’t run into a situation like that before.
      You could try doing the conversion one job at a time. That’s how I would approach it. Best of luck and thanks for reading.

      Jonny

Leave a Reply