To start from scratch, move on to Starting with Spring Initializr.
To skip the basics, do the following:
- Download and unzip the source repository for this guide, or clone it using Git:
git clone https://github.com/spring-guides/gs-batch-processing.git
- cd into
gs-batch-processing/initial
- Jump ahead to Create a Business Class.
When you finish, you can check your results against the code in gs-batch-processing/complete
.
Business Data
Typically, your customer or a business analyst supplies a
spreadsheet. For this simple example, you can find some made-up data in src/main/resources/sample-data.csv
:
Jill,Doe
Joe,Doe
Justin,Doe
Jane,Doe
John,Doe
This spreadsheet contains a first name and a last name on each row, separated by a comma. This is a fairly common pattern that Spring can handle without customization.
Next, you need to write an SQL script to create a table to store the data. You can find such a script in src/main/resources/schema-all.sql
:
DROP TABLE people IF EXISTS;
CREATE TABLE people (
person_id BIGINT IDENTITY NOT NULL PRIMARY KEY,
first_name VARCHAR(20),
last_name VARCHAR(20)
);
Spring Boot runs schema-@@platform@@.sql automatically during startup. -all is the default for all platforms. |
Starting with Spring Initializr
If you use Maven, visit the Spring Initializr to generate a new project with the required dependencies (Spring Batch and HyperSQL Database).
The following listing shows the pom.xml
file created when you choose Maven:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.2</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>batch-processing-complete</artifactId> <version>0.0.1-SNAPSHOT</version> <name>batch-processing-complete</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-batch</artifactId> </dependency> <dependency> <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.batch</groupId> <artifactId>spring-batch-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
If you use Gradle, visit the Spring Initializr to generate a new project with the required dependencies (Spring Batch and HyperSQL Database).
The following listing shows the build.gradle
file created when you choose Gradle:
plugins { id 'org.springframework.boot' version '2.5.2' id 'io.spring.dependency-management' version '1.0.11.RELEASE' id 'java' } group = 'com.example' version = '0.0.1-SNAPSHOT' sourceCompatibility = '1.8' repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-batch' runtimeOnly 'org.hsqldb:hsqldb' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.batch:spring-batch-test' } test { useJUnitPlatform() }
Manual Initialization (optional)
If you want to initialize the project manually rather than use the links shown earlier, follow the steps given below:
-
Navigate to https://start.spring.io. This service pulls in all the dependencies you need for an application and does most of the setup for you.
-
Choose either Gradle or Maven and the language you want to use. This guide assumes that you chose Java.
-
Click Dependencies and select Spring Batch and HyperSQL Database.
-
Click Generate.
-
Download the resulting ZIP file, which is an archive of a web application that is configured with your choices.
If your IDE has the Spring Initializr integration, you can complete this process from your IDE. |
Create a Business Class
Now that you can see the format of data inputs and outputs, you can
write code to represent a row of data, as the following example (from src/main/java/com/example/batchprocessing/Person.java
) shows:
package com.example.batchprocessing;
public class Person {
private String lastName;
private String firstName;
public Person() {
}
public Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
@Override
public String toString() {
return "firstName: " + firstName + ", lastName: " + lastName;
}
}
You can instantiate the Person
class either with first and last name through a constructor or by setting the properties.
Create an Intermediate Processor
A common paradigm in batch processing is to ingest data, transform
it, and then pipe it out somewhere else. Here, you need to write a
simple transformer that converts the names to uppercase. The following
listing (from src/main/java/com/example/batchprocessing/PersonItemProcessor.java
) shows how to do so:
package com.example.batchprocessing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.item.ItemProcessor;
public class PersonItemProcessor implements ItemProcessor<Person, Person> {
private static final Logger log = LoggerFactory.getLogger(PersonItemProcessor.class);
@Override
public Person process(final Person person) throws Exception {
final String firstName = person.getFirstName().toUpperCase();
final String lastName = person.getLastName().toUpperCase();
final Person transformedPerson = new Person(firstName, lastName);
log.info("Converting (" + person + ") into (" + transformedPerson + ")");
return transformedPerson;
}
}
PersonItemProcessor
implements Spring Batch’s ItemProcessor
interface. This makes it easy to wire the code into a batch job that
you will define later in this guide. According to the interface, you
receive an incoming Person
object, after which you transform it to an upper-cased Person
.
The input and output types need not be the same. In fact, after one source of data is read, sometimes the application’s data flow needs a different data type. |
Put Together a Batch Job
Now you need to put together the actual batch job. Spring Batch provides many utility classes that reduce the need to write custom code. Instead, you can focus on the business logic.
To configure your job, you must first create a Spring @Configuration
class like the following example in src/main/java/com/exampe/batchprocessing/BatchConfiguration.java
:
@Configuration
@EnableBatchProcessing
public class BatchConfiguration {
@Autowired
public JobBuilderFactory jobBuilderFactory;
@Autowired
public StepBuilderFactory stepBuilderFactory;
...
}
For starters, the @EnableBatchProcessing
annotation adds
many critical beans that support jobs and save you a lot of leg work.
This example uses a memory-based database (provided by @EnableBatchProcessing
),
meaning that, when it is done, the data is gone. It also autowires a
couple factories needed further below. Now add the following beans to
your BatchConfiguration
class to define a reader, a processor, and a writer:
@Bean
public FlatFileItemReader<Person> reader() {
return new FlatFileItemReaderBuilder<Person>()
.name("personItemReader")
.resource(new ClassPathResource("sample-data.csv"))
.delimited()
.names(new String[]{"firstName", "lastName"})
.fieldSetMapper(new BeanWrapperFieldSetMapper<Person>() {{
setTargetType(Person.class);
}})
.build();
}
@Bean
public PersonItemProcessor processor() {
return new PersonItemProcessor();
}
@Bean
public JdbcBatchItemWriter<Person> writer(DataSource dataSource) {
return new JdbcBatchItemWriterBuilder<Person>()
.itemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<>())
.sql("INSERT INTO people (first_name, last_name) VALUES (:firstName, :lastName)")
.dataSource(dataSource)
.build();
}
The first chunk of code defines the input, processor, and output.
reader()
creates anItemReader
. It looks for a file calledsample-data.csv
and parses each line item with enough information to turn it into aPerson
.processor()
creates an instance of thePersonItemProcessor
that you defined earlier, meant to convert the data to upper case.writer(DataSource)
creates anItemWriter
. This one is aimed at a JDBC destination and automatically gets a copy of the dataSource created by@EnableBatchProcessing
. It includes the SQL statement needed to insert a singlePerson
, driven by Java bean properties.
The last chunk (from src/main/java/com/example/batchprocessing/BatchConfiguration.java
) shows the actual job configuration:
@Bean
public Job importUserJob(JobCompletionNotificationListener listener, Step step1) {
return jobBuilderFactory.get("importUserJob")
.incrementer(new RunIdIncrementer())
.listener(listener)
.flow(step1)
.end()
.build();
}
@Bean
public Step step1(JdbcBatchItemWriter<Person> writer) {
return stepBuilderFactory.get("step1")
.<Person, Person> chunk(10)
.reader(reader())
.processor(processor())
.writer(writer)
.build();
}
The first method defines the job, and the second one defines a single step. Jobs are built from steps, where each step can involve a reader, a processor, and a writer.
In this job definition, you need an incrementer, because jobs use a database to maintain execution state. You then list each step, (though this job has only one step). The job ends, and the Java API produces a perfectly configured job.
In the step definition, you define how much data to write at a time. In this case, it writes up to ten records at a time. Next, you configure the reader, processor, and writer by using the beans injected earlier.
chunk() is prefixed <Person,Person> because it is a generic method. This represents the input and output types of each “chunk” of processing and lines up with ItemReader<Person> and ItemWriter<Person> . |
The last bit of batch configuration is a way to get notified when the job completes. The following example (from src/main/java/com/example/batchprocessing/JobCompletionNotificationListener.java
) shows such a class:
package com.example.batchprocessing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.listener.JobExecutionListenerSupport;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
@Component
public class JobCompletionNotificationListener extends JobExecutionListenerSupport {
private static final Logger log = LoggerFactory.getLogger(JobCompletionNotificationListener.class);
private final JdbcTemplate jdbcTemplate;
@Autowired
public JobCompletionNotificationListener(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public void afterJob(JobExecution jobExecution) {
if(jobExecution.getStatus() == BatchStatus.COMPLETED) {
log.info("!!! JOB FINISHED! Time to verify the results");
jdbcTemplate.query("SELECT first_name, last_name FROM people",
(rs, row) -> new Person(
rs.getString(1),
rs.getString(2))
).forEach(person -> log.info("Found <" + person + "> in the database."));
}
}
}
The JobCompletionNotificationListener
listens for when a job is BatchStatus.COMPLETED
and then uses JdbcTemplate
to inspect the results.
Make the Application Executable
Although batch processing can be embedded in web apps and WAR files,
the simpler approach demonstrated below creates a standalone
application. You package everything in a single, executable JAR file,
driven by a good old Java main()
method.
The Spring Initializr created an application class for you. For this
simple example, it works without further modification. The following
listing (from src/main/java/com/example/batchprocessing/BatchProcessingApplication.java
) shows the application class:
package com.example.batchprocessing;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class BatchProcessingApplication {
public static void main(String[] args) throws Exception {
System.exit(SpringApplication.exit(SpringApplication.run(BatchProcessingApplication.class, args)));
}
}
@SpringBootApplication
is a convenience annotation that adds all of the following:
@Configuration
: Tags the class as a source of bean definitions for the application context.@EnableAutoConfiguration
: Tells Spring Boot to start adding beans based on classpath settings, other beans, and various property settings. For example, ifspring-webmvc
is on the classpath, this annotation flags the application as a web application and activates key behaviors, such as setting up aDispatcherServlet
.@ComponentScan
: Tells Spring to look for other components, configurations, and services in thecom/example
package, letting it find the controllers.
The main()
method uses Spring Boot’s SpringApplication.run()
method to launch an application. Did you notice that there was not a single line of XML? There is no web.xml
file, either. This web application is 100% pure Java and you did not
have to deal with configuring any plumbing or infrastructure.
Note that SpringApplication.exit()
and System.exit()
ensure that the JVM exits upon job completion. See the Application Exit section in Spring Boot Reference documentation for more details.
For demonstration purposes, there is code to create a JdbcTemplate
, query the database, and print out the names of people the batch job inserts.
Build an executable JAR
You can run the application from the command line with Gradle or Maven. You can also build a single executable JAR file that contains all the necessary dependencies, classes, and resources and run that. Building an executable jar makes it easy to ship, version, and deploy the service as an application throughout the development lifecycle, across different environments, and so forth.
If you use Gradle, you can run the application by using ./gradlew bootRun
. Alternatively, you can build the JAR file by using ./gradlew build
and then run the JAR file, as follows:
If you use Maven, you can run the application by using ./mvnw spring-boot:run
. Alternatively, you can build the JAR file with ./mvnw clean package
and then run the JAR file, as follows:
The steps described here create a runnable JAR. You can also build a classic WAR file. |
The job prints out a line for each person that gets transformed. After the job runs, you can also see the output from querying the database. It should resemble the following output:
Converting (firstName: Jill, lastName: Doe) into (firstName: JILL, lastName: DOE)
Converting (firstName: Joe, lastName: Doe) into (firstName: JOE, lastName: DOE)
Converting (firstName: Justin, lastName: Doe) into (firstName: JUSTIN, lastName: DOE)
Converting (firstName: Jane, lastName: Doe) into (firstName: JANE, lastName: DOE)
Converting (firstName: John, lastName: Doe) into (firstName: JOHN, lastName: DOE)
Found <firstName: JILL, lastName: DOE> in the database.
Found <firstName: JOE, lastName: DOE> in the database.
Found <firstName: JUSTIN, lastName: DOE> in the database.
Found <firstName: JANE, lastName: DOE> in the database.
Found <firstName: JOHN, lastName: DOE> in the database.
My Comments
FlatFileItemReader is used to read cvs file. It can be any class that implements ItemReader interface. For example, if I build an ItemReader that is going to read exel file then a reader something likes following
@Bean
ItemReader<StudentDTO> excelStudentReader() {
PoiItemReader<StudentDTO> reader =
new
PoiItemReader<>();
reader.setLinesToSkip(
1
);
reader.setResource(
new
ClassPathResource(
"data/sample-data.xlsx"
));
reader.setRowMapper(excelRowMapper());
return
reader;
}
Job:
in this example, the job has only one step. But if I want more than one step, how will I do? I will use .next() method such as
@Bean public Job personUpdateJob(JobCompletionNotificationListener listener, Step step1, Step step2) throws Exception {
return this.jobBuilderFactory.get("personUpdateJob") .incrementer(new RunIdIncrementer()) .listener(listener) .start(step1) .next(step2) .build(); }
0 comments:
Post a Comment