spring-26-spring-batch added to 2020-11

This commit is contained in:
Александр Оруджев
2021-03-09 00:59:41 +04:00
parent a9e6cc7384
commit 94587dba74
16 changed files with 667 additions and 0 deletions
@@ -0,0 +1,6 @@
.idea/
*.iml
target/
output.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
1 Ivan 23
2 John 24
3 Ivan 23
4 John 24
5 Ivan 23
6 Mary 24
7 Ivan 23
8 John 24
9 Sunny 23
10 John 24
11 Ivan 23
12 John 24
13 Ivan 23
14 John 24
15 Ivan 23
16 John 24
@@ -0,0 +1,127 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ru.otus.example</groupId>
<artifactId>spring-batch-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.3</version>
</parent>
<properties>
<java.version>11</java.version>
<maven.compiler.sourcre>11</maven.compiler.sourcre>
<maven.compiler.target>11</maven.compiler.target>
<mongock.version>4.1.19</mongock.version>
<!--
https://github.com/flapdoodle-oss/de.flapdoodle.embed.mongo/issues/325
-->
<flapdoodle.version>2.0.0</flapdoodle.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-batch</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>de.flapdoodle.embed</groupId>
<artifactId>de.flapdoodle.embed.mongo</artifactId>
<version>${flapdoodle.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.cloudyrock.mongock</groupId>
<artifactId>mongock-spring-v5</artifactId>
<version>${mongock.version}</version>
</dependency>
<dependency>
<groupId>com.github.cloudyrock.mongock</groupId>
<artifactId>mongodb-springdata-v3-driver</artifactId>
<version>${mongock.version}</version>
</dependency>
<!--
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.1.8.RELEASE</version>
</dependency>
-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.shell</groupId>
<artifactId>spring-shell-starter</artifactId>
<version>2.0.1.RELEASE</version>
</dependency>
<!--Тестирование-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.batch</groupId>
<artifactId>spring-batch-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit-jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit-jupiter.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>spring-batch-demo</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -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);
}
}
@@ -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));
}
}
@@ -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;
}
@@ -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;
}
}
@@ -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<Person> reader(@Value("#{jobParameters['" + INPUT_FILE_NAME + "']}") String inputFileName) {
return new FlatFileItemReaderBuilder<Person>()
.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<Person, Person>) 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();
}
}
@@ -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;
}
@@ -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;
}
}
@@ -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));
}
}
@@ -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
@@ -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);
}
}
@@ -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
@@ -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)
@@ -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
1 Ivan 23
2 John 24
3 Ivan 23
4 John 24
5 Ivan 23
6 Mary 24
7 Ivan 23
8 John 24
9 Sunny 23
10 John 24
11 Ivan 23
12 John 24
13 Ivan 23
14 John 24
15 Ivan 23
16 John 24