Spring Batch wprowadzenie

Spring Batch – co warto wiedzieć o przetwarzaniu wsadowym

Spring Batch to jeden z wielu pod projektów w frameworku Spring. W skrócie służy on do developowania zadań wsadowych, które mają charakter cykliczny lub zadań wykonywanych na żądanie. Mogą to być proste zadania przetwarzania np. cykliczna aktualizacja rekordów w tabeli lub bardziej skomplikowane wielokrokowe zadania przetwarzania/importowania danych.

Jak zdefiniować joba w Spring Batch?

Na początek będziemy potrzebowali odpowiedniej zależności. Ja używam Gradle, więc dodaję zależność gradlową. Dodaję spring-boot-starter-batch:

compile("org.springframework.boot:spring-boot-starter-batch")

A następnie możemy przejść do zdefiniowania odpowiedniego joba. Najprostszego joba można zdefiniować w następujący sposób:

@Configuration
public class SimpleJobClass {

  @Autowired
  public JobBuilderFactory jobBuilderFactory;

  @Autowired
  public StepBuilderFactory stepBuilderFactory;

  @Autowired
  private DataSource datasource;

  @Autowired
  ItemWriter<Person> itemWriter;

  @Bean
  JdbcCursorItemReader<Person> personReader() {
    return new JdbcCursorItemReaderBuilder<Person>()
        .dataSource(datasource)
        .sql("SELECT * from tmp_person")
        .rowMapper(new BeanPropertyRowMapper<>(Person.class))
        .saveState(false)
        .build();
  }

  @Bean
  public PersonProcessor personProcessor() {
    return new PersonProcessor();
  }

  @Bean
  public JdbcBatchItemWriter<Person> personWriter(DataSource dataSource) {
    return new JdbcBatchItemWriterBuilder<Person>()
      .itemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<>())
      .sql("INSERT INTO people (first_name, last_name) VALUES (:firstName, :lastName)")
      .dataSource(dataSource)
      .build();
  }

  @Bean 
  public Step personStep() {
    return stepBuilderFactory.get("personStep")
        .<Person, Person>chunk(100)
        .reader(personReader())
        .processor(personProcessor())
        .writer(itemWriter)
        .build();
  }

  @Bean
  public Job personJob(Step personStep) {
    return jobBuilderFactory.get("personJob")
      .preventRestart()
      .incrementer(new RunIdIncrementer())
      .flow(personStep)
      .end()
      .build();
  }
}

Każda konfiguracja musi zawierać metody które definiują beany:

  • Reader
  • Processor
  • Writer
  • Step
  • Job

 

Procesor ma za zadanie przetworzenie obiektów wejściowych na wyjściowe (w tym przykładzie nie robi w zasadzie nic):

public class PersonProcessor implements ItemProcessor<Person, Person> {

    @Override
    public Person process(Person source) {
        return source;
    }
}

Co warto wiedzieć…

Jeśli chcemy pobrać id joba, by później w jakiś sposób się do niego odwołać(zapisać lub przetwarzać) możemy skorzystać z dwóch rzeczy:

Metoda w komponencie (np. we writerze) oznaczona adnotacją @Before.

@BeforeStep
public void beforeStep(StepExecution stepExecution) {
  this.stepExecution = stepExecution;
}
// ... i dalej
stepExecution.getJobExecution().getJobId()

Gdy korzystamy z @Before, nasz writer powinien mieć scope @StepScop – domyślnie wszystkie beany w springu są singletonami co oznacza, że przechowywanie w nich jakiegokolwiek stanu nie jest wielowątkowo bezpieczne. Możemy zrezygnować z @StepScope wtedy gdy job nie jest uruchamiany równolegle np. uruchamiacie go wraz ze startem aplikacji, jeśli wasze joby mogą działać równolegle konieczna jest adnotacja @StepScope.

Drugi sposób to skorzystanie z listenera:

public class SimpleExecutionListener implements JobExecutionListener {

    public void beforeJob(JobExecution jobExecution) {
        long jobId = jobExecution.getJobId();
        jobExecution.getExecutionContext().put("jobId",jobId);
    }
}

Jak pobrać informacji o jobie w Spring Batch?

Spring Batch przy uruchomieniu tworzy w bazie danych strukturę dla metadanych, co pozwala pobrać nam informacje o statusie joba zarówno podczas jego wykonywania jak i po zakończeniu. Możemy to zrobić poprzez skorzystanie z jobRepository, ma to jednak pewną wadę. Żeby pobrać z repozytorium dane o jobie potrzebujemy jego nazwy i parametrów uruchomieniowych i o ile nazwa joba jest stałą to parametry mogą się różnić, co trochę utrudnia zadanie. Ponieważ w łatwy sposób przy uruchomieniu joba możemy pobrać jego ID, to logiczne jest, że chcieli byśmy pobrać informacje o nim za pomocą tego właśnie ID. Niestety korzystając z jobRepository nie jest to możliwe.

Jedyne co nam pozostaje to napisanie odpowiednich zapytania dla meta tabeli spring batcha:

Żeby pobrać status joba wykonujemy następujące zapytanie:

SELECT status FROM batch_job_execution where job_execution_id=:id

dodatkowo możemy sprawdzić status uruchomionego joba w runtime’ie naszej aplikacji korzystając z job explorera:

Set<JobExecution> jobExecutions = jobExplorer.findRunningJobExecutions(job.getName());

Jak uruchamiać joby w Spring Batch?

Joby można uruchamiać na 3 sposoby:

Odpalanie wraz ze startem aplikacji, co jest domyślnym sposobem, można tym sterować za pomocą propertiesów:

spring.batch:
  initialize-schema: always # inicjalizuje meta tabele spring batch
  job.enabled: true # uruchamia joby - domyślnie true
  job.names: personJob # ogranicza uruchomienie jobów tylko do wybranych

Kolejny sposób to odpalenie z kodu za pomocą job launchera, możemy np. wystawić sobie endpoint restowy i uruchamiać zdalnie joby. Możemy przy tym odpalać je synchronicznie (domyślny sposób) oraz asynchronicznie używając do tego asyncJobLauncher’a:

@Qualifier("asyncJobLauncher") JobLauncher jobLauncher
JobParameters jobParameters = new JobParametersBuilder(jobExplorer)
                .getNextJobParameters(job)
                .toJobParameters();
try {
  return jobLauncher.run(job, jobParameters).getId();
} catch (Exception e) {
  throw new JobExecutionFailed("Job execution failed", e);
}

Trzecim sposobem jest odpalenie joba jako zadania cron, można to zrobić na wiele sposobów np. używając dodatkowych narzędzi jak Quartz, a można też skorzystać z mechanizmu wbudowanego w springa, konfigurowanego za pomocą adnotacji.

@EnableScheduling – włącza w aplikacji możliwość uruchamiania zaplanowanych zadań

@Scheduler –  pozwala skonfigurować uruchamianie zadania na różne sposoby np. @Scheduled(fixedDelay=5000) lub @Scheduled(cron="*/5 * * * * MON-FRI")

podobnie jak przy uruchamianiu joba z REST API korzystamy tutaj także z jobLauncher:

jobLauncher.run(job, jobParameters).getId()

Zabezpieczenie przed wielokrotnym wykonaniem

Czasem chcemy się zabezpieczyć przed wielokrotnym uruchomieniem joba, możemy to zrobić w korzystając z jobExplorera

Set<JobExecution> jobExecutions = jobExplorer.findRunningJobExecutions(job.getName());
if (!jobExecutions.isEmpty()) {
    throw new IllegalArgumentException("Job already running");
}

 

I na koniec najważniejsza rzecz – Testowanie!

Testowanie to najważniejszy element programowania i nic nie zastąpi nam dobrze działających testów automatycznych. Na szczęście spring udostępnia nam odpowiednie narzędzia do testowania integracyjnego jobów.

Poniżej przykładowy test integracyjny napisany przy użyciu Spock Framework

@SpringBootTest
@ContextConfiguration(classes = [YourSpringBootApplication])
class CompanyImportJobTest extends Specification {

  @Autowired
  JobRepository jobRepository
  @Autowired
  JobLauncher jobLauncher
  @Autowired
  @Qualifier("personJob")
  Job importJob

  JobLauncherTestUtils jobLauncherTestUtils

  void setup() {
    jobLauncherTestUtils = new JobLauncherTestUtils()
    jobLauncherTestUtils.setJob(importJob)
    jobLauncherTestUtils.setJobLauncher(jobLauncher)
    jobLauncherTestUtils.setJobRepository(jobRepository)
  }

  def "should add new person"() {
    when:
      JobExecution jobExecution = jobLauncherTestUtils.launchJob()
    then:
      jobExecution.getStatus() == BatchStatus.COMPLETED
  }
}

 

PS. Artykuł został przeniesiony z mojego starego bloga, którego już dawno nie rozwijam. W ten sposób chciałem ocalić go od zapomnienia 😉

 

Źródła:

Strona projektu
Oficjalny Tutorial Springa
Przykłady github

 

Mateusz Dąbrowski

Cześć jestem Mateusz, zajmuję się programowaniem już ponad 12 lat z czego ponad 8 programuję w Javie. Zapraszam Cię do lektury mojego bloga. Możesz przeczytać więcej o mnie >>TUTAJ<<

6 thoughts to “Spring Batch – co warto wiedzieć o przetwarzaniu wsadowym”

  1. Znam już podstawy Pythona i teraz uczę się Scali. Czy powinienem zostawić tylko jeden język? Patrząc na tiobe Python bardzo szybko dogania Jave i zastanawiam się czy za jakiś czas rynek nie zdominują takie prostsze języki programowania jak Python, Ruby, Crystal i Elixir czy Nim. Java zostanie natomiast do babrania się w starych monolitycznych projektach..

    1. Nie ma sensu uczyć się kilku języków na raz. Jeśli znasz podstawy Pythona, to znasz tylko podstawy. Warto najpierw poznać jeden język. Poznać w sensie popisać w nim zawodowo kilka lat. Jak Ci się spodoba zostaniesz na lata. Jak nie zawsze możesz zmienić język.

      Co do Javy to jestem spokojny. Rozwija się. Powstają ciągle nowe frameworki, inne narzędzia. Może Python ją dogoni, ale czy wyprzedzi, nie wiem być może. Napewno żaden z pozostałych języków nie jest dla Javy zagrożeniem? Mam w planach napsać coś o przyszłości Javy i jak ja to widzę, więc zapraszam do śledzenia.

      1. Nie Chodzi mi o to, że Python zastąpi Jave w segmencie Enterprise, tylko że będzie w nim powstawać coraz więcej małych serwisów. Podobno PHP 8 które jest dużo szybsze od Pythona, ma zakusy aby zabrać Javie trochę udziału w większych projektach. PHP 8 bardzo dużo czerpie z Javy i ma JiT. Na zachodzi powstaje bardzo dużo Mikroserwisów z różnymi językami typu Go, Swift, Rust, Kotlin i niektóre firmy odchodzą od monolitów typowo Javowych. Scala miała być nowszą Javą, ale wykorzystuje ją się też w Big Data, jako alternatywę R, Juli czy Pythona.

        1. Heh. Już tyle takich historii słyszałem, że ten język wyprzedzi inny itd. A jakie to ma znaczenie? Jeśli nawet to taka zmiana trwają latami, 5-10 lat. Jest czas, żeby się przesiąść na inny język. Naprawdę nie ma to znaczenia. Jak zaczynałem kilkanaście lat temu było podobnie. Tylko, że ja postanowiłem się rozwijać i z PHP przesiadłem się na Javę.

          Dzisiaj robię coś w Javie, PHP, Pythonie i JS. Bo akurat mam takie potrzeby. Jak będę chciał coś w mobilu zrobić to pewnie użyję Kotlina. Po drodze jeszcze była Scala i GoLang.

          Ale nadal mam swój główny jeżyk i jest nim Java. W nim robię większość rzeczy, chyba że sytuacja na to nie pozwala.

          Jest kilka języków mainstreamowych i ich udział w rynku może się z czasem jakoś tam zmieniać, ale od lat wszystko wygląda bardzo podobnie, więc nie ma się czym martwić 😉

        2. Twoja droga to PHP 5 na Java 6? Teraz gdybyś chciał zmienić technologię to jaki język programowania wybierzesz? Coś podobnego do Javy czyli Kotlin lub Scala?

        3. To zrobiłbym tak samo. Przeszedłbym na Javę (wersja nie ma tu znaczenia). W Javie jest najwięcej ofert pracy (w porównaniu do Scali i Kotlina). Ja nie chciałem być już fullStackiem w php. Chciałem iść w stronę backendu, a Java nadaje się do tego świetnie. Zwłaszcza teraz gdy tak popularne są serwisy Restowe.

          PS. Trochę nie rozumiem do czego dążysz w swoich komentarzach. Chcesz się uczyć Scali czy Kotlina to śmiało ? Nic nie stoi na przeszkodzie ? Jeśli szukasz odpowiedzi na pytanie: „Który język wybrać najlepiej?”, to nie ma takiej odpowiedzi. Wszystkie są tak samo dobre, jeden jest lepszy w jednej rzeczy drugi w innej… Wszystkie są do siebie podobne. Chcesz programować to wybierz jeden, naucz się w nim programować i cały czas udoskonalaj swoje umiejętności.

Komentarze są zamknięte.