Proljetna serija - Tasklets vs Chunks

1. Uvod

Spring Batch pruža dva različita načina za provođenje posla: korištenje taskleta i dijelova.

U ovom ćemo članku naučiti kako konfigurirati i implementirati obje metode koristeći jednostavan primjer iz stvarnog života.

2. Ovisnosti

Krenimo od dodavanje potrebnih ovisnosti:

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

Da biste dobili najnoviju verziju spring-batch-core i spring-batch-testa, pogledajte Maven Central.

3. Naš slučaj upotrebe

Razmotrimo CSV datoteku sa sljedećim sadržajem:

Mae Hodges, 22.10.1972. Gary Potter, 22.02.1953. Betty Wise, 17.02.1968. Wayne Rose, 04.06.1977. Adam Caldwell, 27.09.1999. Lucille Phillips, 14.05.1992.

The prvi položaj svake crte predstavlja ime osobe, a drugi položaj predstavlja datum njenog rođenja.

Naš slučaj upotrebe je da generirajte drugu CSV datoteku koja sadrži ime i dob svake osobe:

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

Sad kad je naša domena jasna, krenimo dalje i gradimo rješenje koristeći oba pristupa. Počet ćemo s taskletama.

4. Pristup zadacima

4.1. Uvod i dizajn

Taskleti su namijenjeni izvršavanju jednog zadatka unutar koraka. Naš posao sastojat će se od nekoliko koraka koji se izvršavaju jedan za drugim. Svaki korak treba izvršiti samo jedan definirani zadatak.

Naš posao sastojat će se od tri koraka:

  1. Čitajte retke iz ulazne CSV datoteke.
  2. Izračunajte dob za svaku osobu u ulaznoj CSV datoteci.
  3. Napišite ime i dob svake osobe u novu izlaznu CSV datoteku.

Sad kad je velika slika spremna, stvorimo jedan razred po koraku.

LinesReader bit će zadužen za čitanje podataka iz ulazne datoteke:

javna klasa LinesReader implementira Tasklet {// ...}

LinesProcessor izračunat će dob za svaku osobu u datoteci:

javna klasa LinesProcessor implementira Tasklet {// ...}

Konačno, LinesWriter imat će odgovornost za pisanje imena i dobi u izlaznu datoteku:

javna klasa LinesWriter implementira Tasklet {// ...}

U ovom trenutku, sve naše korake provesti Zadatak sučelje. To će nas prisiliti da provedemo svoje izvršiti metoda:

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

Ovom ćemo metodom dodati logiku za svaki korak. Prije početka s tim kodom, konfigurirajmo svoj posao.

4.2. Konfiguracija

Moramo dodajte neku konfiguraciju u kontekst aplikacije Spring. Nakon dodavanja standardne deklaracije graha za klase stvorene u prethodnom odjeljku, spremni smo stvoriti našu definiciju posla:

@Configuration @EnableBatchProcessing javna klasa TaskletsConfig {@Autowired private JobBuilderFactory jobs; @Automobilski privatni koraci StepBuilderFactory; @Bean zaštićeni korak readLines () {povratak koraka .get ("readLines") .tasklet (linesReader ()) .build (); } @Bean zaštićeni korak processLines () {povratak koraka .get ("processLines") .tasklet (linesProcessor ()) .build (); } @Bean zaštićeni korak writeLines () {povratak koraka .get ("writeLines") .tasklet (linesWriter ()) .build (); } @Bean public Job job () {return jobs .get ("taskletsJob") .start (readLines ()) .next (processLines ()) .next (writeLines ()) .build (); } // ...}

To znači da naš “TaskletsJob” sastojat će se od tri koraka. Prvi (readLines) izvršit će tasklet definiran u grahu linesReader i prijeđite na sljedeći korak: linije procesa. ProcessLines izvršit će tasklet definiran u grahu linijeProcesor i prijeđite na zadnji korak: writeLines.

Naš tijek posla je definiran i spremni smo dodati malo logike!

4.3. Model i uređaji

Kako ćemo manipulirati linijama u CSV datoteci, stvorit ćemo klasu Crta:

linija javne klase implementira Serializable {ime privatnog niza; privatno LocalDate dob; privatno Duga dob; // standardni konstruktor, getteri, postavljači i implementacija toString}

Imajte na umu da Crta provodi Serijalizirati. To je zato što Crta djelovat će kao DTO za prijenos podataka između koraka. Prema Spring Batchu, objekti koji se prenose između koraka moraju biti serializirani.

S druge strane, možemo početi razmišljati o čitanju i pisanju redaka.

Za to ćemo se poslužiti OpenCSV-om:

 com.opencsv opencsv 4.1 

Potražite najnoviju verziju OpenCSV u Maven Central.

Jednom kada je uključen OpenCSV, također ćemo stvoriti a FileUtils razred. Pružit će metode za čitanje i pisanje CSV redaka:

javna klasa FileUtils {javna linija readLine () baca iznimku {if (CSVReader == null) initReader (); Niz [] redak = CSVReader.readNext (); if (line == null) return null; vrati novu liniju (linija [0], LocalDate.parse (linija [1], DateTimeFormatter.ofPattern ("MM / dd / gggg"))); } public void writeLine (Line line) baca iznimku {if (CSVWriter == null) initWriter (); String [] lineStr = novi niz [2]; lineStr [0] = line.getName (); lineStr [1] = line .getAge () .toString (); CSVWriter.writeNext (lineStr); } // ...}

Primijeti da readLine djeluje kao omot preko OpenCSV-a readNext metoda i vraća a Crta objekt.

Isti način, writeLine obavija OpenCSV-ove writeNext primanje a Crta objekt. Potpuna implementacija ove klase može se naći u projektu GitHub.

U ovom smo trenutku svi spremni započeti sa svakim korakom implementacije.

4.4. LinesReader

Idemo naprijed i dovršimo naše LinesReader razred:

javna klasa LinesReader implementira Tasklet, StepExecutionListener {private final Logger logger = LoggerFactory .getLogger (LinesReader.class); privatne linije s popisa; privatni FileUtils fu; @Override public void beforeStep (StepExecution stepExecution) {lines = new ArrayList (); fu = novi FileUtils ("taskletsvschunks / input / tasklets-vs-chunks.csv"); logger.debug ("Inicijaliziran čitač linija."); } @Override public RepeatStatus izvršavanje (StepContribution stepContribution, ChunkContext chunkContext) baca iznimku {Line line = fu.readLine (); while (linija! = null) {linije.add (linija); logger.debug ("Čitaj redak:" + line.toString ()); linija = fu.readLine (); } povratak RepeatStatus.FINISHED; } @Override javni ExitStatus afterStep (StepExecution stepExecution) {fu.closeReader (); stepExecution .getJobExecution () .getExecutionContext () .put ("linije", this.lines); logger.debug ("Čitač linija je završen."); povratak ExitStatus.COMPLETED; }}

Izvršenje LinesReader-a metoda stvara a FileUtils primjer preko putanje ulazne datoteke. Zatim, dodaje retke na popis dok više nema redaka za čitanje.

Naš razred također provodi StepExecutionListener koji pruža dvije dodatne metode: prijeKorak i nakonKorak. Te ćemo metode koristiti za inicijalizaciju i zatvaranje stvari prije i poslije izvršiti trči.

Ako pogledamo nakonKorak koda, primijetit ćemo redak u kojem se nalazi popis rezultata (linije) stavlja se u kontekst posla kako bi bio dostupan za sljedeći korak:

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

U ovom je trenutku naš prvi korak već ispunio svoju odgovornost: učitavanje CSV linija u Popis u sjećanju. Prijeđimo na drugi korak i obradimo ih.

4.5. LinesProcessor

LinesProcessor također će provesti StepExecutionListener i naravno, Zadatak. To znači da će se provesti prijeKorak, izvršiti i nakonKorak metode također:

javna klasa LinesProcessor implementira Tasklet, StepExecutionListener {private Logger logger = LoggerFactory.getLogger (LinesProcessor.class); privatne linije s popisa; @Override public void beforeStep (StepExecution stepExecution) {ExecutionContext ExecuContext = stepExecution .getJobExecution () .getExecutionContext (); this.lines = (Popis) ExecuContext.get ("retci"); logger.debug ("Procesor linija je inicijaliziran."); } @Override public RepeatStatus execute (StepContribution stepContribution, ChunkContext chunkContext) baca iznimku {for (Line line: lines) {long age = ChronoUnit.YEARS.between (line.getDob (), LocalDate.now ()); logger.debug ("Izračunata dob" + dob + "za liniju" + line.toString ()); line.setAge (dob); } povratak RepeatStatus.FINISHED; } @Override javni ExitStatus afterStep (StepExecution stepExecution) {logger.debug ("Procesor linija je završio."); povratak ExitStatus.COMPLETED; }}

Lako je to razumjeti učitava linije popis iz konteksta posla i izračunava dob svake osobe.

Nema potrebe stavljati drugi popis rezultata u kontekst jer se izmjene događaju na istom objektu koji dolazi iz prethodnog koraka.

I spremni smo za naš posljednji korak.

4.6. LinesWriter

LinesWriterZadatak je prijeći preko linije napišite i u izlaznu datoteku napišite ime i dob:

javna klasa LinesWriter implementira Tasklet, StepExecutionListener {private final Logger logger = LoggerFactory .getLogger (LinesWriter.class); privatne linije s popisa; privatni FileUtils fu; @Override public void beforeStep (StepExecution stepExecution) {ExecutionContext ExecuContext = stepExecution .getJobExecution () .getExecutionContext (); this.lines = (Popis) ExecuContext.get ("retci"); fu = novi FileUtils ("output.csv"); logger.debug ("Pokretanje programa Lines Writer."); } @Override public RepeatStatus izvršavanje (StepContribution stepContribution, ChunkContext chunkContext) baca iznimku {for (Line line: lines) {fu.writeLine (line); logger.debug ("Napisao redak" + line.toString ()); } povratak RepeatStatus.FINISHED; } @Override javni ExitStatus afterStep (StepExecution stepExecution) {fu.closeWriter (); logger.debug ("Lines Writer je završio."); povratak ExitStatus.COMPLETED; }}

Završili smo s provedbom našeg posla! Stvorimo test da ga pokrenemo i vidimo rezultate.

4.7. Pokretanje posla

Da bismo pokrenuli posao, stvorit ćemo test:

@RunWith (SpringJUnit4ClassRunner.class) @ContextConfiguration (classes = TaskletsConfig.class) javna klasa TaskletsTest {@Autowired private JobLauncherTestUtils jobLauncherTestUtils; @Test javna praznina givenTaskletsJob_whenJobEnds_thenStatusCompleted () baca izuzetak {JobExecution jobExecution = jobLauncherTestUtils.launchJob (); assertEquals (ExitStatus.COMPLETED, jobExecution.getExitStatus ()); }}

ContextConfiguration napomena upućuje na klasu konfiguracije konteksta Spring koja ima našu definiciju posla.

Prije pokretanja testa trebat ćemo dodati nekoliko dodatnih graha:

@Bean public JobLauncherTestUtils jobLauncherTestUtils () {return new JobLauncherTestUtils (); } @Bean public JobRepository jobRepository () baca izuzetak {MapJobRepositoryFactoryBean factory = new MapJobRepositoryFactoryBean (); factory.setTransactionManager (actionManager ()); return (JobRepository) factory.getObject (); } @Bean public PlatformTransactionManageractionManager () {return new ResourcelessTransactionManager (); } @Bean public JobLauncher jobLauncher () baca iznimku {SimpleJobLauncher jobLauncher = new SimpleJobLauncher (); jobLauncher.setJobRepository (jobRepository ()); povratak jobLauncher; }

Sve je spremno! Samo naprijed i pokrenite test!

Nakon završetka posla, izlaz.csv ima li očekivani sadržaj i zapisnici pokazuju tijek izvršenja:

[glavna] DEBUG o.b.t.tasklets.LinesReader - Inicijaliziran čitač linija. [main] DEBUG obttasklets.LinesReader - Čitaj redak: [Mae Hodges, 22.10.1972.] [main] DEBUG obttasklets.LinesReader - Čitaj redak: [Gary Potter, 22.2.1953.] [main] DEBUG obttasklets .LinesReader - Čitaj redak: [Betty Wise, 17.02.1968.] [Glavna] DEBUG obttasklets.LinesReader - Čitaj redak: [Wayne Rose, 04/06/1977] [glavna] DEBUG obttasklets.LinesReader - Čitaj redak: [Adam Caldwell, 27. srpnja 1995.] [glavna] DEBUG obttasklets.LinesReader - Pročitajte liniju: [Lucille Phillips, 05. 14. 1992.] [glavna] DEBUG obttasklets.LinesReader - Čitač linija završena. [glavna] DEBUG o.b.t.tasklets.LinesProcessor - Inicijaliziran procesor linija. [main] DEBUG obttasklets.LinesProcessor - Izračunata dob 45 godina za liniju [Mae Hodges, 10/22 / 1972] [main] DEBUG obttasklets.LinesProcessor - Izračunata dob 64 godina za liniju [Gary Potter, 22.02.1953] [glavna ] DEBUG obttasklets.LinesProcessor - Izračunata dob 49 za liniju [Betty Wise, 17.02.1968.] [Glavna] DEBUG obttasklets.LinesProcessor - Izračunata starost 40 za liniju [Wayne Rose, 04/06/1977] [glavna] DEBUG obttasklets.LinesProcessor - Izračunata dob 22 za liniju [Adam Caldwell, 27.09.1995.] [glavna] DEBUG obttasklets.LinesProcessor - Izračunata starost 25 godina za liniju [Lucille Phillips, 14.05.1992.] [glavna] DEBUG obttasklets .LinesProcessor - Procesor linija je završen. [glavna] DEBUG o.b.t.tasklets.LinesWriter - Pokretanje programa Lines Writer. [main] DEBUG obttasklets.LinesWriter - Napisana linija [Mae Hodges, 10/22 / 1972,45] [main] DEBUG obttasklets.LinesWriter - Napisana linija [Gary Potter, 22.02.1953,64] [main] DEBUG obttasklets.LinesWriter - Napisana linija [Betty Wise, 02/17 / 1968,49] [glavna] DEBUG obttasklets.LinesWriter - Napisana linija [Wayne Rose, 04/06 / 1977,40] [glavna] DEBUG obttasklets.LinesWriter - Napisana linija [Adam Caldwell, 27. srpnja 1995., 22] [glavna] DEBUG obttasklets.LinesWriter - Napisana linija [Lucille Phillips, 05. 14. 1992., 25] [glavna] DEBUG obttasklets.LinesWriter - Lines Writer završila .

To je to za Tasklets. Sada možemo prijeći na pristup Chunks.

5. Pristup komadima

5.1. Uvod i dizajn

Kao što i samo ime govori, ovaj pristup izvodi radnje nad dijelovima podataka. Odnosno, umjesto čitanja, obrade i pisanja svih redaka odjednom, istovremeno će čitati, obrađivati ​​i pisati fiksnu količinu zapisa (komad).

Zatim će ponavljati ciklus sve dok u datoteci više nema podataka.

Kao rezultat, protok će biti malo drugačiji:

  1. Iako postoje redovi:
    • Učinite za X količinu redaka:
      • Pročitajte jedan redak
      • Obradite jedan redak
    • Napišite X količinu redaka.

Dakle, mi također trebamo stvarati tri zrna za pristup usmjeren na komade:

javna klasa LineReader {// ...}
javna klasa LineProcessor {// ...}
javni razred LinesWriter {// ...}

Prije prijelaza na implementaciju, konfigurirajmo svoj posao.

5.2. Konfiguracija

Definicija posla također će izgledati drugačije:

@Configuration @EnableBatchProcessing ChunksConfig javne klase {@Automobilski privatni JobBuilderFactory poslovi; @Automobilski privatni koraci StepBuilderFactory; @Bean public ItemReader itemReader () {return new LineReader (); } @Bean public ItemProcessor itemProcessor () {return new LineProcessor (); } @Bean public ItemWriter itemWriter () {return new LinesWriter (); } @Bean zaštićeni korak processLines (čitač ItemReader, procesor ItemProcessor, Writer ItemWriter) {return steps.get ("processLines"). chunk (2) .reader (čitač) .procesor (procesor) .writer (programer) .build (); } @Bean public Job job () {return jobs .get ("chunksJob") .start (processLines (itemReader (), itemProcessor (), itemWriter ())) .build (); }}

U ovom slučaju postoji samo jedan korak koji izvodi samo jedan tasklet.

Međutim, taj tasklet definira čitač, pisač i procesor koji će djelovati na dijelove podataka.

Imajte na umu da interval predaje označava količinu podataka koji će se obraditi u jednom komadu. Naš posao će istovremeno čitati, obrađivati ​​i pisati dva retka.

Sada smo spremni dodati svoju logiku!

5.3. Čitač linija

Čitač linija bit će zadužen za čitanje jednog zapisa i vraćanje a Crta primjer sa svojim sadržajem.

Da postanem čitatelj, naš razred mora implementirati Predmet za čitanje sučelje:

javna klasa LineReader implementira ItemReader {@Override public Line read () baca izuzetak {Line line = fu.readLine (); if (line! = null) logger.debug ("Read line:" + line.toString ()); povratna linija; }}

Kôd je jednostavan, samo čita jedan redak i vraća ga. Također ćemo implementirati StepExecutionListener za konačnu verziju ove klase:

javna klasa LineReader implementira ItemReader, StepExecutionListener {private final Logger logger = LoggerFactory .getLogger (LineReader.class); privatni FileUtils fu; @Preuzmi javnu prazninu prije koraka (StepExecution stepExecution) {fu = novi FileUtils ("taskletsvschunks / input / tasklets-vs-chunks.csv"); logger.debug ("Inicijaliziran čitač linija."); } @Override public Line read () baca izuzetak {Line line = fu.readLine (); if (line! = null) logger.debug ("Read line:" + line.toString ()); povratna linija; } @Preuzmi javni ExitStatus afterStep (StepExecution stepExecution) {fu.closeReader (); logger.debug ("Čitač linija je završen."); povratak ExitStatus.COMPLETED; }}

Treba primijetiti da prijeKorak i nakonKorak izvršiti prije i nakon cijelog koraka.

5.4. LineProcessor

LineProcessor slijedi gotovo istu logiku nego Čitač linija.

Međutim, u ovom slučaju, provest ćemo PredmetProcesor i njegova metoda postupak():

javna klasa LineProcessor implementira ItemProcessor {private Logger logger = LoggerFactory.getLogger (LineProcessor.class); @Override public Line process (Line line) baca iznimku {long age = ChronoUnit.YEARS .between (line.getDob (), LocalDate.now ()); logger.debug ("Izračunata dob" + dob + "za liniju" + line.toString ()); line.setAge (dob); povratna linija; }}

The postupak() metoda uzima ulaznu liniju, obrađuje je i vraća izlaznu liniju. Opet, također ćemo implementirati StepExecutionListener:

javna klasa LineProcessor implementira ItemProcessor, StepExecutionListener {private Logger logger = LoggerFactory.getLogger (LineProcessor.class); @Override public void beforeStep (StepExecution stepExecution) {logger.debug ("Inicijaliziran linijski procesor."); } @Override postupak javne linije (Line line) baca izuzetak {long age = ChronoUnit.YEARS .between (line.getDob (), LocalDate.now ()); logger.debug ("Izračunata dob" + dob + "za liniju" + line.toString ()); line.setAge (dob); povratna linija; } @Override javni ExitStatus afterStep (StepExecution stepExecution) {logger.debug ("Line Processor završio."); povratak ExitStatus.COMPLETED; }}

5.5. LinesWriter

Za razliku od čitača i procesora, LinesWriter napisat će čitav dio redaka tako da prima a Popis od Linije:

javna klasa LinesWriter implementira ItemWriter, StepExecutionListener {private final Logger logger = LoggerFactory .getLogger (LinesWriter.class); privatni FileUtils fu; @Override public void beforeStep (StepExecution stepExecution) {fu = new FileUtils ("output.csv"); logger.debug ("Initial Line Writer inicijaliziran."); } @Override public void write (Popis redaka) baca iznimku {for (Line line: lines) {fu.writeLine (line); logger.debug ("Napisao redak" + line.toString ()); }} @Override javni ExitStatus afterStep (StepExecution stepExecution) {fu.closeWriter (); logger.debug ("Line Writer završio."); povratak ExitStatus.COMPLETED; }}

LinesWriter kod govori sam za sebe. I opet, spremni smo testirati svoj posao.

5.6. Pokretanje posla

Izradit ćemo novi test, isti kao i onaj koji smo kreirali za pristup taskletsu:

@RunWith (SpringJUnit4ClassRunner.class) @ContextConfiguration (classes = ChunksConfig.class) javna klasa ChunksTest {@Autowired private JobLauncherTestUtils jobLauncherTestUtils; @Test javna praznina givenChunksJob_whenJobEnds_thenStatusCompleted () baca izuzetak {JobExecution jobExecution = jobLauncherTestUtils.launchJob (); assertEquals (ExitStatus.COMPLETED, jobExecution.getExitStatus ()); }}

Nakon konfiguriranja KomadiConfig kao što je gore objašnjeno za TaskletsConfig, svi smo spremni pokrenuti test!

Kad posao završi, to možemo vidjeti izlaz.csv sadrži ponovno očekivani rezultat, a zapisnici opisuju tok:

[main] DEBUG o.b.t.chunks.LineReader - Inicijaliziran čitač linija. [glavna] DEBUG o.b.t.chunks.LinesWriter - Inicijaliziran redak. [main] DEBUG o.b.t.chunks.LineProcessor - Line Processor inicijaliziran. [main] DEBUG obtchunks.LineReader - Čitaj redak: [Mae Hodges, 22.10.1972.] [main] DEBUG obtchunks.LineReader - Čitaj redak: [Gary Potter, 22.2.1953.] [main] DEBUG obtchunks .LineProcessor - Izračunata starost 45 godina za liniju [Mae Hodges, 22.10.1972.] [Glavna] DEBUG obtchunks.LineProcessor - Izračunata dob 64 godine za liniju [Gary Potter, 22.2.1953.] [Main] DEBUG obtchunks.LinesWriter - Napisao redak [Mae Hodges, 10/22 / 1972,45] [glavna] DEBUG obtchunks.LinesWriter - Napisao redak [Gary Potter, 02/22 / 1953,64] [glavna] DEBUG obtchunks.LineReader - Pročitaj redak: [Betty Wise, 17. srpnja 1968.] [glavna] DEBUG obtchunks.LineReader - Pročitajte liniju: [Wayne Rose, 04. 06. 1977] [glavna] DEBUG obtchunks.LineProcessor - Izračunata dob 49 godina za liniju [Betty Wise, 17.02.1968. [Glavna] DEBUG obtchunks.LineProcessor - Izračunata starost 40 godina za liniju [Wayne Rose, 04/06/1977] [main] DEBUG obtchunks.LinesWriter - Napisana linija [Betty Wise, 02/17 / 1968 , 49] [glavna] DEBUG obtchunks.LinesWriter - Napisana linija [Wayne Rose, 04. 06. 1977.]] [Glavna] DEBUG ob t.chunks.LineReader - Čitaj redak: [Adam Caldwell, 27. 9. 1995.] [glavna] DEBUG obtchunks.LineReader - Čitaj redak: [Lucille Phillips, 14. 5. 1992.] [glavna] DEBUG obtchunks.LineProcessor - Izračunata dob 22 za liniju [Adam Caldwell, 27/27/1995] [glavna] DEBUG obtchunks.LineProcessor - Izračunata starost 25 za liniju [Lucille Phillips, 14/14 / 1992] [glavna] DEBUG obtchunks.LinesWriter - Napisana linija [Adam Caldwell, 27. srpnja 1995., 22] [glavna] DEBUG obtchunks.LinesWriter - Napisana linija [Lucille Phillips, 05/14 / 1992,25] [main] DEBUG obtchunks.LineProcessor - Line Processor je završen. [glavna] DEBUG o.b.t.chunks.LinesWriter - Line Writer je završio. [glavna] DEBUG o.b.t.chunks.LineReader - Čitač linija je završen.

Imamo isti rezultat i drugačiji tijek. Iz dnevnika je vidljivo kako se posao izvršava slijedeći ovaj pristup.

6. Zaključak

Različiti konteksti pokazat će potrebu za jednim ili drugim pristupom. Iako se Taskleti osjećaju prirodnije za scenarije 'jedan zadatak za drugim', dijelovi pružaju jednostavno rješenje za rješavanje paginiranih čitanja ili situacija u kojima ne želimo zadržati značajnu količinu podataka u memoriji.

Kompletnu provedbu ovog primjera možete pronaći u projekt GitHub.


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