Enterprise Java

Spring Batch – Replacing XML Job Configuration With JavaConfig

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.

Spring-Batch 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:
  3. <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-batch</artifactId>
        <version>1.2.4.RELEASE</version>
    </dependency>
  4. 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>
  5. Comment out the job definitions in the original batch configuration file named module-context.xml.
  6. Comment out the Spring app context configuration elements in the configuration file named launch-context.xml.
  7. 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&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&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);
	}

}

Keyhole Software

Keyhole is a midwest-based consulting firm with a tight-knit technical team. We work primarily with Java, JavaScript and .NET technologies, specializing in application development. We love the challenge that comes in consulting and blog often regarding some of the technical situations and technologies we face.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button