Spring Batch 3.0 – это подпроект Spring Framework, реализующий стандарт JSR-352 для выполнения пакетных заданий. Приступим сразу к делу и покажем на примере для чего это все нужно. Напишем небольшое веб-приложение, которое будет запускать пакетное задание и отображать состояние выполнения клиенту. В нескольких частях мы будем решать возникшие проблемы и изучать “фишки” Spring Batch’a.
Исходники для этого примера можно найти на https://github.com/JavaGrinko/batch-example
Скачайте репозиторий и переключитесь на ветку step0:
1 2 |
git clone https://github.com/JavaGrinko/batch-example.git git checkout -f step0 |
Мы напишем приложение, которое загружает товары из csv-файла в базу данных. Будем использовать:
- Spring Boot в качестве каркаса для приложения
- Встроенная база данных H2
- Lombok
- Spring Batch версии 3.0.7.RELEASE
Подключим все зависимости в build.gradle файл:
1 2 3 4 5 6 7 |
dependencies { compile 'com.h2database:h2:1.4.192' compile 'org.projectlombok:lombok:1.16.10' compile "org.springframework.boot:spring-boot-starter-data-jpa:1.4.0.RELEASE" compile 'org.springframework.batch:spring-batch-core:3.0.7.RELEASE' testCompile group: 'junit', name: 'junit', version: '4.11' } |
Модель данных для товара будет выглядеть следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package javagrinko.batch.example.model; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Data @Builder @NoArgsConstructor @AllArgsConstructor public class Product { private Long id; private String name; private String description; private Double price; } |
Добавим в папку src/main/resource файл schema-all.sql, который Spring Boot автоматически найдет и обработает при запуске приложения:
1 2 3 4 5 6 7 8 |
DROP TABLE products IF EXISTS; CREATE TABLE products ( product_id IDENTITY NOT NULL PRIMARY KEY, name VARCHAR(100), description VARCHAR(100), price FLOAT ); |
Добавим в ту же папку файл с товарами import.csv:
1 2 3 4 5 |
1000,Смартфон APPLE iPhone 6 16Gb Space Gray,Самый модный смартфон,40000 1001,LED телевизор SONY KDL,Самый яркий телевизор,20000 1002,Мясорубка MOULINEX HV4 ME,Рубит мясо просто супер,10000 1003,Вертикальный пылесос PHILIPS,Электрошвабра,18000 1004,Компьютерный стол AKMA,Широкий стол с тумбой,9000 |
Данные готовы, осталось всего лишь перегнать их в базу данных. Делать мы это будем с помощью пакетного задания. Здесь придется написать немного теории по Batch, чтобы понимать что происходит. Для начала, термины предметной области на картинке из стандарта JSR-352:
Step – это шаг пакетного задания. Содержит ItemReader, ItemProcessor и ItemWriter для чтения, обработки и записи результата обработки. Всё очень абстрактно, ведь спект типов задач очень широк. Например, шаг может читать данные из (файла|БД|REST-сервиса и т.д.), обрабатывать их произвольным образом (проверять, конвертировать в другие объекты и т.д.) и записывать результат обработки в (файл|БД|отправить на email и т.д.).
Job – это объект, который содержит последовательный список шагов Step и называется работой.
JobLauncher – это сервис, который запускает работу на выполнение с дополнительными параметрами. JobLauncher можно настроить под различные нужды, например, для асинхронного запуска задач.
JobRepository выполняет CRUD операции с мета-данными работ Job и шагов Step. Это нужно для некоторых “фишек” Batch, например, для перезапуска выполнения работы после аварийного отключения электричества.
Теперь вы знаете основные термины JSR-352 и Spring Batch.
Наша работа будет состоять из одного единственного шага Step step1. Начнём с Java Config, в котором опишем step1:
1 2 3 4 5 6 7 8 9 10 11 12 |
@Autowired public StepBuilderFactory stepBuilderFactory; @Bean public Step step1() { return stepBuilderFactory.get("step1") .<product, product="">chunk(10) .reader(reader()) .processor(processor()) .writer(writer()) .build(); } |
Вместо того, чтобы реализовывать интерфейс Step, воспользуемся поставляемой Batch’ем фабрикой шагов. Метод get возвращает объект StepBuilder и задает имя для будущего шага. Имя рекомендуется задавать уникальное в пределах одной работы, потому как при совпадении имён может возникнуть нежелательное поведение системы при перезапусках.
Далее, указывается chunk, который отвечает за “накапливание” пачки записей в ItemReader перед передачей в обработку ItemProcessor. В нашем случае этот параметр будет отвечать за размер транзакции при вставке записей в базу данных. Далее указываются бины, реализующие интерфейсы ItemReader, ItemProcessor и ItemWriter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
@Bean public FlatFileItemReader<product> reader() { FlatFileItemReader<product> reader = new FlatFileItemReader<>(); reader.setResource(new ClassPathResource("import.csv")); reader.setLineMapper(new DefaultLineMapper<product>() {{ setLineTokenizer(new DelimitedLineTokenizer() {{ setNames(new String[]{"id", "name", "description", "price"}); }}); setFieldSetMapper(new BeanWrapperFieldSetMapper<product>() {{ setTargetType(Product.class); }}); }}); return reader; }</product></product></product></product> @Bean public ItemProcessor<product, product=""> processor() { return person -> person; }</product,> @Autowired public DataSource dataSource; @Bean public JdbcBatchItemWriter<product> writer() { JdbcBatchItemWriter<product> writer = new JdbcBatchItemWriter<>(); writer.setItemSqlParameterSourceProvider( new BeanPropertyItemSqlParameterSourceProvider<>()); writer.setSql("INSERT INTO products " + "(product_id, name, description, price) " + "VALUES (:id, :name, :description, :price)"); writer.setDataSource(dataSource); return writer; } |
Spring Batch поставляет несколько стандартных реализаций интерфейсов ItemReader и ItemWriter из пакета org.springframework.batch.item. Нам подходит реализация FlatFileItemReader, так как она поддерживает формат csv, также воспользуемся реализацией JdbcBatchItemWriter. В дальнейшей статье мы напишем собственные реализации этих интерфейсов. Интерфейс ItemProcessor является функциональным и мы напишем для него лямбда-выражение, которое пока ничего не делает и возвращает объект без изменений.
Теперь, когда шаг сконфигурирован, приступим к конфигурированию самой работы Job. Воспользуемся стандартной Batch фабрикой JobBuilderFactory:
1 2 3 4 5 6 7 8 9 |
@Autowired public JobBuilderFactory jobBuilderFactory; @Bean public Job importUserJob() { return jobBuilderFactory.get("importProductsJob") .start(step1()) .build(); } |
Методом get начинаем конфигурирование новой работы с названия importProductsJob. Рекомендуется задавать уникальное название работы в пределах одной базы данных, используемой JobLauncher‘ом и JobRepository. Мы не будем описывать бины JobLauncher и JobRepository, так как пока устраивают стандартные реализации, которые Spring Boot уже загрузил в контекст.
Вот так выглядит окончательный конфигурационный файл:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
package javagrinko.batch.example.batch; import javagrinko.batch.example.model.Product; import lombok.extern.log4j.Log4j; import org.springframework.batch.core.Job; import org.springframework.batch.core.Step; import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; import org.springframework.batch.core.launch.support.RunIdIncrementer; import org.springframework.batch.item.ItemProcessor; import org.springframework.batch.item.database.BeanPropertyItemSqlParameterSourceProvider; import org.springframework.batch.item.database.JdbcBatchItemWriter; import org.springframework.batch.item.file.FlatFileItemReader; import org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper; import org.springframework.batch.item.file.mapping.DefaultLineMapper; import org.springframework.batch.item.file.transform.DelimitedLineTokenizer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import javax.sql.DataSource; @Configuration @Log4j public class BatchExampleConfig { @Autowired public JobBuilderFactory jobBuilderFactory; @Autowired public StepBuilderFactory stepBuilderFactory; @Autowired public DataSource dataSource; @Bean public FlatFileItemReader<product> reader() { FlatFileItemReader<product> reader = new FlatFileItemReader<>(); reader.setResource(new ClassPathResource("import.csv")); reader.setLineMapper(new DefaultLineMapper<product>() {{ setLineTokenizer(new DelimitedLineTokenizer() {{ setNames(new String[]{"id", "name", "description", "price"}); }}); setFieldSetMapper(new BeanWrapperFieldSetMapper<product>() {{ setTargetType(Product.class); }}); }}); return reader; }</product></product></product></product> @Bean public ItemProcessor<product, product=""> processor() { return person -> person; }</product,> @Bean public JdbcBatchItemWriter<product> writer() { JdbcBatchItemWriter<product> writer = new JdbcBatchItemWriter<>(); writer.setItemSqlParameterSourceProvider( new BeanPropertyItemSqlParameterSourceProvider<>()); writer.setSql("INSERT INTO products " + "(product_id, name, description, price) " + "VALUES (:id, :name, :description, :price)"); writer.setDataSource(dataSource); return writer; }</product></product> @Bean public Step step1() { return stepBuilderFactory.get("step1") .<product, product="">chunk(10) .reader(reader()) .processor(processor()) .writer(writer()) .build(); }</product,> @Bean public Job importUserJob() { return jobBuilderFactory.get("importProductsJob") .start(step1()) .build(); } } |
Для запуска задачи необходимо вызвать метод run бина JobLauncher:
1 2 3 4 5 6 7 8 9 |
@Autowired private JobLauncher jobLauncher; @Autowired private Job importUserJob; void start(){ jobLauncher.run(job, new JobParameters()); } |
Более детальное обсуждение в Spring Batch 3.0 – Часть 2: ItemReader, ItemPocessor и ItemWriter