Skip to content

Latest commit

 

History

History
293 lines (248 loc) · 11 KB

README.md

File metadata and controls

293 lines (248 loc) · 11 KB

Spring Batch

🛠️ Requisitos

📝 Actividad

Durante esta actividad se crearán dos jobs (bankAccountsBackupJob y printUsersJob) con el fin de conocer diferentes maneras de ejecutarlos.

  • Uno de ellos se ejecutará de forma automática cada cierto periodo de tiempo y simulará el respaldo en un archivo TXT de cuentas existentes en un archivo CSV.
  • El otro se ejecutara de forma manual y realizara un procesamiento sobre una lista de usuarios y escribirá el resultado en consola.

Esta actividad toma como base la última versión del proyecto LearningJava. Descrita en la clase anterior: README

Habilitar Spring Batch

  1. Lo primero que tenemos que realizar es agregar las dependencias de Spring Batch en el archivo pom de nuestro proyecto.

    img.png

bankAccountsBackupJob

  1. Con la dependencia agregada ahora podremos crear el job bankAccountsBackupJob para lo cual sera necesario crear una clase de Configuración (@Configuration) en la cual inyectaremos los objetos de tipo JobBuilderFactory y StepBuilderFactory para crear tanto el Job como un Step.

    @Configuration
    public class BankAccountJob {
    
    // Inyectar dependencias
    
    /**
      * Crea el job y específica steps y listener.
      * @return bankAccountsBackupJob
      */
     @Bean
     public Job bankAccountsBackupJob() {
         return jobBuilderFactory.get("bankAccountsBackupJob")
                 .start(bankAccountsBackupStep(stepBuilderFactory))
                 .listener(jobExecutionListener())
                 .build();
    
     }
    
     /**
      * Se define el step y los procesos para leer, procesar y escribir.
      * @param stepBuilderFactory
      * @return bankAccountsBackupStep
      */
     @Bean
     public Step bankAccountsBackupStep(StepBuilderFactory stepBuilderFactory) {
         return stepBuilderFactory.get("bankAccountsBackupStep")
                 .<BankAccountDTO, String>chunk(5).reader(bankAccountsReader())
                 .processor(bankAccountItemProcessor()).writer(bankAccountsWriter()).build();
     }
    }
  2. En la misma clase creamos un itemReader y un itemWriter para leer de un archivo y escribir en otro.

    @Configuration
    public class BankAccountJob {
     // ..código
     /**
      * Define un itemReader para leer un archivo csv y mapear el contenido usando BankAccountDTO.
      * @return
      */
     @Bean
     public FlatFileItemReader<BankAccountDTO> bankAccountsReader() {
         return new FlatFileItemReaderBuilder<BankAccountDTO>()
                 .name("bankAccountsReader")
                 .resource(new ClassPathResource("csv/accounts.csv"))
                 .delimited().names(new String[] {"country", "accountName", "accountType", "accountBalance", "userName"})
                 .targetType(BankAccountDTO.class).build();
     }
    
     /**
      * Define un itemWriter para escribir en un archivo txt.
      * @return
      */
     @Bean
     public FlatFileItemWriter<String> bankAccountsWriter() {
         return new FlatFileItemWriterBuilder<String>()
                 .name("bankAccountsWriter")
                 .resource(new FileSystemResource(
                         "target/test-outputs/bankAccountsBackup.txt"))
                 .lineAggregator(new PassThroughLineAggregator<>()).build();
     }
    }

    Como se puede observar existe un archivo con extension .csv que se lee desde el folder csv, en este caso el archivo debe existir en la carpeta resources. El archivo contiene la información de algunas cuentas en el formato especificado en el itemReader: "country", "accountName", "accountType", "accountBalance", "userName".

  3. Continuando en la misma clase, creamos dos métodos uno para invocar un itemProcessor y otro para invocar un listener que escuchara cuando un Job ha terminado su ejecución.

    @Configuration
    public class BankAccountJob {
     // ..código
     @Bean
     public BankAccountItemProcessor bankAccountItemProcessor() {
         return new BankAccountItemProcessor();
     }
    
     @Bean
     public JobExecutionListener jobExecutionListener() {
         return new BatchJobCompletionListener();
     }
    }

    Las clases invocadas son las siguientes:

    • Processor img.png
    • Listener img.png
  4. Por último para este job es necesario crear una clase de configuración para controlar su ejecución. En este caso se utilizó la anotación @Scheduled para programar la ejecución cada cierto tiempo.

    public class BatchConfiguration {
     // Inyectar Job y JobLauncher
     /**
      * Ejecuta el job de bankAccount cada 15 segundos, agrega un par de parámetros al job.
      * @throws Exception
      */
     @Scheduled(fixedRate = 15000)
     public void scheduledByFixedRate() throws Exception {
         SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.S");
         LOGGER.info("Batch job starting");
         JobParameters jobParameters = new JobParametersBuilder()
                 .addString("launchDate", format.format(Calendar.getInstance().getTime()))
                 .addString("project", "LearningJava")
                 .toJobParameters();
         jobLauncher.run(bankAccountJob.bankAccountsBackupJob(), jobParameters);
         LOGGER.info("Batch job executed successfully");
     }
    }

    En esta clase es importante utilizar las anotaciones @Configuration, @EnableBatchProcessing y @EnableScheduling.

  5. Ahora podemos ejecutar la aplicación y veriamos el resultado tanto en consola como en el archivo generado por el job.

    • Consola img.png
    • Archivo img.png img.png

printUsersJob

  1. Al igual que en el Job anterior, creamos uno nuevo con las mismas consideraciones.

    @Configuration
    public class UserJob {
     // Código
     @Bean
     public Job printUsersJob() {
         return jobBuilderFactory.get("printUsersJob")
                 .incrementer(new RunIdIncrementer())
                 .flow(printUserStep())
                 .end().listener(new BatchJobCompletionListener())
                 .build();
    
     }
    
     @Bean
     public Step printUserStep() {
         return stepBuilderFactory.get("printUserStep")
                 .<String, String>chunk(3)
                 .reader(new UserReader())
                 .processor(new UserProcessor())
                 .writer(new UserWriter())
                 .build();
     }
    }

    En este caso se hace la invocación directa del listener y las clases ItemReader, ItemProcessor e ItemWriter se crean en archivos separados.

  2. Se crean las clases ItemReader, ItemProcessor e ItemWriter.

    • ItemReader
    public class UserReader implements ItemReader<String> {
     // Código
     private String[] stringArray = {"Phoebe Buffay", "Rachel Green", "Monica Geller", "Chandler Bing", "Ross Geller", "Joey Tribbiani"};
    
     private int index = 0;
    
     @Override
     public String read() throws Exception {
         if (index >= stringArray.length) {
             return null;
         }
         String data = index + " " + stringArray[index];
         index++;
         LOGGER.info("UserReader: Reading data: {}", data);
         return data;
     }
    }
    • ItemProcessor
    public class UserProcessor implements ItemProcessor<String, String> {
     // Código
     @Override
     public String process(String data) throws Exception {
         LOGGER.info("UserProcessor: Processing data: {}", data);
         data = data.toUpperCase();
         return data;
     }
    }
    • ItemWriter
    public class UserWriter implements ItemWriter<String> {
     // Código
     @Override
     public void write(List<? extends String> list) throws Exception {
         for (String data: list) {
             LOGGER.info("UserWriter: Writing data: " + data);
         }
         LOGGER.info("UserWriter: Writing data completed!!");
     }
    }
  3. Este Job se ejecutara de forma manual, por lo tanto sera necesario crear un endpoint (en un nuevo controlador) que invoque su ejecución.

    @Tag(name = "Batch",
         description = "Inicia manualmente un job.")
    @RestController
    @RequestMapping(path = "/batch")
    public class BatchController {
    
     // Código
    
     @GetMapping(path = "/start")
     public ResponseEntity<String> startBatch() {
         JobParameters Parameters = new JobParametersBuilder()
                 .addLong("startAt", System.currentTimeMillis()).toJobParameters();
         try {
             jobLauncher.run(userJob.printUsersJob(), Parameters);
         } catch (JobExecutionAlreadyRunningException | JobRestartException
                  | JobInstanceAlreadyCompleteException | JobParametersInvalidException e) {
             e.printStackTrace();
         }
         return new ResponseEntity<>("Batch Process started!!", HttpStatus.OK);
       }
     }
  4. Por defecto un Job se ejecuta al momento de que se inicia la aplicación, para evitar eso y controlar la ejecución de estos es necesario agregar lo siguiente en nuestro archivo de propiedades (.properties o .yml).

    spring.batch.job.enabled=false
    
  5. Iniciamos la aplicación y ejecutamos de forma manual el Job.

    • Request img.png
    • Result img.png

Extra 💡

Existe una forma de ver tanto los Jobs, como su información (parametros, etc), que han sido ejecutados. Para hacerlo hay que agregar la dependencia de H2 y habilitar su consola mediante el archivo de propiedades.

img.png

 spring.h2.console.enabled=true
 spring.datasource.url=jdbc:h2:mem:testdb
 spring.datasource.username= sa
 spring.datasource.password= password

Podemos acceder a dicha consola mediante la URL: http://localhost:8080/h2-console utilizando los valores definidos en el archivo de propiedades.

img.png

Dentro de la consola podemos consultar la información de los jobs, steps, etc. que se han ejecutado.

img.png img.png

⚠️ Recuerda agregar a la lista de endpoints disponibles sin seguridad a aquellas rutas que lo requieran.

📚 Recursos