Add java configuration for the Football Job sample

Issue #3663
This commit is contained in:
Mahmoud Ben Hassine
2023-09-22 13:25:26 +02:00
parent 4f6f31d94b
commit e693879144
30 changed files with 569 additions and 359 deletions

View File

@@ -225,285 +225,10 @@ object
### Football Job
This is a (American) Football statistics loading job. We gave it the
id of `footballJob` in our configuration file. Before diving
into the batch job, we'll examine the two input files that need to
be loaded. First is `player.csv`, which can be found in the
samples project under
src/main/resources/data/footballjob/input/. Each line within this
file represents a player, with a unique id, the players name,
position, etc:
This is a (American) Football statistics loading job. It loads two files containing players and games
data into a database, and then combines them to summarise how each player performed for a particular year.
AbduKa00,Abdul-Jabbar,Karim,rb,1974,1996
AbduRa00,Abdullah,Rabih,rb,1975,1999
AberWa00,Abercrombie,Walter,rb,1959,1982
AbraDa00,Abramowicz,Danny,wr,1945,1967
AdamBo00,Adams,Bob,te,1946,1969
AdamCh00,Adams,Charlie,wr,1979,2003
...
One of the first noticeable characteristics of the file is that each
data element is separated by a comma, a format most are familiar
with known as 'CSV'. Other separators such as pipes or semicolons
could just as easily be used to delineate between unique
elements. In general, it falls into one of two types of flat file
formats: delimited or fixed length. (The fixed length case was
covered in the `fixedLengthImportJob`.
The second file, 'games.csv' is formatted the same as the previous
example, and resides in the same directory:
AbduKa00,1996,mia,10,nwe,0,0,0,0,0,29,104,,16,2
AbduKa00,1996,mia,11,clt,0,0,0,0,0,18,70,,11,2
AbduKa00,1996,mia,12,oti,0,0,0,0,0,18,59,,0,0
AbduKa00,1996,mia,13,pit,0,0,0,0,0,16,57,,0,0
AbduKa00,1996,mia,14,rai,0,0,0,0,0,18,39,,7,0
AbduKa00,1996,mia,15,nyg,0,0,0,0,0,17,96,,14,0
...
Each line in the file represents an individual player's performance
in a particular game, containing such statistics as passing yards,
receptions, rushes, and total touchdowns.
Our example batch job is going to load both files into a database,
and then combine each to summarise how each player performed for a
particular year. Although this example is fairly trivial, it shows
multiple types of input, and the general style is a common batch
scenario. That is, summarising a very large dataset so that it can
be more easily manipulated or viewed by an online web-based
application. In an enterprise solution the third step, the reporting
step, could be implemented through the use of Eclipse BIRT or one of
the many Java Reporting Engines. Given this description, we can then
easily divide our batch job up into 3 'steps': one to load the
player data, one to load the game data, and one to produce a summary
report:
**Note:** One of the nice features of Spring is a project called
Spring IDE. When you download the project you can install Spring
IDE and add the Spring configurations to the IDE project. This is
not a tutorial on Spring IDE but the visual view into Spring beans
is helpful in understanding the structure of a Job
Configuration. Spring IDE produces the following diagram:
![Spring Batch Football Object Model](src/site/resources/images/spring-batch-football-graph.jpg "Spring Batch Football Object Model")
This corresponds exactly with the `footballJob.xml` job
configuration file which can be found in the jobs folder under
`src/main/resources`. When you drill down into the football job
you will see that the configuration has a list of steps:
<property name="steps">
<list>
<bean id="playerload" parent="simpleStep" .../>
<bean id="gameLoad" parent="simpleStep" .../>
<bean id="playerSummarization" parent="simpleStep" .../>
</list>
</property>
A step is run until there is no more input to process, which in
this case would mean that each file has been completely
processed. To describe it in a more narrative form: the first step,
playerLoad, begins executing by grabbing one line of input from the
file, and parsing it into a domain object. That domain object is
then passed to a dao, which writes it out to the PLAYERS table. This
action is repeated until there are no more lines in the file,
causing the playerLoad step to finish. Next, the gameLoad step does
the same for the games input file, inserting into the GAMES
table. Once finished, the playerSummarization step can begin. Unlike
the first two steps, playerSummarization input comes from the
database, using a Sql statement to combine the GAMES and PLAYERS
table. Each returned row is packaged into a domain object and
written out to the PLAYER_SUMMARY table.
Now that we've discussed the entire flow of the batch job, we can
dive deeper into the first step: playerLoad:
<bean id="playerload" parent="simpleStep">
<property name="commitInterval" value="${job.commit.interval}" />
<property name="startLimit" value="100" />
<property name="itemReader"
ref="playerFileItemReader" />
<property name="itemWriter">
<bean
class="org.springframework.batch.sample.domain.football.internal.internal.PlayerItemWriter">
<property name="playerDao">
<bean
class="org.springframework.batch.sample.domain.football.internal.internal.JdbcPlayerDao">
<property name="dataSource"
ref="dataSource" />
</bean>
</property>
</bean>
</property>
</bean>
The root bean in this case is a `SimpleStepFactoryBean`, which
can be considered a 'blueprint' of sorts that tells the execution
environment basic details about how the batch job should be
executed. It contains four properties: (others have been removed for
greater clarity) commitInterval, startLimit, itemReader and
itemWriter . After performing all necessary startup, the framework
will periodically delegate to the reader and writer. In this way,
the developer can remain solely concerned with their business
logic.
* *ItemReader* the item reader is the source of the information
pipe. At the most basic level input is read in from an input
source, parsed into a domain object and returned. In this way, the
good batch architecture practice of ensuring all data has been
read before beginning processing can be enforced, along with
providing a possible avenue for reuse.
* *ItemWriter* this is the business logic. At a high level,
the item writer takes the item returned from the reader
and 'processes' it. In our case it's a data access object that is
simply responsible for inserting a record into the PLAYERS
table. As you can see the developer does very little.
The application developer simply provides a job configuration with a
configured number of steps, an ItemReader associated to some type
of input source, and ItemWriter associated to some type of
output source and a little mapping of data from flat records to
objects and the pipe is ready wired for processing.
Another property in the step configuration, the commitInterval,
gives the framework vital information about how to control
transactions during the batch run. Due to the large amount of data
involved in batch processing, it is often advantageous to 'batch'
together multiple logical units of work into one transaction, since
starting and committing a transaction is extremely expensive. For
example, in the playerLoad step, the framework calls read() on the
item reader. The item reader reads one record from the file, and
returns a domain object representation which is passed to the
processor. The writer then writes the one record to the database. It
can then be said that one iteration = one call to
`ItemReader.read()` = one line of the file. Therefore, setting
your commitInterval to 5 would result in the framework committing a
transaction after 5 lines have been read from the file, with 5
resultant entries in the PLAYERS table.
Following the general flow of the batch job, the next step is to
describe how each line of the file will be parsed from its string
representation into a domain object. The first thing the provider
will need is an `ItemReader`, which is provided as part of the Spring
Batch infrastructure. Because the input is flat-file based, a
`FlatFileItemReader` is used:
<bean id="playerFileItemReader"
class="org.springframework.batch.item.file.FlatFileItemReader">
<property name="resource"
value="classpath:data/footballjob/input/${player.file.name}" />
<property name="lineTokenizer">
<bean
class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer">
<property name="names"
value="ID,lastName,firstName,position,birthYear,debutYear" />
</bean>
</property>
<property name="fieldSetMapper">
<bean
class="org.springframework.batch.sample.domain.football.internal.internal.PlayerFieldSetMapper" />
</property>
</bean>
There are three required dependencies of the item reader; the first
is a resource to read in, which is the file to process. The second
dependency is a `LineTokenizer`. The interface for a
`LineTokenizer` is very simple, given a string; it will return a
`FieldSet` that wraps the results from splitting the provided
string. A `FieldSet` is Spring Batch's abstraction for flat file
data. It allows developers to work with file input in much the same
way as they would work with database input. All the developers need
to provide is a `FieldSetMapper` (similar to a Spring
`RowMapper`) that will map the provided `FieldSet` into an
`Object`. Simply by providing the names of each token to the
`LineTokenizer`, the `ItemReader` can pass the
`FieldSet` into our `PlayerMapper`, which implements the
`FieldSetMapper` interface. There is a single method,
`mapLine()`, which maps `FieldSet`s the same way that
developers are comfortable mapping `ResultSet`s into Java
`Object`s, either by index or field name. This behaviour is by
intention and design similar to the `RowMapper` passed into a
`JdbcTemplate`. You can see this below:
public class PlayerMapper implements FieldSetMapper {
public Object mapLine(FieldSet fs) {
if(fs == null){
return null;
}
Player player = new Player();
player.setID(fs.readString("ID"));
player.setLastName(fs.readString("lastName"));
player.setFirstName(fs.readString("firstName"));
player.setPosition(fs.readString("position"));
player.setDebutYear(fs.readInt("debutYear"));
player.setBirthYear(fs.readInt("birthYear"));
return player;
}
}
The flow of the `ItemReader`, in this case, starts with a call
to read the next line from the file. This is passed into the
provided `LineTokenizer`. The `LineTokenizer` splits the
line at every comma, and creates a `FieldSet` using the created
`String` array and the array of names passed in.
**Note:** it is only necessary to provide the names to create the
`FieldSet` if you wish to access the field by name, rather
than by index.
Once the domain representation of the data has been returned by the
provider, (i.e. a `Player` object in this case) it is passed to
the `ItemWriter`, which is essentially a Dao that uses a Spring
`JdbcTemplate` to insert a new row in the PLAYERS table.
The next step, gameLoad, works almost exactly the same as the
playerLoad step, except the games file is used.
The final step, playerSummarization, is much like the previous two
steps, in that it reads from a reader and returns a domain object to
a writer. However, in this case, the input source is the database,
not a file:
<bean id="playerSummarizationSource" class="org.springframework.batch.item.database.JdbcCursorItemReader">
<property name="dataSource" ref="dataSource" />
<property name="mapper">
<bean
class="org.springframework.batch.sample.domain.football.internal.internal.PlayerSummaryMapper" />
</property>
<property name="sql">
<value>
SELECT games.player_id, games.year_no, SUM(COMPLETES),
SUM(ATTEMPTS), SUM(PASSING_YARDS), SUM(PASSING_TD),
SUM(INTERCEPTIONS), SUM(RUSHES), SUM(RUSH_YARDS),
SUM(RECEPTIONS), SUM(RECEPTIONS_YARDS), SUM(TOTAL_TD)
from games, players where players.player_id =
games.player_id group by games.player_id, games.year_no
</value>
</property>
</bean>
The `JdbcCursorItemReader` has three dependences:
* A `DataSource`
* The `RowMapper` to use for each row.
* The Sql statement used to create the cursor.
When the step is first started, a query will be run against the
database to open a cursor, and each call to `itemReader.read()`
will move the cursor to the next row, using the provided
`RowMapper` to return the correct object. As with the previous
two steps, each record returned by the provider will be written out
to the database in the PLAYER_SUMMARY table. Finally to run this
sample application you can execute the JUnit test
`FootballJobFunctionalTests`, and you'll see an output showing
each of the records as they are processed. Please keep in mind that
AoP is used to wrap the `ItemWriter` and output each record as it
is processed to the logger, which may impact performance.
[Football Job](./src/main/java/org/springframework/batch/sample/football/README.md)
### Header Footer Sample

View File

@@ -0,0 +1,150 @@
package org.springframework.batch.sample.football;
import javax.sql.DataSource;
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.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.item.database.JdbcCursorItemReader;
import org.springframework.batch.item.database.builder.JdbcCursorItemReaderBuilder;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder;
import org.springframework.batch.sample.football.internal.GameFieldSetMapper;
import org.springframework.batch.sample.football.internal.JdbcGameDao;
import org.springframework.batch.sample.football.internal.JdbcPlayerDao;
import org.springframework.batch.sample.football.internal.JdbcPlayerSummaryDao;
import org.springframework.batch.sample.football.internal.PlayerFieldSetMapper;
import org.springframework.batch.sample.football.internal.PlayerItemWriter;
import org.springframework.batch.sample.football.internal.PlayerSummaryMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.jdbc.support.JdbcTransactionManager;
@Configuration
@EnableBatchProcessing
public class FootballJobConfiguration {
// step 1 configuration
@Bean
public FlatFileItemReader<Player> playerFileItemReader() {
return new FlatFileItemReaderBuilder<Player>().name("playerFileItemReader")
.resource(new ClassPathResource("org/springframework/batch/sample/football/data/player-small1.csv"))
.delimited()
.names("ID", "lastName", "firstName", "position", "birthYear", "debutYear")
.fieldSetMapper(new PlayerFieldSetMapper())
.build();
}
@Bean
public PlayerItemWriter playerWriter() {
PlayerItemWriter playerItemWriter = new PlayerItemWriter();
JdbcPlayerDao playerDao = new JdbcPlayerDao();
playerDao.setDataSource(dataSource());
playerItemWriter.setPlayerDao(playerDao);
return playerItemWriter;
}
@Bean
public Step playerLoad(JobRepository jobRepository, JdbcTransactionManager transactionManager) {
return new StepBuilder("playerLoad", jobRepository).<Player, Player>chunk(2, transactionManager)
.reader(playerFileItemReader())
.writer(playerWriter())
.build();
}
// step 2 configuration
@Bean
public FlatFileItemReader<Game> gameFileItemReader() {
return new FlatFileItemReaderBuilder<Game>().name("gameFileItemReader")
.resource(new ClassPathResource("org/springframework/batch/sample/football/data/games-small.csv"))
.delimited()
.names("id", "year", "team", "week", "opponent", "completes", "attempts", "passingYards", "passingTd",
"interceptions", "rushes", "rushYards", "receptions", "receptionYards", "totalTd")
.fieldSetMapper(new GameFieldSetMapper())
.build();
}
@Bean
public JdbcGameDao gameWriter() {
JdbcGameDao jdbcGameDao = new JdbcGameDao();
jdbcGameDao.setDataSource(dataSource());
return jdbcGameDao;
}
@Bean
public Step gameLoad(JobRepository jobRepository, JdbcTransactionManager transactionManager) {
return new StepBuilder("gameLoad", jobRepository).<Game, Game>chunk(2, transactionManager)
.reader(gameFileItemReader())
.writer(gameWriter())
.build();
}
// step 3 configuration
@Bean
public JdbcCursorItemReader<PlayerSummary> playerSummarizationSource() {
String sql = """
SELECT GAMES.player_id, GAMES.year_no, SUM(COMPLETES),
SUM(ATTEMPTS), SUM(PASSING_YARDS), SUM(PASSING_TD),
SUM(INTERCEPTIONS), SUM(RUSHES), SUM(RUSH_YARDS),
SUM(RECEPTIONS), SUM(RECEPTIONS_YARDS), SUM(TOTAL_TD)
from GAMES, PLAYERS where PLAYERS.player_id =
GAMES.player_id group by GAMES.player_id, GAMES.year_no
""";
return new JdbcCursorItemReaderBuilder<PlayerSummary>().name("playerSummarizationSource")
.ignoreWarnings(true)
.sql(sql)
.dataSource(dataSource())
.rowMapper(new PlayerSummaryMapper())
.build();
}
@Bean
public JdbcPlayerSummaryDao summaryWriter() {
JdbcPlayerSummaryDao jdbcPlayerSummaryDao = new JdbcPlayerSummaryDao();
jdbcPlayerSummaryDao.setDataSource(dataSource());
return jdbcPlayerSummaryDao;
}
@Bean
public Step summarizationStep(JobRepository jobRepository, JdbcTransactionManager transactionManager) {
return new StepBuilder("summarizationStep", jobRepository)
.<PlayerSummary, PlayerSummary>chunk(2, transactionManager)
.reader(playerSummarizationSource())
.writer(summaryWriter())
.build();
}
// job configuration
@Bean
public Job job(JobRepository jobRepository, Step playerLoad, Step gameLoad, Step summarizationStep) {
return new JobBuilder("footballJob", jobRepository).start(playerLoad)
.next(gameLoad)
.next(summarizationStep)
.build();
}
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.HSQL)
.addScript("/org/springframework/batch/core/schema-drop-hsqldb.sql")
.addScript("/org/springframework/batch/core/schema-hsqldb.sql")
.addScript("/org/springframework/batch/sample/football/sql/schema.sql")
.build();
}
@Bean
public JdbcTransactionManager transactionManager(DataSource dataSource) {
return new JdbcTransactionManager(dataSource);
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.batch.sample.domain.football;
package org.springframework.batch.sample.football;
import java.io.Serializable;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2006-2007 the original author or authors.
* Copyright 2006-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.batch.sample.domain.football;
package org.springframework.batch.sample.football;
import java.io.Serializable;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.batch.sample.domain.football;
package org.springframework.batch.sample.football;
/**
* Interface for writing {@link Player} objects to arbitrary output.

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.sample.domain.football;
package org.springframework.batch.sample.football;
/**
* Domain object representing the summary of a given Player's year.

View File

@@ -0,0 +1,301 @@
# Football Job
## About the sample
This is a (American) Football statistics loading job. We gave it the
id of `footballJob` in our configuration file. Before diving
into the batch job, we'll examine the two input files that need to
be loaded. First is `player.csv`, which can be found in the
samples project under `src/main/resources/org/springframework/batch/sample/football/data`.
Each line within this file represents a player, with a unique id, the players name, position, etc:
```
AbduKa00,Abdul-Jabbar,Karim,rb,1974,1996
AbduRa00,Abdullah,Rabih,rb,1975,1999
AberWa00,Abercrombie,Walter,rb,1959,1982
AbraDa00,Abramowicz,Danny,wr,1945,1967
AdamBo00,Adams,Bob,te,1946,1969
AdamCh00,Adams,Charlie,wr,1979,2003
...
```
One of the first noticeable characteristics of the file is that each
data element is separated by a comma, a format most are familiar
with known as 'CSV'. Other separators such as pipes or semicolons
could just as easily be used to delineate between unique
elements. In general, it falls into one of two types of flat file
formats: delimited or fixed length. (The fixed length case was
covered in the `fixedLengthImportJob`).
The second file, 'games.csv' is formatted the same as the previous
example, and resides in the same directory:
```
AbduKa00,1996,mia,10,nwe,0,0,0,0,0,29,104,,16,2
AbduKa00,1996,mia,11,clt,0,0,0,0,0,18,70,,11,2
AbduKa00,1996,mia,12,oti,0,0,0,0,0,18,59,,0,0
AbduKa00,1996,mia,13,pit,0,0,0,0,0,16,57,,0,0
AbduKa00,1996,mia,14,rai,0,0,0,0,0,18,39,,7,0
AbduKa00,1996,mia,15,nyg,0,0,0,0,0,17,96,,14,0
...
```
Each line in the file represents an individual player's performance
in a particular game, containing such statistics as passing yards,
receptions, rushes, and total touchdowns.
Our example batch job is going to load both files into a database,
and then combine each to summarise how each player performed for a
particular year. Although this example is fairly trivial, it shows
multiple types of input, and the general style is a common batch
scenario. That is, summarising a very large dataset so that it can
be more easily manipulated or viewed by an online web-based
application. In an enterprise solution the third step, the reporting
step, could be implemented through the use of Eclipse BIRT or one of
the many Java Reporting Engines. Given this description, we can then
easily divide our batch job up into 3 'steps': one to load the
player data, one to load the game data, and one to produce a summary
report:
```mermaid
graph LR
A(playerLoad) --> B(gameLoad)
B --> C(playerSummarization)
```
This corresponds exactly with the `footballJob.xml` job configuration file which can be found in
`src/main/resources/org/springframework/batch/sample/football/job`.
When you drill down into the football job you will see that the configuration has a list of steps:
```xml
<property name="steps">
<list>
<bean id="playerload" parent="simpleStep" .../>
<bean id="gameLoad" parent="simpleStep" .../>
<bean id="playerSummarization" parent="simpleStep" .../>
</list>
</property>
```
A step is run until there is no more input to process, which in
this case would mean that each file has been completely
processed. To describe it in a more narrative form: the first step,
playerLoad, begins executing by grabbing one line of input from the
file, and parsing it into a domain object. That domain object is
then passed to a dao, which writes it out to the PLAYERS table. This
action is repeated until there are no more lines in the file,
causing the playerLoad step to finish. Next, the gameLoad step does
the same for the games input file, inserting into the GAMES
table. Once finished, the playerSummarization step can begin. Unlike
the first two steps, playerSummarization input comes from the
database, using a Sql statement to combine the GAMES and PLAYERS
table. Each returned row is packaged into a domain object and
written out to the PLAYER_SUMMARY table.
Now that we've discussed the entire flow of the batch job, we can
dive deeper into the first step: playerLoad:
```xml
<bean id="playerload" parent="simpleStep">
<property name="commitInterval" value="${job.commit.interval}" />
<property name="startLimit" value="100" />
<property name="itemReader"
ref="playerFileItemReader" />
<property name="itemWriter">
<bean
class="org.springframework.batch.sample.football.internal.internal.PlayerItemWriter">
<property name="playerDao">
<bean
class="org.springframework.batch.sample.football.internal.internal.JdbcPlayerDao">
<property name="dataSource"
ref="dataSource" />
</bean>
</property>
</bean>
</property>
</bean>
```
The root bean in this case is a `SimpleStepFactoryBean`, which
can be considered a 'blueprint' of sorts that tells the execution
environment basic details about how the batch job should be
executed. It contains four properties: (others have been removed for
greater clarity) commitInterval, startLimit, itemReader and
itemWriter . After performing all necessary startup, the framework
will periodically delegate to the reader and writer. In this way,
the developer can remain solely concerned with their business
logic.
* *ItemReader* the item reader is the source of the information
pipe. At the most basic level input is read in from an input
source, parsed into a domain object and returned. In this way, the
good batch architecture practice of ensuring all data has been
read before beginning processing can be enforced, along with
providing a possible avenue for reuse.
* *ItemWriter* this is the business logic. At a high level,
the item writer takes the item returned from the reader
and 'processes' it. In our case it's a data access object that is
simply responsible for inserting a record into the PLAYERS
table. As you can see the developer does very little.
The application developer simply provides a job configuration with a
configured number of steps, an ItemReader associated to some type
of input source, and ItemWriter associated to some type of
output source and a little mapping of data from flat records to
objects and the pipe is ready wired for processing.
Another property in the step configuration, the commitInterval,
gives the framework vital information about how to control
transactions during the batch run. Due to the large amount of data
involved in batch processing, it is often advantageous to 'batch'
together multiple logical units of work into one transaction, since
starting and committing a transaction is extremely expensive. For
example, in the playerLoad step, the framework calls read() on the
item reader. The item reader reads one record from the file, and
returns a domain object representation which is passed to the
processor. The writer then writes the one record to the database. It
can then be said that one iteration = one call to
`ItemReader.read()` = one line of the file. Therefore, setting
your commitInterval to 5 would result in the framework committing a
transaction after 5 lines have been read from the file, with 5
resultant entries in the PLAYERS table.
Following the general flow of the batch job, the next step is to
describe how each line of the file will be parsed from its string
representation into a domain object. The first thing the provider
will need is an `ItemReader`, which is provided as part of the Spring
Batch infrastructure. Because the input is flat-file based, a
`FlatFileItemReader` is used:
```xml
<bean id="playerFileItemReader"
class="org.springframework.batch.item.file.FlatFileItemReader">
<property name="resource"
value="classpath:data/footballjob/input/${player.file.name}" />
<property name="lineTokenizer">
<bean
class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer">
<property name="names"
value="ID,lastName,firstName,position,birthYear,debutYear" />
</bean>
</property>
<property name="fieldSetMapper">
<bean
class="org.springframework.batch.sample.football.internal.internal.PlayerFieldSetMapper" />
</property>
</bean>
```
There are three required dependencies of the item reader; the first
is a resource to read in, which is the file to process. The second
dependency is a `LineTokenizer`. The interface for a
`LineTokenizer` is very simple, given a string; it will return a
`FieldSet` that wraps the results from splitting the provided
string. A `FieldSet` is Spring Batch's abstraction for flat file
data. It allows developers to work with file input in much the same
way as they would work with database input. All the developers need
to provide is a `FieldSetMapper` (similar to a Spring
`RowMapper`) that will map the provided `FieldSet` into an
`Object`. Simply by providing the names of each token to the
`LineTokenizer`, the `ItemReader` can pass the
`FieldSet` into our `PlayerMapper`, which implements the
`FieldSetMapper` interface. There is a single method,
`mapLine()`, which maps `FieldSet`s the same way that
developers are comfortable mapping `ResultSet`s into Java
`Object`s, either by index or field name. This behaviour is by
intention and design similar to the `RowMapper` passed into a
`JdbcTemplate`. You can see this below:
```java
public class PlayerMapper implements FieldSetMapper {
public Object mapLine(FieldSet fs) {
if(fs == null){
return null;
}
Player player = new Player();
player.setID(fs.readString("ID"));
player.setLastName(fs.readString("lastName"));
player.setFirstName(fs.readString("firstName"));
player.setPosition(fs.readString("position"));
player.setDebutYear(fs.readInt("debutYear"));
player.setBirthYear(fs.readInt("birthYear"));
return player;
}
}
```
The flow of the `ItemReader`, in this case, starts with a call
to read the next line from the file. This is passed into the
provided `LineTokenizer`. The `LineTokenizer` splits the
line at every comma, and creates a `FieldSet` using the created
`String` array and the array of names passed in.
**Note:** it is only necessary to provide the names to create the
`FieldSet` if you wish to access the field by name, rather
than by index.
Once the domain representation of the data has been returned by the
provider, (i.e. a `Player` object in this case) it is passed to
the `ItemWriter`, which is essentially a Dao that uses a Spring
`JdbcTemplate` to insert a new row in the PLAYERS table.
The next step, gameLoad, works almost exactly the same as the
playerLoad step, except the games file is used.
The final step, playerSummarization, is much like the previous two
steps, in that it reads from a reader and returns a domain object to
a writer. However, in this case, the input source is the database,
not a file:
```xml
<bean id="playerSummarizationSource" class="org.springframework.batch.item.database.JdbcCursorItemReader">
<property name="dataSource" ref="dataSource" />
<property name="mapper">
<bean
class="org.springframework.batch.sample.football.internal.internal.PlayerSummaryMapper" />
</property>
<property name="sql">
<value>
SELECT games.player_id, games.year_no, SUM(COMPLETES),
SUM(ATTEMPTS), SUM(PASSING_YARDS), SUM(PASSING_TD),
SUM(INTERCEPTIONS), SUM(RUSHES), SUM(RUSH_YARDS),
SUM(RECEPTIONS), SUM(RECEPTIONS_YARDS), SUM(TOTAL_TD)
from games, players where players.player_id =
games.player_id group by games.player_id, games.year_no
</value>
</property>
</bean>
```
The `JdbcCursorItemReader` has three dependencies:
* A `DataSource`
* The `RowMapper` to use for each row.
* The Sql statement used to create the cursor.
When the step is first started, a query will be run against the
database to open a cursor, and each call to `itemReader.read()`
will move the cursor to the next row, using the provided
`RowMapper` to return the correct object. As with the previous
two steps, each record returned by the provider will be written out
to the database in the PLAYER_SUMMARY table.
The equivalent Java configuration of the football job can be found in
`org/springframework/batch/sample/football/FootballJobConfiguration.java`.
## Run the sample
You can run the sample from the command line as following:
```
$>cd spring-batch-samples
# Launch the sample using the XML configuration
$>../mvnw -Dtest=FootballJobFunctionalTests#testLaunchJobWithXmlConfiguration test
# Launch the sample using the Java configuration
$>../mvnw -Dtest=FootballJobFunctionalTests#testLaunchJobWithJavaConfiguration test
```

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.batch.sample.domain.football.internal;
package org.springframework.batch.sample.football.internal;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

View File

@@ -14,11 +14,11 @@
* limitations under the License.
*/
package org.springframework.batch.sample.domain.football.internal;
package org.springframework.batch.sample.football.internal;
import org.springframework.batch.item.file.mapping.FieldSetMapper;
import org.springframework.batch.item.file.transform.FieldSet;
import org.springframework.batch.sample.domain.football.Game;
import org.springframework.batch.sample.football.Game;
public class GameFieldSetMapper implements FieldSetMapper<Game> {

View File

@@ -14,11 +14,11 @@
* limitations under the License.
*/
package org.springframework.batch.sample.domain.football.internal;
package org.springframework.batch.sample.football.internal;
import org.springframework.batch.item.Chunk;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.sample.domain.football.Game;
import org.springframework.batch.sample.football.Game;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;

View File

@@ -14,12 +14,12 @@
* limitations under the License.
*/
package org.springframework.batch.sample.domain.football.internal;
package org.springframework.batch.sample.football.internal;
import javax.sql.DataSource;
import org.springframework.batch.sample.domain.football.Player;
import org.springframework.batch.sample.domain.football.PlayerDao;
import org.springframework.batch.sample.football.Player;
import org.springframework.batch.sample.football.PlayerDao;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;

View File

@@ -14,13 +14,13 @@
* limitations under the License.
*/
package org.springframework.batch.sample.domain.football.internal;
package org.springframework.batch.sample.football.internal;
import javax.sql.DataSource;
import org.springframework.batch.item.Chunk;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.sample.domain.football.PlayerSummary;
import org.springframework.batch.sample.football.PlayerSummary;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;

View File

@@ -14,11 +14,11 @@
* limitations under the License.
*/
package org.springframework.batch.sample.domain.football.internal;
package org.springframework.batch.sample.football.internal;
import org.springframework.batch.item.file.mapping.FieldSetMapper;
import org.springframework.batch.item.file.transform.FieldSet;
import org.springframework.batch.sample.domain.football.Player;
import org.springframework.batch.sample.football.Player;
public class PlayerFieldSetMapper implements FieldSetMapper<Player> {

View File

@@ -14,12 +14,12 @@
* limitations under the License.
*/
package org.springframework.batch.sample.domain.football.internal;
package org.springframework.batch.sample.football.internal;
import org.springframework.batch.item.Chunk;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.sample.domain.football.Player;
import org.springframework.batch.sample.domain.football.PlayerDao;
import org.springframework.batch.sample.football.Player;
import org.springframework.batch.sample.football.PlayerDao;
public class PlayerItemWriter implements ItemWriter<Player> {

View File

@@ -13,17 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.sample.domain.football.internal;
package org.springframework.batch.sample.football.internal;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.springframework.batch.sample.domain.football.PlayerSummary;
import org.springframework.batch.sample.football.PlayerSummary;
import org.springframework.jdbc.core.RowMapper;
/**
* RowMapper used to map a ResultSet to a
* {@link org.springframework.batch.sample.domain.football.PlayerSummary}
* RowMapper used to map a ResultSet to a {@link PlayerSummary}
*
* @author Lucas Ward
* @author Mahmoud Ben Hassine

View File

@@ -13,17 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.sample.domain.football.internal;
package org.springframework.batch.sample.football.internal;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.springframework.batch.sample.domain.football.PlayerSummary;
import org.springframework.batch.sample.football.PlayerSummary;
import org.springframework.jdbc.core.RowMapper;
/**
* RowMapper used to map a ResultSet to a
* {@link org.springframework.batch.sample.domain.football.PlayerSummary}
* RowMapper used to map a ResultSet to a {@link PlayerSummary}
*
* @author Lucas Ward
* @author Mahmoud Ben Hassine

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- useful for launching this job on its own from the command line -->
<import resource="classpath:/simple-job-launcher-context.xml" />
<import resource="classpath:/jobs/footballJob.xml" />
</beans>

View File

@@ -1,13 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/batch https://www.springframework.org/schema/batch/spring-batch.xsd
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/batch https://www.springframework.org/schema/batch/spring-batch.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<job id="footballJob" xmlns="http://www.springframework.org/schema/batch">
<step id="playerload" next="gameLoad">
<step id="playerLoad" next="gameLoad">
<tasklet>
<chunk reader="playerFileItemReader" writer="playerWriter"
commit-interval="${job.commit.interval}" />
@@ -29,24 +27,24 @@
</tasklet>
</step>
<bean id="playerWriter" class="org.springframework.batch.sample.domain.football.internal.PlayerItemWriter">
<bean id="playerWriter" class="org.springframework.batch.sample.football.internal.PlayerItemWriter">
<property name="playerDao">
<bean class="org.springframework.batch.sample.domain.football.internal.JdbcPlayerDao">
<bean class="org.springframework.batch.sample.football.internal.JdbcPlayerDao">
<property name="dataSource" ref="dataSource" />
</bean>
</property>
</bean>
<bean id="gameWriter" class="org.springframework.batch.sample.domain.football.internal.JdbcGameDao">
<bean id="gameWriter" class="org.springframework.batch.sample.football.internal.JdbcGameDao">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="summaryWriter" class="org.springframework.batch.sample.domain.football.internal.JdbcPlayerSummaryDao">
<bean id="summaryWriter" class="org.springframework.batch.sample.football.internal.JdbcPlayerSummaryDao">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="playerFileItemReader" class="org.springframework.batch.item.file.FlatFileItemReader">
<property name="resource" value="classpath:data/footballjob/input/${player.file.name}" />
<property name="resource" value="classpath:org/springframework/batch/sample/football/data/${player.file.name}" />
<property name="lineMapper">
<bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
<property name="lineTokenizer">
@@ -55,14 +53,14 @@
</bean>
</property>
<property name="fieldSetMapper">
<bean class="org.springframework.batch.sample.domain.football.internal.PlayerFieldSetMapper" />
<bean class="org.springframework.batch.sample.football.internal.PlayerFieldSetMapper" />
</property>
</bean>
</property>
</bean>
<bean id="gameFileItemReader" class="org.springframework.batch.item.file.FlatFileItemReader">
<property name="resource" value="classpath:data/footballjob/input/${games.file.name}" />
<property name="resource" value="classpath:org/springframework/batch/sample/football/data/${games.file.name}" />
<property name="lineMapper">
<bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
<property name="lineTokenizer">
@@ -71,7 +69,7 @@
</bean>
</property>
<property name="fieldSetMapper">
<bean class="org.springframework.batch.sample.domain.football.internal.GameFieldSetMapper" />
<bean class="org.springframework.batch.sample.football.internal.GameFieldSetMapper" />
</property>
</bean>
</property>
@@ -80,7 +78,7 @@
<bean id="playerSummarizationSource" class="org.springframework.batch.item.database.JdbcCursorItemReader">
<property name="dataSource" ref="dataSource" />
<property name="rowMapper">
<bean class="org.springframework.batch.sample.domain.football.internal.PlayerSummaryMapper" />
<bean class="org.springframework.batch.sample.football.internal.PlayerSummaryMapper" />
</property>
<property name="sql">
<value>
@@ -95,25 +93,6 @@
<property name="verifyCursorPosition" value="${batch.verify.cursor.position}"/>
</bean>
<aop:config>
<aop:aspect id="moduleLogging" ref="logAdvice">
<aop:after
pointcut="execution( * org.springframework.batch.item.ItemWriter+.write(..)) and args(item)"
method="doStronglyTypedLogging" />
</aop:aspect>
<aop:aspect ref="eventAdvice">
<aop:before
pointcut="execution( * org.springframework.batch..Step+.execute(..)) and args(stepExecution)"
method="before" />
<aop:after
pointcut="execution( * org.springframework.batch..Step+.execute(..)) and args(stepExecution)"
method="after" />
<aop:after-throwing throwing="t"
pointcut="execution( * org.springframework.batch..Step+.execute(..)) and args(stepExecution)"
method="onError" />
</aop:aspect>
</aop:config>
<bean id="footballProperties" class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
<property name="properties">
<value>

View File

@@ -0,0 +1,45 @@
DROP TABLE PLAYERS IF EXISTS;
DROP TABLE GAMES IF EXISTS;
DROP TABLE PLAYER_SUMMARY IF EXISTS;
CREATE TABLE PLAYERS (
PLAYER_ID CHAR(8) NOT NULL PRIMARY KEY,
LAST_NAME VARCHAR(35) NOT NULL,
FIRST_NAME VARCHAR(25) NOT NULL,
POS VARCHAR(10) ,
YEAR_OF_BIRTH BIGINT NOT NULL,
YEAR_DRAFTED BIGINT NOT NULL
) ;
CREATE TABLE GAMES (
PLAYER_ID CHAR(8) NOT NULL,
YEAR_NO BIGINT NOT NULL,
TEAM CHAR(3) NOT NULL,
WEEK BIGINT NOT NULL,
OPPONENT CHAR(3) ,
COMPLETES BIGINT ,
ATTEMPTS BIGINT ,
PASSING_YARDS BIGINT ,
PASSING_TD BIGINT ,
INTERCEPTIONS BIGINT ,
RUSHES BIGINT ,
RUSH_YARDS BIGINT ,
RECEPTIONS BIGINT ,
RECEPTIONS_YARDS BIGINT ,
TOTAL_TD BIGINT
) ;
CREATE TABLE PLAYER_SUMMARY (
ID CHAR(8) NOT NULL,
YEAR_NO BIGINT NOT NULL,
COMPLETES BIGINT NOT NULL ,
ATTEMPTS BIGINT NOT NULL ,
PASSING_YARDS BIGINT NOT NULL ,
PASSING_TD BIGINT NOT NULL ,
INTERCEPTIONS BIGINT NOT NULL ,
RUSHES BIGINT NOT NULL ,
RUSH_YARDS BIGINT NOT NULL ,
RECEPTIONS BIGINT NOT NULL ,
RECEPTIONS_YARDS BIGINT NOT NULL ,
TOTAL_TD BIGINT NOT NULL
) ;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2007-2022 the original author or authors.
* Copyright 2007-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,22 +13,27 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.sample;
package org.springframework.batch.sample.football;
import javax.sql.DataSource;
import org.junit.jupiter.api.Test;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.test.JobLauncherTestUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.jdbc.JdbcTestUtils;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SpringJUnitConfig(
locations = { "/simple-job-launcher-context.xml", "/jobs/footballJob.xml", "/job-runner-context.xml" })
@SpringJUnitConfig(locations = { "/simple-job-launcher-context.xml",
"/org/springframework/batch/sample/football/job/footballJob.xml", "/job-runner-context.xml" })
class FootballJobFunctionalTests {
@Autowired
@@ -42,7 +47,7 @@ class FootballJobFunctionalTests {
}
@Test
void testLaunchJob() throws Exception {
void testLaunchJobWithXmlConfiguration() throws Exception {
JdbcTestUtils.deleteFromTables(jdbcTemplate, "PLAYERS", "GAMES", "PLAYER_SUMMARY");
jobLauncherTestUtils.launchJob();
@@ -51,4 +56,20 @@ class FootballJobFunctionalTests {
assertTrue(count > 0);
}
@Test
void testLaunchJobWithJavaConfiguration() throws Exception {
// given
ApplicationContext context = new AnnotationConfigApplicationContext(FootballJobConfiguration.class);
JobLauncher jobLauncher = context.getBean(JobLauncher.class);
Job job = context.getBean(Job.class);
// when
jobLauncher.run(job, new JobParameters());
// then
int count = JdbcTestUtils.countRowsInTable(new JdbcTemplate(context.getBean(DataSource.class)),
"PLAYER_SUMMARY");
assertTrue(count > 0);
}
}

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.sample.domain.football.internal;
package org.springframework.batch.sample.football.internal;
import java.sql.ResultSet;
import java.sql.SQLException;
@@ -24,7 +24,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.batch.item.Chunk;
import org.springframework.batch.sample.domain.football.Game;
import org.springframework.batch.sample.football.Game;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;

View File

@@ -13,14 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.sample.domain.football.internal;
package org.springframework.batch.sample.football.internal;
import javax.sql.DataSource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.batch.sample.domain.football.Player;
import org.springframework.batch.sample.football.Player;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.sample.domain.football.internal;
package org.springframework.batch.sample.football.internal;
import javax.sql.DataSource;
@@ -21,7 +21,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.batch.item.Chunk;
import org.springframework.batch.sample.domain.football.PlayerSummary;
import org.springframework.batch.sample.football.PlayerSummary;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;