Spring Batch - Tasklets vs Chunks

1. Introduksjon

Spring Batch gir to forskjellige måter å implementere en jobb på: ved hjelp av oppgaver og biter.

I denne artikkelen lærer vi hvordan vi konfigurerer og implementerer begge metodene ved hjelp av et enkelt eksempel fra virkeligheten.

2. Avhengigheter

La oss komme i gang med legge til de nødvendige avhengighetene:

 org.springframework.batch spring-batch-core 4.2.0.RELEASE org.springframework.batch spring-batch-test 4.2.0.RELEASE test 

For å få den nyeste versjonen av spring-batch-core og spring-batch-test, se Maven Central.

3. Brukssaken vår

La oss vurdere en CSV-fil med følgende innhold:

Mae Hodges, 10/22/1972 Gary Potter, 02/22/1953 Betty Wise, 02/17/1968 Wayne Rose, 04/06/1977 Adam Caldwell, 09/27/1995 Lucille Phillips, 05/14/1992

De første posisjon på hver linje representerer en persons navn og den andre posisjonen representerer fødselsdatoen.

Vår brukstilfelle er å generere en annen CSV-fil som inneholder hver persons navn og alder:

Mae Hodges, 45 Gary Potter, 64 Betty Wise, 49 Wayne Rose, 40 Adam Caldwell, 22 Lucille Phillips, 25

Nå som domenet vårt er klart, la oss gå videre og bygge en løsning ved hjelp av begge tilnærminger. Vi begynner med oppgaver.

4. Oppgaver tilnærming

4.1. Introduksjon og design

Oppgaver er ment å utføre en enkelt oppgave innen et trinn. Jobben vår vil bestå av flere trinn som utføres etter hverandre. Hvert trinn skal bare utføre en definert oppgave.

Jobben vår vil bestå av tre trinn:

  1. Les linjer fra CSV-filen.
  2. Beregn alder for hver person i input-CSV-filen.
  3. Skriv navn og alder på hver person til en ny CSV-fil.

Nå som det store bildet er klart, la oss lage en klasse per trinn.

LinesReader har ansvaret for å lese data fra inndatafilen:

offentlig klasse LinesReader implementerer oppgave {// ...}

LinesProsessor vil beregne alderen for hver person i filen:

offentlig klasse LinesProcessor implementerer oppgave {// ...}

Endelig, LinesWriter har ansvaret for å skrive navn og alder til en utdatafil:

offentlig klasse LinesWriter implementerer oppgave {// ...}

På dette punktet, alle trinnene våre implementeres Oppgave grensesnitt. Det vil tvinge oss til å implementere det henrette metode:

@Override public RepeatStatus execute (StepContribution stepContribution, ChunkContext chunkContext) kaster unntak {// ...}

Denne metoden er der vi legger til logikken for hvert trinn. Før vi begynner med den koden, la oss konfigurere jobben vår.

4.2. Konfigurasjon

Vi må legg til noen konfigurasjoner i vårens applikasjonskontekst. Etter å ha lagt til standard bønnedeklarasjon for klassene som ble opprettet i forrige seksjon, er vi klare til å lage vår jobbdefinisjon:

@Configuration @EnableBatchProcessing public class TaskletsConfig {@Autowired private JobBuilderFactory jobs; @Autowired private StepBuilderFactory trinn; @Bean-beskyttet trinn readLines () {retur trinn .get ("readLines"). Oppgave (linesReader ()) .build (); } @Bean-beskyttet trinn processLines () {retur trinn .get ("processLines"). Oppgave (linesProcessor ()) .build (); } @Bean-beskyttet trinn writeLines () {retur trinn .get ("writeLines"). Oppgave (linesWriter ()) .build (); } @Bean offentlig jobbjobb () {returner jobber .get ("taskletsJob") .start (readLines ()) .next (processLines ()) .next (writeLines ()) .build (); } // ...}

Dette betyr at vår “TaskletsJob” vil bestå av tre trinn. Den første (readLines) vil utføre oppgaven som er definert i bønnen linjerLeser og gå til neste trinn: prosesslinjer. Prosesslinjer vil utføre oppgaven som er definert i bønnen linjerProsessor og gå til det siste trinnet: writeLines.

Jobbflyten vår er definert, og vi er klare til å legge til litt logikk!

4.3. Modell og verktøy

Når vi skal manipulere linjer i en CSV-fil, skal vi lage en klasse Linje:

offentlig klasse Line implementerer Serializable {private String name; privat LocalDate dob; privat Lang alder; // standard konstruktør, getters, setters og toString implementering}

Vær oppmerksom på at Linje redskaper Serialiserbar. Det er fordi Linje vil fungere som en DTO for å overføre data mellom trinn. I følge Spring Batch, objekter som overføres mellom trinn, må serienummeres.

På den annen side kan vi begynne å tenke på å lese og skrive linjer.

For det bruker vi OpenCSV:

 com.opencsv opencsv 4.1 

Se etter den nyeste OpenCSV-versjonen i Maven Central.

Når OpenCSV er inkludert, vi skal også lage en FileUtils klasse. Det vil gi metoder for lesing og skriving av CSV-linjer:

offentlig klasse FileUtils {public Line readLine () kaster Unntak {hvis (CSVReader == null) initReader (); Streng [] linje = CSVReader.readNext (); hvis (linje == null) returnerer null; returner ny linje (linje [0], LocalDate.parse (linje [1], DateTimeFormatter.ofPattern ("MM / dd / åååå")); } public void writeLine (Line line) kaster Unntak {if (CSVWriter == null) initWriter (); Streng [] lineStr = ny streng [2]; lineStr [0] = line.getName (); lineStr [1] = linje .getAge () .toString (); CSVWriter.writeNext (lineStr); } // ...}

Legg merke til det readLine fungerer som en wrapper over OpenCSV readNext metode og returnerer a Linje gjenstand.

Samme måten, writeLine pakker OpenCSV-er skrivNeste motta en Linje gjenstand. Full implementering av denne klassen finner du i GitHub Project.

På dette punktet er vi klar til å starte med hvert trinn implementering.

4.4. LinesReader

La oss gå videre og fullføre LinesReader klasse:

offentlig klasse LinesReader implementerer Tasklet, StepExecutionListener {private final Logger logger = LoggerFactory .getLogger (LinesReader.class); private Liste linjer; private FileUtils fu; @Override public void beforeStep (StepExecution stepExecution) {lines = new ArrayList (); fu = nye FileUtils ("taskletsvschunks / input / tasklets-vs-chunks.csv"); logger.debug ("Linjeleser initialisert."); } @ Override public RepeatStatus execute (StepContribution stepContribution, ChunkContext chunkContext) kaster Unntak {Line line = fu.readLine (); while (line! = null) {lines.add (line); logger.debug ("Les linje:" + linje.tilString ()); linje = fu.readLine (); } returner RepeatStatus.FINISHED; } @ Override offentlig ExitStatus afterStep (StepExecution stepExecution) {fu.closeReader (); stepExecution .getJobExecution () .getExecutionContext () .put ("linjer", this.lines); logger.debug ("Linjeleser avsluttet."); returner ExitStatus.COMPLETED; }}

LinesReader utfører metoden skaper en FileUtils forekomst over inndatafilen. Deretter, legger til linjer i en liste til det ikke er flere linjer å lese.

Vår klasse også redskaper StepExecutionListener som gir to ekstra metoder: før trinn og etter trinn. Vi bruker disse metodene til å initialisere og lukke ting før og etter henrette løper.

Hvis vi tar en titt på etter trinn kode, vil vi legge merke til linjen der resultatlisten (linjer) blir satt i jobbsammenheng for å gjøre den tilgjengelig for neste trinn:

stepExecution .getJobExecution () .getExecutionContext () .put ("linjer", this.lines);

På dette punktet har vårt første trinn allerede oppfylt sitt ansvar: last CSV-linjer i en Liste i minne. La oss gå til andre trinn og behandle dem.

4.5. LinesProsessor

LinesProsessor vil også implementere StepExecutionListener og selvfølgelig, Oppgave. Det betyr at den vil implementere før trinn, henrette og etter trinn metoder også:

offentlig klasse LinesProcessor implementerer Tasklet, StepExecutionListener {private Logger logger = LoggerFactory.getLogger (LinesProcessor.class); private Liste linjer; @ Overstyr offentlig tomrom førStep (StepExecution stepExecution) {ExecutionContext executionContext = stepExecution .getJobExecution () .getExecutionContext (); this.lines = (List) executContext.get ("linjer"); logger.debug ("Linjeprosessor initialisert."); } @Override public RepeatStatus execute (StepContribution stepContribution, ChunkContext chunkContext) kaster Unntak {for (Line line: lines) {long age = ChronoUnit.YEARS.between (line.getDob (), LocalDate.now ()); logger.debug ("Beregnet alder" + alder + "for linje" + line.toString ()); line.setAge (alder); } returner RepeatStatus.FINISHED; } @Override public ExitStatus afterStep (StepExecution stepExecution) {logger.debug ("Linjeprosessor avsluttet."); returner ExitStatus.COMPLETED; }}

Det er uanstrengt å forstå det den laster linjer liste fra jobbsammenheng og beregner alderen på hver person.

Det er ikke nødvendig å sette en annen resultatliste i konteksten ettersom endringer skjer på det samme objektet som kommer fra forrige trinn.

Og vi er klare for vårt siste skritt.

4.6. LinesWriter

LinesWriter’S oppgave er å gå over linjer liste opp og skriv navn og alder til utdatafilen:

offentlig klasse LinesWriter implementerer Tasklet, StepExecutionListener {private final Logger logger = LoggerFactory .getLogger (LinesWriter.class); private Liste linjer; private FileUtils fu; @ Overstyr offentlig tomrom førStep (StepExecution stepExecution) {ExecutionContext executionContext = stepExecution .getJobExecution () .getExecutionContext (); this.lines = (List) executContext.get ("linjer"); fu = nye FileUtils ("output.csv"); logger.debug ("Lines Writer initialized."); } @ Override public RepeatStatus execute (StepContribution stepContribution, ChunkContext chunkContext) kaster Unntak {for (Linje linje: linjer) {fu.writeLine (linje); logger.debug ("Skrev linje" + linje.tilString ()); } returner RepeatStatus.FINISHED; } @ Override offentlig ExitStatus afterStep (StepExecution stepExecution) {fu.closeWriter (); logger.debug ("Lines Writer slutt."); returner ExitStatus.COMPLETED; }}

Vi er ferdige med implementeringen av jobben! La oss lage en test for å kjøre den og se resultatene.

4.7. Kjører jobben

For å kjøre jobben oppretter vi en test:

@RunWith (SpringJUnit4ClassRunner.class) @ContextConfiguration (classes = TaskletsConfig.class) public class TaskletsTest {@Autowired private JobLauncherTestUtils jobLauncherTestUtils; @Test offentlig ugyldig givenTaskletsJob_whenJobEnds_thenStatusCompleted () kaster Unntak {JobExecution jobExecution = jobLauncherTestUtils.launchJob (); assertEquals (ExitStatus.COMPLETED, jobExecution.getExitStatus ()); }}

Kontekstkonfigurasjon merknader peker på vårkontekstkonfigurasjonsklassen, som har vår stillingsdefinisjon.

Vi må legge til et par ekstra bønner før du kjører testen:

@Bean offentlige JobLauncherTestUtils jobLauncherTestUtils () {returner nye JobLauncherTestUtils (); } @Bean offentlig JobRepository jobRepository () kaster Unntak {MapJobRepositoryFactoryBean fabrikk = ny MapJobRepositoryFactoryBean (); factory.setTransactionManager (transactionManager ()); return (JobRepository) factory.getObject (); } @Bean offentlig PlatformTransactionManager transactionManager () {returner nye ResourcelessTransactionManager (); } @Bean offentlig JobLauncher jobLauncher () kaster Unntak {SimpleJobLauncher jobLauncher = ny SimpleJobLauncher (); jobLauncher.setJobRepository (jobRepository ()); retur jobbLauncher; }

Alt er klart! Gå videre og kjør testen!

Etter at jobben er ferdig, output.csv har forventet innhold og logger viser utførelsesflyten:

[hoved] DEBUG o.b.t.tasklets.LinesReader - Linjeleser initialisert. [hoved] DEBUG obttasklets.LinesReader - Les linje: [Mae Hodges, 10/22/1972] [main] DEBUG obttasklets.LinesReader - Les linje: [Gary Potter, 02/22/1953] [main] DEBUG obttasklets .LinesReader - Les linje: [Betty Wise, 02/17/1968] [hoved] DEBUG obttasklets.LinesReader - Les linje: [Wayne Rose, 04/06/1977] [main] DEBUG obttasklets.LinesReader - Les linje: [Adam Caldwell, 09/27/1995] [main] DEBUG obttasklets.LinesReader - Les linje: [Lucille Phillips, 05/14/1992] [main] DEBUG obttasklets.LinesReader - Lines Reader avsluttet. [main] DEBUG o.b.t.tasklets.LinesProcessor - Linjeprosessor initialisert. [hoved] DEBUG obttasklets.LinesProcessor - Beregnet alder 45 for linje [Mae Hodges, 10/22/1972] [main] DEBUG obttasklets.LinesProcessor - Beregnet alder 64 for linje [Gary Potter, 02/22/1953] [main ] DEBUG obttasklets.LinesProcessor - Beregnet alder 49 for linje [Betty Wise, 02/17/1968] [main] DEBUG obttasklets.LinesProcessor - Beregnet alder 40 for line [Wayne Rose, 04/06/1977] [main] DEBUG obttasklets.LinesProcessor - Beregnet alder 22 for linje [Adam Caldwell, 09/27/1995] [hoved] DEBUG obttasklets.LinesProcessor - Beregnet alder 25 for linje [Lucille Phillips, 05/14/1992] [main] DEBUG obttasklets .LinesProcessor - Linjeprosessor avsluttet. [hoved] DEBUG o.b.t.tasklets.LinesWriter - Lines Writer initialisert. [hoved] DEBUG obttasklets.LinesWriter - Skrev linje [Mae Hodges, 10/22 / 1972,45] [main] DEBUG obttasklets.LinesWriter - Skrev linje [Gary Potter, 02/22 / 1953,64] [main] DEBUG obttasklets.LinesWriter - Skrev linje [Betty Wise, 02/17 / 1968,49] [main] DEBUG obttasklets.LinesWriter - Wrote line [Wayne Rose, 04/06 / 1977,40] [main] DEBUG obttasklets.LinesWriter - Skrev linje [Adam Caldwell, 09/27 / 1995,22] [main] DEBUG obttasklets.LinesWriter - Wrote line [Lucille Phillips, 05/14 / 1992,25] [main] DEBUG obttasklets.LinesWriter - Lines Writer endte .

Det er det for oppgaver. Nå kan vi gå videre til Chunks-tilnærmingen.

5. Chunks Approach

5.1. Introduksjon og design

Som navnet antyder, denne tilnærmingen utfører handlinger over biter av data. Det vil si at i stedet for å lese, behandle og skrive alle linjene på en gang, vil den lese, behandle og skrive en fast mengde poster (klump) om gangen.

Deretter vil den gjenta syklusen til det ikke er flere data i filen.

Som et resultat vil flyten være litt annerledes:

  1. Mens det er linjer:
    • Gjør for X-antall linjer:
      • Les en linje
      • Behandle en linje
    • Skriv X antall linjer.

Så vi må også lage tre bønner for klumporientert tilnærming:

offentlig klasse LineReader {// ...}
offentlig klasse LineProcessor {// ...}
offentlig klasse LinesWriter {// ...}

Før vi går videre til implementering, la oss konfigurere jobben vår.

5.2. Konfigurasjon

Jobbdefinisjonen vil også se annerledes ut:

@Configuration @EnableBatchProcessing offentlig klasse ChunksConfig {@Autowired private JobBuilderFactory jobber; @Autowired private StepBuilderFactory trinn; @Bean public ItemReader itemReader () {return new LineReader (); } @Bean public ItemProcessor itemProcessor () {returner ny LineProcessor (); } @Bean public ItemWriter itemWriter () {return new LinesWriter (); } @Bean-beskyttet Step processLines (ItemReader-leser, ItemProcessor-prosessor, ItemWriter-skribent) {return steps.get ("processLines"). klump (2) .leser (leser). prosessor (prosessor). skribent (skribent) .bygg (); } @Bean offentlig jobbjobb () {returnerer jobber .get ("chunksJob") .start (processLines (itemReader (), itemProcessor (), itemWriter ())) .build (); }}

I dette tilfellet er det bare ett trinn som bare utfører en oppgave.

Imidlertid den oppgaven definerer en leser, en forfatter og en prosessor som vil handle over biter av data.

Merk at forpliktelsesintervall angir mengden data som skal behandles i en del. Jobben vår vil lese, behandle og skrive to linjer om gangen.

Nå er vi klare til å legge til klumplogikken vår!

5.3. LineReader

LineReader vil ha ansvaret for å lese en plate og returnere en Linje instans med innholdet.

For å bli leser, klassen vår må implementere ItemReader grensesnitt:

offentlig klasse LineReader implementerer ItemReader {@Override public Line read () kaster Unntak {Line line = fu.readLine (); if (line! = null) logger.debug ("Les linje:" + line.toString ()); retur linje; }}

Koden er grei, den leser bare en linje og returnerer den. Vi implementerer også StepExecutionListener for den endelige versjonen av denne klassen:

offentlig klasse LineReader implementerer ItemReader, StepExecutionListener {private final Logger logger = LoggerFactory .getLogger (LineReader.class); private FileUtils fu; @ Overstyr offentlig tomrom før trinn (StepExecution stepExecution) {fu = nye FileUtils ("taskletsvschunks / input / tasklets-vs-chunks.csv"); logger.debug ("Line Reader initialized."); } @ Override public Line read () kaster unntak {Line line = fu.readLine (); if (line! = null) logger.debug ("Les linje:" + line.toString ()); retur linje; } @ Override offentlig ExitStatus afterStep (StepExecution stepExecution) {fu.closeReader (); logger.debug ("Linjeleser avsluttet."); returner ExitStatus.COMPLETED; }}

Det skal bemerkes at før trinn og etter trinn utføre henholdsvis før og etter hele trinnet.

5.4. LineProcessor

LineProcessor følger stort sett den samme logikken enn LineReader.

I dette tilfellet vi implementerer ItemProsessor og dens metode prosess():

offentlig klasse LineProcessor implementerer ItemProcessor {private Logger logger = LoggerFactory.getLogger (LineProcessor.class); @Override public Line-prosess (Line line) kaster unntak {long age = ChronoUnit.YEARS .between (line.getDob (), LocalDate.now ()); logger.debug ("Beregnet alder" + alder + "for linje" + line.toString ()); line.setAge (alder); retur linje; }}

De prosess() metoden tar en inngangslinje, behandler den og returnerer en utgangslinje. Igjen implementerer vi også StepExecutionListener:

offentlig klasse LineProcessor implementerer ItemProcessor, StepExecutionListener {private Logger logger = LoggerFactory.getLogger (LineProcessor.class); @Override public void beforeStep (StepExecution stepExecution) {logger.debug ("Line Processor initialized."); } @Override offentlig Linjeprosess (Linjelinje) kaster Unntak {lang alder = ChronoUnit.YEARS .between (line.getDob (), LocalDate.now ()); logger.debug ("Beregnet alder" + alder + "for linje" + line.toString ()); line.setAge (alder); retur linje; } @ Override offentlig ExitStatus afterStep (StepExecution stepExecution) {logger.debug ("Linjeprosessor avsluttet."); returner ExitStatus.COMPLETED; }}

5.5. LinesWriter

I motsetning til leser og prosessor, LinesWriter vil skrive en hel del med linjer slik at den får en Liste av Linjer:

offentlig klasse LinesWriter implementerer ItemWriter, StepExecutionListener {private final Logger logger = LoggerFactory .getLogger (LinesWriter.class); private FileUtils fu; @ Overstyr offentlig tomrom før Step (StepExecution stepExecution) {fu = nye FileUtils ("output.csv"); logger.debug ("Line Writer initialized."); } @ Override public void write (List lines) kaster Unntak {for (Line line: lines) {fu.writeLine (line); logger.debug ("Skrev linje" + linje.tilString ()); }} @ Override offentlig ExitStatus afterStep (StepExecution stepExecution) {fu.closeWriter (); logger.debug ("Line Writer slutt."); returner ExitStatus.COMPLETED; }}

LinesWriter kode taler for seg selv. Og igjen, vi er klare til å teste jobben vår.

5.6. Kjører jobben

Vi oppretter en ny test, den samme som den vi opprettet for oppgavene:

@RunWith (SpringJUnit4ClassRunner.class) @ContextConfiguration (klasser = ChunksConfig.class) offentlig klasse ChunksTest {@Autowired private JobLauncherTestUtils jobLauncherTestUtils; @Test offentlig ugyldig givenChunksJob_whenJobEnds_thenStatusCompleted () kaster unntak {JobExecution jobExecution = jobLauncherTestUtils.launchJob (); assertEquals (ExitStatus.COMPLETED, jobExecution.getExitStatus ()); }}

Etter konfigurering ChunksConfig som forklart ovenfor for TaskletsConfig, vi er klare til å kjøre testen!

Når jobben er gjort, kan vi se det output.csv inneholder forventet resultat igjen, og loggene beskriver flyten:

[main] DEBUG o.b.t. chunks.LineReader - Line Reader initialisert. [hoved] DEBUG o.b.t. chunks.LinesWriter - Line Writer initialisert. [hoved] DEBUG o.b.t.chunks.LineProcessor - Line Processor initialisert. [hoved] DEBUG obtchunks.LineReader - Les linje: [Mae Hodges, 10/22/1972] [main] DEBUG obtchunks.LineReader - Les linje: [Gary Potter, 02/22/1953] [main] DEBUG obtchunks .LineProcessor - Beregnet alder 45 for linje [Mae Hodges, 10/22/1972] [hoved] DEBUG obtchunks.LineProcessor - Beregnet alder 64 for linje [Gary Potter, 02/22/1953] [main] DEBUG obtchunks.LinesWriter - Skrev linje [Mae Hodges, 10/22 / 1972,45] [main] DEBUG obtchunks.LinesWriter - Wrote line [Gary Potter, 02/22 / 1953,64] [main] DEBUG obtchunks.LineReader - Les linje: [Betty Wise, 02/17/1968] [main] DEBUG obtchunks.LineReader - Les linje: [Wayne Rose, 04/06/1977] [main] DEBUG obtchunks.LineProcessor - Beregnet alder 49 for linje [Betty Wise, 02/17/1968] [main] DEBUG obtchunks.LineProcessor - Beregnet alder 40 for linje [Wayne Rose, 04/06/1977] [main] DEBUG obtchunks.LinesWriter - Skrev linje [Betty Wise, 02/17/1968 , 49] [main] DEBUG obtchunks.LinesWriter - Skrev linje [Wayne Rose, 04/06 / 1977,40] [main] DEBUG ob t.chunks.LineReader - Les linje: [Adam Caldwell, 09/27/1995] [main] DEBUG obtchunks.LineReader - Read line: [Lucille Phillips, 05/14/1992] [main] DEBUG obtchunks.LineProcessor - Beregnet alder 22 for linje [Adam Caldwell, 09/27/1995] [main] DEBUG obtchunks.LineProcessor - Beregnet alder 25 for line [Lucille Phillips, 05/14/1992] [main] DEBUG obtchunks.LinesWriter - Wrote line [Adam Caldwell, 09/27 / 1995,22] [main] DEBUG obtchunks.LinesWriter - Skrev linje [Lucille Phillips, 05/14 / 1992,25] [main] DEBUG obtchunks.LineProcessor - Line Processor avsluttet. [hoved] DEBUG o.b.t. chunks.LinesWriter - Line Writer avsluttet. [hoved] DEBUG o.b.t.chunks.LineReader - Line Reader avsluttet.

Vi har samme resultat og en annen flyt. Logger viser tydelig hvordan jobben utføres etter denne tilnærmingen.

6. Konklusjon

Ulike sammenhenger vil vise behovet for en eller annen tilnærming. Mens oppgavene føles mer naturlige for 'den ene oppgaven etter den andre' scenarier, gir biter en enkel løsning for å håndtere paginerte avlesninger eller situasjoner der vi ikke vil ha en betydelig mengde data i minnet.

Den komplette implementeringen av dette eksemplet finner du i GitHub-prosjektet.


$config[zx-auto] not found$config[zx-overlay] not found