diff --git a/2020-11/spring-26-spring-batch/spring-batch-demo/.gitignore b/2020-11/spring-26-spring-batch/spring-batch-demo/.gitignore new file mode 100644 index 00000000..04e0ae48 --- /dev/null +++ b/2020-11/spring-26-spring-batch/spring-batch-demo/.gitignore @@ -0,0 +1,6 @@ +.idea/ +*.iml + +target/ + +output.csv diff --git a/2020-11/spring-26-spring-batch/spring-batch-demo/entries.csv b/2020-11/spring-26-spring-batch/spring-batch-demo/entries.csv new file mode 100644 index 00000000..bde433ce --- /dev/null +++ b/2020-11/spring-26-spring-batch/spring-batch-demo/entries.csv @@ -0,0 +1,16 @@ +Ivan,23 +John,24 +Ivan,23 +John,24 +Ivan,23 +Mary,24 +Ivan,23 +John,24 +Sunny,23 +John,24 +Ivan,23 +John,24 +Ivan,23 +John,24 +Ivan,23 +John,24 diff --git a/2020-11/spring-26-spring-batch/spring-batch-demo/pom.xml b/2020-11/spring-26-spring-batch/spring-batch-demo/pom.xml new file mode 100644 index 00000000..2507f544 --- /dev/null +++ b/2020-11/spring-26-spring-batch/spring-batch-demo/pom.xml @@ -0,0 +1,127 @@ + + + 4.0.0 + + ru.otus.example + spring-batch-demo + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 2.4.3 + + + + 11 + 11 + 11 + 4.1.19 + + 2.0.0 + + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-batch + + + + com.h2database + h2 + + + + org.springframework.boot + spring-boot-starter-data-mongodb + + + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo + ${flapdoodle.version} + test + + + + com.github.cloudyrock.mongock + mongock-spring-v5 + ${mongock.version} + + + + com.github.cloudyrock.mongock + mongodb-springdata-v3-driver + ${mongock.version} + + + + + + org.projectlombok + lombok + true + + + + org.springframework.shell + spring-shell-starter + 2.0.1.RELEASE + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.batch + spring-batch-test + test + + + + org.junit.jupiter + junit-jupiter-api + ${junit-jupiter.version} + test + + + + org.junit.jupiter + junit-jupiter-engine + ${junit-jupiter.version} + test + + + + + spring-batch-demo + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/2020-11/spring-26-spring-batch/spring-batch-demo/src/main/java/ru/otus/example/springbatch/Main.java b/2020-11/spring-26-spring-batch/spring-batch-demo/src/main/java/ru/otus/example/springbatch/Main.java new file mode 100644 index 00000000..00a0d2d1 --- /dev/null +++ b/2020-11/spring-26-spring-batch/spring-batch-demo/src/main/java/ru/otus/example/springbatch/Main.java @@ -0,0 +1,19 @@ +package ru.otus.example.springbatch; + +import com.github.cloudyrock.spring.v5.EnableMongock; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.PropertySource; + +import java.io.IOException; + +@EnableMongock +@SpringBootApplication +public class Main { + + public static void main(String[] args) { + SpringApplication.run(Main.class, args); + } +} + + diff --git a/2020-11/spring-26-spring-batch/spring-batch-demo/src/main/java/ru/otus/example/springbatch/chandgelogs/InitMongoDBDataChangeLog.java b/2020-11/spring-26-spring-batch/spring-batch-demo/src/main/java/ru/otus/example/springbatch/chandgelogs/InitMongoDBDataChangeLog.java new file mode 100644 index 00000000..feb52399 --- /dev/null +++ b/2020-11/spring-26-spring-batch/spring-batch-demo/src/main/java/ru/otus/example/springbatch/chandgelogs/InitMongoDBDataChangeLog.java @@ -0,0 +1,45 @@ +package ru.otus.example.springbatch.chandgelogs; + +import com.github.cloudyrock.mongock.ChangeLog; +import com.github.cloudyrock.mongock.ChangeSet; +import com.github.cloudyrock.mongock.driver.mongodb.springdata.v3.decorator.impl.MongockTemplate; +import com.mongodb.client.MongoDatabase; +import ru.otus.example.springbatch.model.Person; + +@ChangeLog(order = "001") +public class InitMongoDBDataChangeLog { + + @ChangeSet(order = "000", id = "dropDB", author = "stvort", runAlways = true) + public void dropDB(MongoDatabase database){ + database.drop(); + } + + @ChangeSet(order = "001", id = "initPersons", author = "stvort", runAlways = true) + public void initPersons(MongockTemplate template){ + template.save(new Person("Джон", 21)); + template.save(new Person("Игорь", 32)); + template.save(new Person("Дмитрий", 52)); + template.save(new Person("Михаил", 22)); + template.save(new Person("Герман", 33)); + template.save(new Person("Джон", 21)); + template.save(new Person("Игорь", 32)); + template.save(new Person("Дмитрий", 52)); + template.save(new Person("Михаил", 22)); + template.save(new Person("Герман", 33)); + template.save(new Person("Джон", 21)); + template.save(new Person("Игорь", 32)); + template.save(new Person("Дмитрий", 52)); + template.save(new Person("Михаил", 22)); + template.save(new Person("Герман", 33)); + template.save(new Person("Джон", 21)); + template.save(new Person("Игорь", 32)); + template.save(new Person("Дмитрий", 52)); + template.save(new Person("Михаил", 22)); + template.save(new Person("Герман", 33)); + template.save(new Person("Джон", 21)); + template.save(new Person("Игорь", 32)); + template.save(new Person("Дмитрий", 52)); + template.save(new Person("Михаил", 22)); + template.save(new Person("Герман", 33)); + } +} diff --git a/2020-11/spring-26-spring-batch/spring-batch-demo/src/main/java/ru/otus/example/springbatch/config/AppProps.java b/2020-11/spring-26-spring-batch/spring-batch-demo/src/main/java/ru/otus/example/springbatch/config/AppProps.java new file mode 100644 index 00000000..5d9deb61 --- /dev/null +++ b/2020-11/spring-26-spring-batch/spring-batch-demo/src/main/java/ru/otus/example/springbatch/config/AppProps.java @@ -0,0 +1,14 @@ +package ru.otus.example.springbatch.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties("app") +public class AppProps { + private String inputFile; + private String outputFile; + +} diff --git a/2020-11/spring-26-spring-batch/spring-batch-demo/src/main/java/ru/otus/example/springbatch/config/BatchConfig.java b/2020-11/spring-26-spring-batch/spring-batch-demo/src/main/java/ru/otus/example/springbatch/config/BatchConfig.java new file mode 100644 index 00000000..54525aea --- /dev/null +++ b/2020-11/spring-26-spring-batch/spring-batch-demo/src/main/java/ru/otus/example/springbatch/config/BatchConfig.java @@ -0,0 +1,19 @@ +package ru.otus.example.springbatch.config; + +import org.springframework.batch.core.configuration.JobRegistry; +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.configuration.support.JobRegistryBeanPostProcessor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@EnableBatchProcessing +@Configuration +public class BatchConfig { + @Bean + public JobRegistryBeanPostProcessor postProcessor(JobRegistry jobRegistry) { + var processor = new JobRegistryBeanPostProcessor(); + processor.setJobRegistry(jobRegistry); + return processor; + } + +} diff --git a/2020-11/spring-26-spring-batch/spring-batch-demo/src/main/java/ru/otus/example/springbatch/config/JobConfig.java b/2020-11/spring-26-spring-batch/spring-batch-demo/src/main/java/ru/otus/example/springbatch/config/JobConfig.java new file mode 100644 index 00000000..bf9df4bb --- /dev/null +++ b/2020-11/spring-26-spring-batch/spring-batch-demo/src/main/java/ru/otus/example/springbatch/config/JobConfig.java @@ -0,0 +1,169 @@ +package ru.otus.example.springbatch.config; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.batch.core.*; +import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; +import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.core.launch.support.RunIdIncrementer; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.item.ItemReader; +import org.springframework.batch.item.file.FlatFileItemReader; +import org.springframework.batch.item.file.FlatFileItemWriter; +import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder; +import org.springframework.batch.item.file.builder.FlatFileItemWriterBuilder; +import org.springframework.batch.item.file.transform.DelimitedLineAggregator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.FileSystemResource; +import ru.otus.example.springbatch.model.Person; +import ru.otus.example.springbatch.service.HappyBirthdayService; + +import java.util.List; + + +@SuppressWarnings("all") +@Configuration +public class JobConfig { + private static final int CHUNK_SIZE = 5; + private final Logger logger = LoggerFactory.getLogger("Batch"); + + public static final String OUTPUT_FILE_NAME = "outputFileName"; + public static final String INPUT_FILE_NAME = "inputFileName"; + public static final String IMPORT_USER_JOB_NAME = "importUserJob"; + + @Autowired + private JobBuilderFactory jobBuilderFactory; + + @Autowired + private StepBuilderFactory stepBuilderFactory; + + @StepScope + @Bean + public FlatFileItemReader reader(@Value("#{jobParameters['" + INPUT_FILE_NAME + "']}") String inputFileName) { + return new FlatFileItemReaderBuilder() + .name("personItemReader") + .resource(new FileSystemResource(inputFileName)) + + // Работа через lineMapper + .lineMapper((s, i) -> { + String[] fieldsValues = s.split(","); + return new Person(fieldsValues[0], Integer.parseInt(fieldsValues[1])); + }) +/* + + // Работа через fieldSetMapper + .delimited() + .names("name", "age") + .fieldSetMapper(new BeanWrapperFieldSetMapper<>() {{ + setTargetType(Person.class); + }}) +*/ + + + .build(); + } + + @StepScope + @Bean + public ItemProcessor processor(HappyBirthdayService happyBirthdayService) { + return (ItemProcessor) happyBirthdayService::doHappyBirthday; + } + + @StepScope + @Bean + public FlatFileItemWriter writer(@Value("#{jobParameters['" + OUTPUT_FILE_NAME + "']}") String outputFileName) { + return new FlatFileItemWriterBuilder<>() + .name("personItemWriter") + .resource(new FileSystemResource(outputFileName)) + .lineAggregator(new DelimitedLineAggregator<>()) + .build(); + } + + @Bean + public Job importUserJob(Step step1) { + return jobBuilderFactory.get(IMPORT_USER_JOB_NAME) + .incrementer(new RunIdIncrementer()) + .flow(step1) + .end() + .listener(new JobExecutionListener() { + @Override + public void beforeJob(JobExecution jobExecution) { + logger.info("Начало job"); + } + + @Override + public void afterJob(JobExecution jobExecution) { + logger.info("Конец job"); + } + }) + .build(); + } + + @Bean + public Step step1(FlatFileItemWriter writer, ItemReader reader, ItemProcessor itemProcessor) { + return stepBuilderFactory.get("step1") + .chunk(CHUNK_SIZE) + .reader(reader) + .processor(itemProcessor) + .writer(writer) + .listener(new ItemReadListener() { + public void beforeRead() { + logger.info("Начало чтения"); + } + + public void afterRead(Object o) { + logger.info("Конец чтения"); + } + + public void onReadError(Exception e) { + logger.info("Ошибка чтения"); + } + }) + .listener(new ItemWriteListener() { + public void beforeWrite(List list) { + logger.info("Начало записи"); + } + + public void afterWrite(List list) { + logger.info("Конец записи"); + } + + public void onWriteError(Exception e, List list) { + logger.info("Ошибка записи"); + } + }) + .listener(new ItemProcessListener() { + public void beforeProcess(Object o) { + logger.info("Начало обработки"); + } + + public void afterProcess(Object o, Object o2) { + logger.info("Конец обработки"); + } + + public void onProcessError(Object o, Exception e) { + logger.info("Ошбка обработки"); + } + }) + .listener(new ChunkListener() { + public void beforeChunk(ChunkContext chunkContext) { + logger.info("Начало пачки"); + } + + public void afterChunk(ChunkContext chunkContext) { + logger.info("Конец пачки"); + } + + public void afterChunkError(ChunkContext chunkContext) { + logger.info("Ошибка пачки"); + } + }) +// .taskExecutor(new SimpleAsyncTaskExecutor()) + .build(); + } +} diff --git a/2020-11/spring-26-spring-batch/spring-batch-demo/src/main/java/ru/otus/example/springbatch/model/Person.java b/2020-11/spring-26-spring-batch/spring-batch-demo/src/main/java/ru/otus/example/springbatch/model/Person.java new file mode 100644 index 00000000..b95cb1ff --- /dev/null +++ b/2020-11/spring-26-spring-batch/spring-batch-demo/src/main/java/ru/otus/example/springbatch/model/Person.java @@ -0,0 +1,13 @@ +package ru.otus.example.springbatch.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@NoArgsConstructor +@AllArgsConstructor +@Data +public class Person { + private String name; + private int age; +} diff --git a/2020-11/spring-26-spring-batch/spring-batch-demo/src/main/java/ru/otus/example/springbatch/service/HappyBirthdayService.java b/2020-11/spring-26-spring-batch/spring-batch-demo/src/main/java/ru/otus/example/springbatch/service/HappyBirthdayService.java new file mode 100644 index 00000000..00ec7287 --- /dev/null +++ b/2020-11/spring-26-spring-batch/spring-batch-demo/src/main/java/ru/otus/example/springbatch/service/HappyBirthdayService.java @@ -0,0 +1,13 @@ +package ru.otus.example.springbatch.service; + +import org.springframework.stereotype.Service; +import ru.otus.example.springbatch.model.Person; + +@Service +public class HappyBirthdayService { + + public Person doHappyBirthday(Person person){ + person.setAge(person.getAge() + 1); + return person; + } +} diff --git a/2020-11/spring-26-spring-batch/spring-batch-demo/src/main/java/ru/otus/example/springbatch/shell/BatchCommands.java b/2020-11/spring-26-spring-batch/spring-batch-demo/src/main/java/ru/otus/example/springbatch/shell/BatchCommands.java new file mode 100644 index 00000000..0e4296c4 --- /dev/null +++ b/2020-11/spring-26-spring-batch/spring-batch-demo/src/main/java/ru/otus/example/springbatch/shell/BatchCommands.java @@ -0,0 +1,60 @@ +package ru.otus.example.springbatch.shell; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.JobParametersInvalidException; +import org.springframework.batch.core.explore.JobExplorer; +import org.springframework.batch.core.launch.JobInstanceAlreadyExistsException; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.core.launch.JobOperator; +import org.springframework.batch.core.launch.NoSuchJobException; +import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException; +import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException; +import org.springframework.batch.core.repository.JobRestartException; +import org.springframework.shell.standard.ShellComponent; +import org.springframework.shell.standard.ShellMethod; +import ru.otus.example.springbatch.config.AppProps; + + +import static ru.otus.example.springbatch.config.JobConfig.*; + +@RequiredArgsConstructor +@ShellComponent +public class BatchCommands { + + private final AppProps appProps; + private final Job importUserJob; + + private final JobLauncher jobLauncher; + private final JobOperator jobOperator; + private final JobExplorer jobExplorer; + + //http://localhost:8080/h2-console/ + + @ShellMethod(value = "startMigrationJobWithJobLauncher", key = "sm-jl") + public void startMigrationJobWithJobLauncher() throws Exception { + JobExecution execution = jobLauncher.run(importUserJob, new JobParametersBuilder() + .addString(INPUT_FILE_NAME, appProps.getInputFile()) + .addString(OUTPUT_FILE_NAME, appProps.getOutputFile()) + .toJobParameters()); + System.out.println(execution); + } + + @ShellMethod(value = "startMigrationJobWithJobOperator", key = "sm-jo") + public void startMigrationJobWithJobOperator() throws Exception { + Long executionId = jobOperator.start(IMPORT_USER_JOB_NAME, + INPUT_FILE_NAME + "=" + appProps.getInputFile() + "\n" + + OUTPUT_FILE_NAME + "=" + appProps.getOutputFile() + ); + System.out.println(jobOperator.getSummary(executionId)); + } + + @ShellMethod(value = "showInfo", key = "i") + public void showInfo() { + System.out.println(jobExplorer.getJobNames()); + System.out.println(jobExplorer.getLastJobInstance(IMPORT_USER_JOB_NAME)); + } +} diff --git a/2020-11/spring-26-spring-batch/spring-batch-demo/src/main/resources/application.yml b/2020-11/spring-26-spring-batch/spring-batch-demo/src/main/resources/application.yml new file mode 100644 index 00000000..74c2b363 --- /dev/null +++ b/2020-11/spring-26-spring-batch/spring-batch-demo/src/main/resources/application.yml @@ -0,0 +1,37 @@ +spring: + batch: + job: + enabled: false + shell: + interactive: + enabled: true + + datasource: + url: jdbc:h2:mem:testdb + driverClassName: org.h2.Driver + username: sa + password: + + h2: + console: + enabled: true + path: /h2-console + + data: + mongodb: + #uri: mongodb://localhost + host: localhost + port: 27017 + database: SpringBatchExampleDB + +mongock: + runner-type: InitializingBean + change-logs-scan-package: + - ru.otus.example.springbatch.chandgelogs + +app: + ages-count-to-add: 1 + input-file: entries.csv + output-file: output.dat + +#debug: true diff --git a/2020-11/spring-26-spring-batch/spring-batch-demo/src/test/java/ru/otus/example/springbatch/config/ImportUserJobTest.java b/2020-11/spring-26-spring-batch/spring-batch-demo/src/test/java/ru/otus/example/springbatch/config/ImportUserJobTest.java new file mode 100644 index 00000000..97b1b035 --- /dev/null +++ b/2020-11/spring-26-spring-batch/spring-batch-demo/src/test/java/ru/otus/example/springbatch/config/ImportUserJobTest.java @@ -0,0 +1,73 @@ +package ru.otus.example.springbatch.config; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.test.AssertFile; +import org.springframework.batch.test.JobLauncherTestUtils; +import org.springframework.batch.test.JobRepositoryTestUtils; +import org.springframework.batch.test.context.SpringBatchTest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.io.FileSystemResource; + + +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Objects; + +import static org.assertj.core.api.Assertions.assertThat; +import static ru.otus.example.springbatch.config.JobConfig.*; + +@SpringBootTest +@SpringBatchTest +class ImportUserJobTest { + + private static final String TEST_INPUT_FILE_NAME = "test-entries.csv"; + private static final String EXPECTED_OUTPUT_FILE_NAME = "expected-test-output.dat"; + private static final String TEST_OUTPUT_FILE_NAME = "test-output.dat"; + + @Autowired + private JobLauncherTestUtils jobLauncherTestUtils; + + @Autowired + private JobRepositoryTestUtils jobRepositoryTestUtils; + + @BeforeEach + void clearMetaData() { + jobRepositoryTestUtils.removeJobExecutions(); + } + + @Test + void testJob() throws Exception { + var classLoader = ImportUserJobTest.class.getClassLoader(); + var testInputFileName = URLDecoder.decode( + Objects.requireNonNull(classLoader.getResource(TEST_INPUT_FILE_NAME)).getFile(), + StandardCharsets.UTF_8 + ); + var expectedResultFileName = URLDecoder.decode( + Objects.requireNonNull(classLoader.getResource(EXPECTED_OUTPUT_FILE_NAME)).getFile(), + StandardCharsets.UTF_8 + ); + + FileSystemResource expectedResult = new FileSystemResource(expectedResultFileName); + FileSystemResource actualResult = new FileSystemResource(TEST_OUTPUT_FILE_NAME); + + Job job = jobLauncherTestUtils.getJob(); + assertThat(job).isNotNull() + .extracting(Job::getName) + .isEqualTo(IMPORT_USER_JOB_NAME); + + JobParameters parameters = new JobParametersBuilder() + .addString(INPUT_FILE_NAME, testInputFileName) + .addString(OUTPUT_FILE_NAME, TEST_OUTPUT_FILE_NAME) + .toJobParameters(); + JobExecution jobExecution = jobLauncherTestUtils.launchJob(parameters); + + assertThat(jobExecution.getExitStatus().getExitCode()).isEqualTo("COMPLETED"); + AssertFile.assertFileEquals(expectedResult, actualResult); + } +} \ No newline at end of file diff --git a/2020-11/spring-26-spring-batch/spring-batch-demo/src/test/resources/application.yml b/2020-11/spring-26-spring-batch/spring-batch-demo/src/test/resources/application.yml new file mode 100644 index 00000000..49c12c2e --- /dev/null +++ b/2020-11/spring-26-spring-batch/spring-batch-demo/src/test/resources/application.yml @@ -0,0 +1,24 @@ +spring: + batch: + job: + enabled: false + shell: + interactive: + enabled: false + + datasource: + url: jdbc:h2:mem:testdb + driverClassName: org.h2.Driver + username: sa + password: + + data: + mongodb: + host: localhost + port: 0 + database: SpringBatchTestExampleDB + +mongock: + runner-type: InitializingBean + change-logs-scan-package: + - ru.otus.example.springbatch.chandgelogs \ No newline at end of file diff --git a/2020-11/spring-26-spring-batch/spring-batch-demo/src/test/resources/expected-test-output.dat b/2020-11/spring-26-spring-batch/spring-batch-demo/src/test/resources/expected-test-output.dat new file mode 100644 index 00000000..b4c926b1 --- /dev/null +++ b/2020-11/spring-26-spring-batch/spring-batch-demo/src/test/resources/expected-test-output.dat @@ -0,0 +1,16 @@ +Person(name=Ivan, age=24) +Person(name=John, age=25) +Person(name=Ivan, age=24) +Person(name=John, age=25) +Person(name=Ivan, age=24) +Person(name=Mary, age=25) +Person(name=Ivan, age=24) +Person(name=John, age=25) +Person(name=Sunny, age=24) +Person(name=John, age=25) +Person(name=Ivan, age=24) +Person(name=John, age=25) +Person(name=Ivan, age=24) +Person(name=John, age=25) +Person(name=Ivan, age=24) +Person(name=John, age=25) diff --git a/2020-11/spring-26-spring-batch/spring-batch-demo/src/test/resources/test-entries.csv b/2020-11/spring-26-spring-batch/spring-batch-demo/src/test/resources/test-entries.csv new file mode 100644 index 00000000..bde433ce --- /dev/null +++ b/2020-11/spring-26-spring-batch/spring-batch-demo/src/test/resources/test-entries.csv @@ -0,0 +1,16 @@ +Ivan,23 +John,24 +Ivan,23 +John,24 +Ivan,23 +Mary,24 +Ivan,23 +John,24 +Sunny,23 +John,24 +Ivan,23 +John,24 +Ivan,23 +John,24 +Ivan,23 +John,24