Accueil > Java EE > Premiers pas avec Spring Batch 3 et ses annotations

Premiers pas avec Spring Batch 3 et ses annotations

A quoi sert Spring Batch ?

Spring Batch 3 est un framework qui offre de nombreuses fonctionnalités permettant de traiter de gros volumes de données avec de bonnes performances.

On peut par exemple citer :

– Reprise d’un batch après un arrêt inattendu

– Gestion des transactions

Mise en situation

Afin d’éprouver un peu Spring Batch 3, j’ai dans un premier temps cherché un “gros” fichier à intégrer.

Sur ce site http://download.geonames.org/export/dump/?C=S;O=D j’ai récupéré le fichier allCountries.zip. Ce qui donne un fichier CSV d’1Go.

Le fichier contient toutes les villes du monde (environ 9 millions) avec quelques données pour chaque ville :

  • noms alternatifs
  • latittude
  • longitude

Le but de ce tutorial est d’enregistrer toutes les données dans une base MongoDB.

Le code de ce petit projet est disponible sur le repository Git de Viseo : https://github.com/Viseo/spring-batch-import-cities.

Un peu de théorie …

Avec Spring Batch on configure un Job. Un job peut avoir une ou plusieurs étapes “ Steps”. En général on configure chaque step pour :

  • lire une donnée à partir d’une source (ItemReader)
  • la transformer (ItemProcessor)
  • l’écrire (ItemWriter)

Processus de traitement de Spring Batch

Préparation du projet avec Spring Batch 3

 

Il s’agit d’un projet Maven classique en java 7 avec les dépendances Spring suivantes :

<!-- Spring Batch -->
<dependency>
  <groupId>org.springframework.batch</groupId>
  <artifactId>spring-batch-core</artifactId>
  <version>3.0.0.RELEASE</version>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-batch</artifactId>
  <version>1.0.2.RELEASE</version>
</dependency>
<!-- Spring data mongodb -->
<dependency>
  <groupId>org.springframework.data</groupId>
  <artifactId>spring-data-mongodb</artifactId>
  <version>1.5.0.RELEASE</version>
</dependency>

Configuration du Job

Beaucoup d’exemples sur Internet montrent comment configurer un job à partir de fichiers de configuration XML. J’ai donc fait un exemple avec des annotations pour changer.

Quelques beans

Afin de lire, transformer et écrire les données en base mongo, j’ai créé 3 beans :

  • CityCsv : bean à l’image des lignes contenues dans le fichier des villes
  • City et Feature : beans représentant mon modèle de données

Classe de configuration BatchConfiguration

@Configuration
@EnableBatchProcessing
public class BatchConfiguration {
  …
}

L’annotation @EnableBatchProcessing, permet le chargement d’une configuration par défaut avec entre autre la création d’une base en mémoire pour sauvegarder l’état du job et des « steps ». Evidemment pas de reprise possible avec cette base en mémoire.

Lecture des données : ItemReader

Pour lire les données il suffit de configurer un ItemReader avec :

  • Le type de la source en instanciant un FlatFileItemReader qui comme son nom l’indique sert à lire des fichiers plats (csv, txt). On peut facilement lire d’autres types de source xml, base de données, … en utilisant les classes fournies avec spring batch)
  • La source de données (resource) : par exemple un fichier avec un chemin absolu ou un chemin du classpath (Exemple avec allCountriesSample.txt qui contient quelques données pour tester le bacth)
  • Le type de bean représentant une ligne (lineMapper) : ici pour chaque ligne on va instancier un CityCsv
  • La manière de mapper les données dans un objet CityCsv :
    • Ici chaque champ est séparé de l’autre par une tabulation (delimiter). Mais on pourrait avoir des champs avec une taille fixe ou respectant une certaine expression régulière.
    • L’ordre des colonnes avec le champ du bean CityCsv associé (names).
@Bean
public ItemReader<CityCsv> reader() {
    FlatFileItemReader<CityCsv> reader = new FlatFileItemReader<CityCsv>();
    final ClassPathResource resource = new ClassPathResource("allCountriesSample.txt");
    reader.setResource(resource);    
    reader.setLineMapper(new DefaultLineMapper<CityCsv>() {{
      setLineTokenizer(new DelimitedLineTokenizer() {{
         setNames(new String[] { "id","name","asciiname","alternatenames","latitude","longitude","featureClass","featureCode","countryCode","cc2","admin1Code",
                 "admin2Code","admin3Code","admin4Code","population","elevation","dem","timezone","modificationDate"});
         setDelimiter(DelimitedLineTokenizer.DELIMITER_TAB);
      }});
      setFieldSetMapper(new BeanWrapperFieldSetMapper<CityCsv>() {{
         setTargetType(CityCsv.class);
      }});
    }}); return reader;
}

Voilà la définition de l’itemReader n’est pas plus difficile que ça.

Transformation des données : ItemProcessor

Avant d’enregistrer les villes extraites du fichier, j’ai besoin de faire certaines opérations :

  • Obtenir des objets Date à partir de différents formats de date
  • Transformer des champs « à plat » en liste. Par exemple avec les noms alternatifs des villes

Je crée donc un CityItemProcessor qui va me permettre de transformer un CityCsv en City avant l’écriture en base. Dans BatchConfiguration :

@Bean 
public ItemProcessor<CityCsv, City> processor() {
    return new CityItemProcessor(); 
}

Dans CityItemProcessor :

@Override
public City process(CityCsv cityCsv) throws Exception {

    final City city = new City();
    city.setId(cityCsv.getId());
    final String alternateNames = cityCsv.getAlternatenames();
    if (alternateNames != null && !alternateNames.isEmpty()) {
        city.setAlternateNames(Lists.newArrayList(alternateNames.split(",")));
    }
    ...    return city;
}

Ecriture des données en base : ItemWriter

Il reste à écrire chaque « City » en base. Pour cela on configure un ItemWriter dans BatchConfiguration :

@Bean
public ItemWriter<City> writer(MongoOperations template) {
    final MongoItemWriter<City> mongoItemWriter = new MongoItemWriter<City>();
    mongoItemWriter.setTemplate(template);
    mongoItemWriter.setCollection("city");
    return mongoItemWriter;
}

J’ai choisi d’enregistrer les données dans une base mongoDb pour sa facilité d’utilisation. J’ai évidemment utilisé la classe MongoItemWriter fournie par spring-batch. Mais on peut tout aussi facilement enregistré dans une base MySQL, Oracle, … ou dans fichier csv, xml, …

Exécution de l’ensemble

Mon batch ne contient qu’une seule étape « Step », déclarée dans BatchConfiguration. Dans ce step on déclare les reader/processor/writer qui vont être utilisés.

@Bean
public Step step1(StepBuilderFactory stepBuilderFactory, 
      ItemReader<CityCsv> reader, ItemWriter<City> writer, 
      ItemProcessor<CityCsv, City> processor) {
    return stepBuilderFactory.get("step1")
            .<CityCsv, City> chunk(10)
            .reader(reader)
            .processor(processor)
            .writer(writer)
            .build();
}

Et enfin la déclaration du job en lui même :

@Bean
public Job importCitiesJob(JobBuilderFactory jobs, Step s1) {
    return jobs.get("importCitiesJob")
            .incrementer(new RunIdIncrementer())
            .flow(s1)
            .end()
            .build();
}

Quelques explications sur les méthodes utilisées ci-dessus :

jobs.get(« importCitiesJob »)

Permet de créer un JobBuilder. On passe en paramètre le nom du job souhaité.

 

incrementer()

Chaque instance de job a des paramètres (JobParameters). Pour pouvoir démarrer 2 instances du même job, il faut qu’il y ait au moins un paramètre différent. La méthode incrementer() permet de setter les paramètres initiaux.

En utilisant RunIdIncrementer cela initialise un paramètre run.id. Celui-ci vaudra 1 à l’exécution de la première instance du job importCitiesJob. A l’exécution d’une deuxième instance du job, ce paramètre sera initialisé à 2, etc.

On pourrait créer notre propre Incrementer qui aurait un paramètre date incrémenté à chaque lancement.

 

flow(s1)

Cette méthode construit un JobFlowBuilder et indique la première étape à démarrer (celle passée en paramètre).

Exemple pour configurer l’enchaînement d’une deuxième étape :

On crée une deuxième étape ‘step2′ qui fait un System.out.pintln de « Hello World »

@Bean
public Step step2(StepBuilderFactory stepBuilderFactory) {
    return stepBuilderFactory.get("step2").tasklet(new HelloWorldTasklet())
           .build();
}

On modifie la définition de notre job pour prendre en paramètre les 2 étapes et on indique que l’étape 2 suit l’étape 1 (méthode next()) :

@Bean
public Job importCitiesJob(JobBuilderFactory jobs, @Qualifier("step1")Step s1, 
@Qualifier("step2")Step s2) {
    return jobs.get("importCitiesJob")
            .incrementer(new RunIdIncrementer())
            .flow(s1).next(s2)
            .end()
            .build();
}

Les deux étapes s’exécuteront dans le bon ordre.

 

end() – méthode de JobFlowBuilder

Permet de construire le JobFlow

build() – méthode de JobBuilder

Permet de construire un Job qui exécute un Flow qui exécute les étapes qu’il contient.

 

Performances

Pour lire/modifier/écrire les 9 millions de villes il faut à ce batch 1h22. Ci-dessous un graph de la mémoire utilisée :

Spring batch Memoire utilisee

Spring batch Mémoire utilisée

Compléments

  • Le projet en pièce jointe contient une classe Application avec un main pour lancer le job sur quelques lignes.
  • Dans la déclaration du step, on peut utiliser un ItemReadListener pour faire des traitements avant/après lecture et lorsqu’il y a une erreur. (Il existe la même fonctionnalité lors de la transformation et de l’écriture)
  • En mettant chunk(10), j’indique que 10 éléments vont être stockés en mémoire avant d’être enregistrés. Ce paramétrage peut être très utile quand on veut enregistrer des données dans une base relationnelle. Au lieu de d’enregistrer chaque ville une par une, on fait un traitement par lot. Cela réduit le temps utilisé pour ouvrir et fermer des transactions et donc réduit la durée du batch. Attention cependant à la consommation de la mémoire !
  • Mon fichier contient des villes potentiellement en erreur (format de date KO par exemple). J’ai donc ajouté une « tolérance » qui permet de ne pas arrêter le job tant que je n’ai pas atteint les 200 000 exceptions.
    .faultTolerant().skip(Exception.class).skipLimit(200000)

Conclusion

J’ai déjà utilisé Spring-Batch dans plusieurs projets et voici mon feed-back sur ce framework :

  • L’utilisation des classes fournies par Spring-Batch permet de ne pas ré-inventer la roue à chaque fois (lecture d’un fichier csv ligne par ligne, écriture dans un fichier xml, …).
  • Les temps de traitement sont plutôt bons.
  • Entre la version 1 et la version 3, le paramétrage des différents éléments (steps/reader/writer/…) a été considérablement simplifié. On peut donc créer un batch en 20 minutes.

Pour aller plus loin, on peut s’intéresser à la fonctionnalité de reprise du job. Dans ce cas là il configurer le job pour enregistrer les données des étapes (date de début, date de fin, état, …) en base. On peut même utiliser une console mise à disposition par Spring Batch Admin pour visualiser l’avancement des jobs et les relancer si nécessaire.

Vous pouvez maintenant tester le job en récupérant les sources sur le repository Git de Viseo : https://github.com/Viseo/spring-batch-import-cities.

Categories: Java EE Tags: , , , ,


× 7 = quatorze