Dans ce petit guide, je vais démontrer comment créer une solution basée sur spring boot batch.
Le batch réalisera un import à partir d’un fichier excel pour insérer les données dans une base après les avoir transformées.
Le projet
Téléchargez cette archive et importez là sur IntelliJ IDEA ou Spring Tool Suite (STS). Pour info cette archive contient :
- le pom.xml du projet
- les packages globaux du projet
- le fichier csv contenant les données excel source
- le ddl de la table cible (à exécuter sur une base en local avant de commencer l’exercice)
Business Class
Une fois le projet importé, la première étape sera de créer une classe représentative de notre donnée métier. Ici nous traiterons d’une personne. Je vais donc créer la classe suivante :
src/main/java/hello/Person.java
package hello;
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;
}
}
Intermediate processor
Le paradigme commun dans les processus batchs et de charger les données puis de les transformer et finalement de les charger dans une autre source de données. Ici la transformation sera simple, je vais transformer les attributs « firstname » et « lastname » en majuscule via ce simple processor.
src/main/java/hello/PersonItemProcessor.java
package hello;
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 implémente l’interface ItemProcessor de Spring Batch. Cela rend plus facile le déclenchement du code dans le batch que l’on définira plus bas. Selon cette interface, je reçois un objet Person que l’on transforme dans un même objet Person mais avec les attributs en majuscule.
NB : Il n’y a aucune obligation d’avoir le même objet en entrée et en sortie du processor.
Batch Configuration
Maintenant j’ai besoin d’assembler le batch en question. Spring batch fourni un bon nombre de classes utiles pour réduire le besoin d’écrire du code spécifique. Grâce à cela, on peut se focaliser uniquement sur la logique métier du batch.
src/main/java/hello/BatchConfiguration.java
package hello;
import javax.sql.DataSource;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecutionListener;
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.core.launch.support.RunIdIncrementer;
import org.springframework.batch.item.database.BeanPropertyItemSqlParameterSourceProvider;
import org.springframework.batch.item.database.JdbcBatchItemWriter;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper;
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.ClassPathResource;
import org.springframework.jdbc.core.JdbcTemplate;
@Configuration
@EnableBatchProcessing
public class BatchConfiguration {
@Autowired
public JobBuilderFactory jobBuilderFactory;
@Autowired
public StepBuilderFactory stepBuilderFactory;
@Autowired
public DataSource dataSource;
// tag::readerwriterprocessor[]
@Bean
public FlatFileItemReader reader() {
FlatFileItemReader reader = new FlatFileItemReader();
reader.setResource(new ClassPathResource("sample-data.csv"));
reader.setLineMapper(new DefaultLineMapper() {{
setLineTokenizer(new DelimitedLineTokenizer() {{
setNames(new String[] { "firstName", "lastName" });
}});
setFieldSetMapper(new BeanWrapperFieldSetMapper() {{
setTargetType(Person.class);
}});
}});
return reader;
}
@Bean
public PersonItemProcessor processor() {
return new PersonItemProcessor();
}
@Bean
public JdbcBatchItemWriter writer() {
JdbcBatchItemWriter writer = new JdbcBatchItemWriter<Person>();
writer.setItemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider());
writer.setSql("INSERT INTO people (first_name, last_name) VALUES (:firstName, :lastName)");
writer.setDataSource(dataSource);
return writer;
}
// end::readerwriterprocessor[]
// tag::jobstep[]
@Bean
public Job importUserJob(JobCompletionNotificationListener listener) {
return jobBuilderFactory.get("importUserJob")
.incrementer(new RunIdIncrementer())
.listener(listener)
.flow(step1())
.end()
.build();
}
@Bean
public Step step1() {
return stepBuilderFactory.get("step1")
.<Person, Person> chunk(10)
.reader(reader())
.processor(processor())
.writer(writer())
.build();
}
// end::jobstep[]
}
Quelques explications :
L’annotation @EnableBatchProcessing ajoute pas mal de beans critique aux batchs qui permettent de gagner pas mal de lignes de code.
Le premier morceau de code défini les parties « input », « processor » et « output » du batch :
- reader() crée un ItemReader. Il va regarder après un fichier nommé sample-data.csv et parser chaque ligne pour les transformer en Person
- processor() crée une instance du PersonItemProcessor que l’on a défini un peu plus en amont.
- write(DataSource) crée un ItemWriter. Celui-ci a pour cible une connection JDBC et va automatiquement recevoir une copie de la datasource créer par @EnableBatchProcessing. Cela inclue le code SQL pour insérer une Person.
Le deuxième morceau de code se concentre sur la configuration du batch :
- importUserJob(…) et step1() définissent une seule « step ». Les jobs sont construits à partir de ces steps, où chaque « step » peut inclure un reader, un processor et un writer.
- Dans cette définition de batch, j’ai besoin d’un RunIdIncrementer car le job utilise une base de donnée pour maintenir l’état de l’exécution. Je liste ensuite chaque étape, ici il n’y en a qu’une, l’étape step1().
- Le job se termine ensuite.
- Dans cette configuration, je définis également combien de donnée sont à écrire en même temps. Dans notre cas, il écrit 10 données à la fois. Après, je configure le reader, processor et writer utilisant les données d’auparavant.
Listener de fin de job
src/main/java/hello/JobCompletionNotificationListener.java
package hello;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
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.jdbc.core.RowMapper;
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");
List results = jdbcTemplate.query("SELECT first_name, last_name FROM people", new RowMapper() {
@Override
public Person mapRow(ResultSet rs, int row) throws SQLException {
return new Person(rs.getString(1), rs.getString(2));
}
});
for (Person person : results) {
log.info("Found <" + person + "> in the database.");
}
}
}
}
Cette classe écoute la fin du job avec comme déclencheur un BatchStatus.Completed et ensuite utilise un JdbcTemplate pour inspecter les résultats.
Rendre l’application exécutable
src/main/java/hello/Application.java
package hello;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) throws Exception {
SpringApplication.run(Application.class, args);
}
}
@SpringBootApplication est une annotation pratique qui ajoute les suivantes :
- @Configuration qui tag la classe comme une source de définitions de bean pour le contexte de l’application.
- @EnableAutoConfiguration qui dit à Spring Boot de commencer à ajouter les beans basés sur les paramètres du classpath, d’autres beans et de plusieurs autres propriétés.
- @ComposentScan qui dit à Spring de regarder après d’autres composants, configurations ou services dans le package hello.
La méthode main() utilise la méthode de Spring boot SpringApplication.run() pour lancer l’application. Et voilà, l’application est prête à être lancer et tout ça sans une seule ligne de XML !
La magie de Spring Boot a encore une fois opérée.