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.
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.
- Upgrade your Java build and Spring environment to Java 7, if you haven’t already.
- Add the Spring Boot dependency to the Maven pom.xml:
- 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>
- Comment out the job definitions in the original batch configuration file named module-context.xml.
- Comment out the Spring app context configuration elements in the configuration file named launch-context.xml.
- Comment out the @Component annotation on the Reader, Processor, and Writer elements. Do not comment out the @Service annotation on the CurrencyConversionServiceImpl class.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-batch</artifactId> <version>1.2.4.RELEASE</version> </dependency>
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); } }
Reference: | Spring Batch – Replacing XML Job Configuration With JavaConfig from our JCG partner Jonny Hackett at the Keyhole Software blog. |