Перейти к основному содержимому

Весенняя загрузка с весенней партией

· 7 мин. чтения

1. Обзор

Spring Batch — это мощная платформа для разработки надежных пакетных приложений. В нашем предыдущем уроке мы представили Spring Batch .

В этом руководстве мы будем основываться на предыдущем и узнаем, как настроить и создать базовое пакетное приложение с использованием Spring Boot .

2. Зависимости Maven

Во-первых, давайте добавим spring-boot-starter-batch в наш pom.xml :

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-batch</artifactId>
<version>2.4.0</version>
</dependency>

Мы также добавим зависимость org.hsqldb , которая также доступна в Maven Central :

<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>2.5.1</version>
<scope>runtime</scope>
</dependency>

3. Определение простого пакетного задания Spring

Мы собираемся создать задание, которое импортирует список кофе из CSV-файла, преобразует его с помощью специального процессора и сохраняет окончательные результаты в базе данных в оперативной памяти .

3.1. Начиная

Давайте начнем с определения точки входа нашего приложения:

@SpringBootApplication
public class SpringBootBatchProcessingApplication {

public static void main(String[] args) {
SpringApplication.run(SpringBootBatchProcessingApplication.class, args);
}
}

Как мы видим, это стандартное приложение Spring Boot. Поскольку мы хотим использовать значения конфигурации по умолчанию, где это возможно, мы собираемся использовать очень легкий набор свойств конфигурации приложения.

Мы определим эти свойства в нашем файле src/main/resources/application.properties :

file.input=coffee-list.csv

Это свойство содержит расположение нашего входного списка кофе. Каждая строка содержит марку, происхождение и некоторые характеристики нашего кофе:

Blue Mountain,Jamaica,Fruity
Lavazza,Colombia,Strong
Folgers,America,Smokey

Как мы увидим, это плоский файл CSV, что означает, что Spring может обрабатывать его без какой-либо специальной настройки.

Далее мы добавим SQL-скрипт schema-all.sql , чтобы создать журнальный столик для хранения данных:

DROP TABLE coffee IF EXISTS;

CREATE TABLE coffee (
coffee_id BIGINT IDENTITY NOT NULL PRIMARY KEY,
brand VARCHAR(20),
origin VARCHAR(20),
characteristics VARCHAR(30)
);

Удобно, что Spring Boot автоматически запускает этот скрипт во время запуска .

3.2. Класс домена кофе

Впоследствии нам понадобится простой доменный класс для хранения наших кофейных напитков:

public class Coffee {

private String brand;
private String origin;
private String characteristics;

public Coffee(String brand, String origin, String characteristics) {
this.brand = brand;
this.origin = origin;
this.characteristics = characteristics;
}

// getters and setters
}

Как упоминалось ранее, наш объект Coffee содержит три свойства:

  • Бренд
  • Происхождение
  • Некоторые дополнительные характеристики

4. Конфигурация задания

Теперь перейдем к ключевому компоненту — конфигурации нашей работы. Мы пойдем шаг за шагом, создавая нашу конфигурацию и объясняя каждую часть по пути:

@Configuration
@EnableBatchProcessing
public class BatchConfiguration {

@Autowired
public JobBuilderFactory jobBuilderFactory;

@Autowired
public StepBuilderFactory stepBuilderFactory;

@Value("${file.input}")
private String fileInput;

// ...
}

Во-первых, мы начнем со стандартного класса Spring @Configuration . Затем мы добавляем аннотацию @EnableBatchProcessing в наш класс. Примечательно, что это дает нам доступ ко многим полезным bean-компонентам, которые поддерживают работу, и сэкономит нам много работы.

Кроме того, использование этой аннотации также предоставляет нам доступ к двум полезным фабрикам, которые мы будем использовать позже при построении нашей конфигурации задания и шагов задания.

В последней части нашей первоначальной конфигурации мы включаем ссылку на свойство file.input , которое мы объявили ранее.

4.1. Читатель и писатель для нашей работы

Теперь мы можем продолжить и определить bean-компонент для чтения в нашей конфигурации:

@Bean
public FlatFileItemReader reader() {
return new FlatFileItemReaderBuilder().name("coffeeItemReader")
.resource(new ClassPathResource(fileInput))
.delimited()
.names(new String[] { "brand", "origin", "characteristics" })
.fieldSetMapper(new BeanWrapperFieldSetMapper() {{
setTargetType(Coffee.class);
}})
.build();
}

Короче говоря, наш bean-компонент для чтения, определенный выше, ищет файл с именем coffee-list.csv и анализирует каждый элемент строки в объект Coffee .

Точно так же мы определяем бин записи:

@Bean
public JdbcBatchItemWriter writer(DataSource dataSource) {
return new JdbcBatchItemWriterBuilder()
.itemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<>())
.sql("INSERT INTO coffee (brand, origin, characteristics) VALUES (:brand, :origin, :characteristics)")
.dataSource(dataSource)
.build();
}

На этот раз мы включаем оператор SQL, необходимый для вставки одного элемента кофе в нашу базу данных, управляемый свойствами Java-бина нашего объекта Coffee . Удобно, что dataSource автоматически создается аннотацией @EnableBatchProcessing .

4.2. Объединяем нашу работу

Наконец, нам нужно добавить фактические шаги работы и конфигурацию:

@Bean
public Job importUserJob(JobCompletionNotificationListener listener, Step step1) {
return jobBuilderFactory.get("importUserJob")
.incrementer(new RunIdIncrementer())
.listener(listener)
.flow(step1)
.end()
.build();
}

@Bean
public Step step1(JdbcBatchItemWriter writer) {
return stepBuilderFactory.get("step1")
.<Coffee, Coffee> chunk(10)
.reader(reader())
.processor(processor())
.writer(writer)
.build();
}

@Bean
public CoffeeItemProcessor processor() {
return new CoffeeItemProcessor();
}

Как видим, наша работа относительно проста и состоит из одного шага, определенного в методе step1 .

Давайте посмотрим, что делает этот шаг:

  • Во-первых, мы настраиваем наш шаг так, чтобы он мог записывать до десяти записей за раз, используя объявление chunk(10)
  • Затем мы считываем данные о кофе с помощью нашего компонента чтения, который мы устанавливаем с помощью метода чтения .
  • Затем мы передаем каждый из наших кофейных напитков пользовательскому процессору, где мы применяем некоторую пользовательскую бизнес-логику.
  • Наконец, мы записываем каждый элемент кофе в базу данных, используя модуль записи, который мы видели ранее.

С другой стороны, наш importUserJob содержит определение нашей работы, которое содержит идентификатор с использованием встроенного класса RunIdIncrementer . Мы также устанавливаем прослушиватель JobCompletionNotificationListener, который используем для получения уведомлений о завершении задания .

Чтобы завершить настройку нашего задания, мы перечислим каждый шаг (хотя в этом задании только один шаг). Теперь у нас есть идеально настроенная работа!

5. Кофейный процессор на заказ

Давайте подробно рассмотрим пользовательский процессор, который мы определили ранее в нашей конфигурации задания:

public class CoffeeItemProcessor implements ItemProcessor<Coffee, Coffee> {

private static final Logger LOGGER = LoggerFactory.getLogger(CoffeeItemProcessor.class);

@Override
public Coffee process(final Coffee coffee) throws Exception {
String brand = coffee.getBrand().toUpperCase();
String origin = coffee.getOrigin().toUpperCase();
String chracteristics = coffee.getCharacteristics().toUpperCase();

Coffee transformedCoffee = new Coffee(brand, origin, chracteristics);
LOGGER.info("Converting ( {} ) into ( {} )", coffee, transformedCoffee);

return transformedCoffee;
}
}

Особенно интересно то, что интерфейс ItemProcessor предоставляет нам механизм для применения определенной бизнес-логики во время выполнения нашего задания.

Для простоты мы определяем наш CoffeeItemProcessor , который принимает входной объект Coffee и преобразует каждое из свойств в верхний регистр .

6. Завершение работы

Кроме того, мы также собираемся написать JobCompletionNotificationListener , чтобы обеспечить некоторую обратную связь, когда наша работа завершится:

@Override
public void afterJob(JobExecution jobExecution) {
if (jobExecution.getStatus() == BatchStatus.COMPLETED) {
LOGGER.info("!!! JOB FINISHED! Time to verify the results");

String query = "SELECT brand, origin, characteristics FROM coffee";
jdbcTemplate.query(query, (rs, row) -> new Coffee(rs.getString(1), rs.getString(2), rs.getString(3)))
.forEach(coffee -> LOGGER.info("Found < {} > in the database.", coffee));
}
}

В приведенном выше примере мы переопределяем метод afterJob и проверяем успешное выполнение задания. Более того, мы запускаем тривиальный запрос, чтобы убедиться, что каждый кофейный элемент успешно сохранен в базе данных .

7. Выполнение нашей работы

Теперь, когда у нас есть все необходимое для выполнения нашей работы, начинается самое интересное. Давайте продолжим и запустим нашу работу:

...
17:41:16.336 [main] INFO c.b.b.JobCompletionNotificationListener -
!!! JOB FINISHED! Time to verify the results
17:41:16.336 [main] INFO c.b.b.JobCompletionNotificationListener -
Found < Coffee [brand=BLUE MOUNTAIN, origin=JAMAICA, characteristics=FRUITY] > in the database.
17:41:16.337 [main] INFO c.b.b.JobCompletionNotificationListener -
Found < Coffee [brand=LAVAZZA, origin=COLOMBIA, characteristics=STRONG] > in the database.
17:41:16.337 [main] INFO c.b.b.JobCompletionNotificationListener -
Found < Coffee [brand=FOLGERS, origin=AMERICA, characteristics=SMOKEY] > in the database.
...

Как мы видим, наша работа прошла успешно, и каждый кофейный элемент был сохранен в базе данных, как и ожидалось .

8. Заключение

В этой статье мы узнали, как создать простое пакетное задание Spring с помощью Spring Boot. Во-первых, мы начали с определения некоторой базовой конфигурации.

Затем мы увидели, как добавить средство чтения файлов и средство записи базы данных. Наконец, мы рассмотрели, как применить некоторую пользовательскую обработку и убедиться, что наша работа выполнена успешно.

Как всегда, полный исходный код статьи доступен на GitHub .