Статьи

Spring Batch CSV Processing

обзор

Темы, которые мы будем обсуждать, включают в себя основные концепции пакетной обработки в Spring Batch и способы импорта данных из CSV в базу данных.

0 — пример приложения Spring Batch CSV Processing

Мы создаем приложение, которое демонстрирует основы Spring Batch для обработки файлов CSV. Наше демонстрационное приложение позволит нам обрабатывать CSV-файл, содержащий сотни записей названий японских аниме.

0.1 — CSV

Я скачал CSV, который мы будем использовать из этого репозитория Github , и он содержит довольно полный список аниме.

Вот скриншот CSV, открытого в Microsoft Excel

Посмотреть и скачать код с Github

1 — Структура проекта

2 — Зависимости проекта

Помимо типичных зависимостей Spring Boot, мы включили spring-boot-starter-batch, которая является зависимостью для Spring Batch, как следует из названия, и hsqldb для базы данных в памяти. Мы также включаем commons-lang3 для ToStringBuilder.

01
02
03
04
05
06
07
08
09
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
<?xml version="1.0" encoding="UTF-8"?>
    <modelVersion>4.0.0</modelVersion>
 
    <groupId>com.michaelcgood</groupId>
    <artifactId>michaelcgood-spring-batch-csv</artifactId>
    <version>0.0.1</version>
    <packaging>jar</packaging>
 
    <name>michaelcgood-spring-batch-csv</name>
    <description>Michael C  Good - Spring Batch CSV Example Application</description>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.7.RELEASE</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-batch</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
 
        <dependency>
            <groupId>org.hsqldb</groupId>
            <artifactId>hsqldb</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.6</version>
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
 
 
</project>

3 — Модель

Это POJO, который моделирует поля аниме. Поля:

  • МНЕ БЫ. Для простоты мы рассматриваем идентификатор как строку. Однако это можно изменить на другой тип данных, например, Integer или Long.
  • Заглавие. Это название аниме, и оно подходит для строки.
  • Описание. Это описание аниме, которое длиннее заголовка, и его также можно рассматривать как строку.

Важно отметить, что наш конструктор класса для трех полей: общедоступный AnimeDTO (идентификатор строки, заголовок строки, описание строки). Это будет использовано в нашем приложении. Также, как обычно, нам нужно сделать конструктор по умолчанию без параметров, иначе Java выдаст ошибку.

01
02
03
04
05
06
07
08
09
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
package com.michaelcgood;
 
import org.apache.commons.lang3.builder.ToStringBuilder;
/**
 * Contains the information of a single anime
 *
 * @author Michael C Good michaelcgood.com
 */
 
public class AnimeDTO {
     
    public String getId() {
        return id;
    }
 
    public void setId(String id) {
        this.id = id;
    }
 
    public String getTitle() {
        return title;
    }
 
    public void setTitle(String title) {
        this.title = title;
    }
 
    public String getDescription() {
        return description;
    }
 
    public void setDescription(String description) {
        this.description = description;
    }
 
 
 
    private String id;
     
 
 
 
    private String title;
    private String description;
     
    public AnimeDTO(){
         
    }
     
    public AnimeDTO(String id, String title, String description){
        this.id = id;
        this.title = title;
        this.description = title;
    }
     
 
     
       @Override
        public String toString() {
           return new ToStringBuilder(this)
                   .append("id", this.id)
                   .append("title", this.title)
                   .append("description", this.description)
                   .toString();
       }
 
 
}

4 — Конфигурация файла CSV в базу данных

В этом классе много чего происходит, и не все написано сразу, поэтому мы собираемся пройтись по коду поэтапно. Посетите Github, чтобы увидеть код в полном объеме.

4.1 — Читатель

Как говорится в документации Spring Batch, FlatFileIteamReader будет «читать строки данных из плоского файла, которые обычно описывают записи с полями данных, которые определены фиксированными позициями в файле или разделены каким-либо специальным символом (например, запятой)».

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
@Bean
    public FlatFileItemReader<AnimeDTO> csvAnimeReader(){
        FlatFileItemReader<AnimeDTO> reader = new FlatFileItemReader<AnimeDTO>();
        reader.setResource(new ClassPathResource("animescsv.csv"));
        reader.setLineMapper(new DefaultLineMapper<AnimeDTO>() {{
            setLineTokenizer(new DelimitedLineTokenizer() {{
                setNames(new String[] { "id", "title", "description" });
            }});
            setFieldSetMapper(new BeanWrapperFieldSetMapper<AnimeDTO>() {{
                setTargetType(AnimeDTO.class);
            }});
        }});
        return reader;
    }

Важные точки:

    • FlatFileItemReader параметризован с моделью. В нашем случае это AnimeDTO.
    • FlatFileItemReader должен установить ресурс. Он использует метод setResource . Здесь мы устанавливаем ресурс animescsv.csv
    • Метод setLineMapper преобразует строки в объекты, представляющие элемент. Наша строка будет аниме-записью, состоящей из идентификатора, заголовка и описания. Эта строка превращается в объект. Обратите внимание, что DefaultLineMapper параметризован с нашей моделью, AnimeDTO.
    • Тем не менее, LineMapper получает необработанную строку, что означает, что есть работа, которая должна быть выполнена для правильного отображения полей. Строка должна быть размечена в FieldSet, о котором заботится DelimitedLineTokenizer. DelimitedLineTokenizer возвращает FieldSet.
    • Теперь, когда у нас есть FieldSet, нам нужно отобразить его. setFieldSetMapper используется для получения объекта FieldSet и отображения его содержимого в DTO, в нашем случае это AnimeDTO.

4.2 — Процессор

Если мы хотим преобразовать данные перед записью их в базу данных, необходим ItemProcessor. Наш код фактически не применяет какую-либо бизнес-логику для преобразования данных, но мы допускаем такую ​​возможность.

4.2.1 — Процессор в CsvFileToDatabaseConfig.Java

csvAnimeProcessor возвращает новый экземпляр объекта AnimeProcessor, который мы рассмотрим ниже.

1
2
3
4
@Bean
    ItemProcessor<AnimeDTO, AnimeDTO> csvAnimeProcessor() {
        return new AnimeProcessor();
    }

4.2.2 — AnimeProcessor.Java

Если мы хотим применить бизнес-логику перед записью в базу данных, вы можете манипулировать строками перед записью в базу данных. Например, вы можете добавить toUpperCase () после getTitle, чтобы заглавные буквы заголовка перед записью в базу данных. Однако я решил не делать этого и не применять какую-либо другую бизнес-логику для этого примера процессора, поэтому никаких манипуляций не производится. Процессор здесь просто для демонстрации.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.michaelcgood;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
import org.springframework.batch.item.ItemProcessor;
 
public class AnimeProcessor implements ItemProcessor<AnimeDTO, AnimeDTO> {
     
    private static final Logger log = LoggerFactory.getLogger(AnimeProcessor.class);
     
    @Override
    public AnimeDTO process(final AnimeDTO AnimeDTO) throws Exception {
         
        final String id = AnimeDTO.getId();
        final String title = AnimeDTO.getTitle();
        final String description = AnimeDTO.getDescription();
 
        final AnimeDTO transformedAnimeDTO = new AnimeDTO(id, title, description);
 
        log.info("Converting (" + AnimeDTO + ") into (" + transformedAnimeDTO + ")");
 
        return transformedAnimeDTO;
    }
 
}

4.3 — Писатель

Метод csvAnimeWriter отвечает за фактическую запись значений в нашу базу данных. Наша база данных является HSQLDB в памяти, однако это приложение позволяет нам легко заменять одну базу данных на другую. Источник данных автоматически подключен.

1
2
3
4
5
6
7
8
@Bean
    public JdbcBatchItemWriter<AnimeDTO> csvAnimeWriter() {
         JdbcBatchItemWriter<AnimeDTO> excelAnimeWriter = new JdbcBatchItemWriter<AnimeDTO>();
         excelAnimeWriter.setItemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<AnimeDTO>());
         excelAnimeWriter.setSql("INSERT INTO animes (id, title, description) VALUES (:id, :title, :description)");
         excelAnimeWriter.setDataSource(dataSource);
            return excelAnimeWriter;
    }

4.4 — Шаг

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

Теперь, когда мы создали ридер и процессор для данных, нам нужно их записать. Для чтения мы использовали чанк-ориентированную обработку, то есть мы считывали данные по одному. Обработка чанков также включает в себя создание «чанков», которые будут записаны в пределах границ транзакции. Для обработки, ориентированной на фрагменты, вы устанавливаете интервал фиксации, и как только число прочитанных элементов становится равным установленному интервалу фиксации, весь фрагмент записывается через ItemWriter, и транзакция фиксируется. Мы устанавливаем размер интервала чанка в 1.

Я предлагаю прочитать документацию Spring Batch о чан-ориентированной обработке .

Затем читатель, процессор и писатель вызывают методы, которые мы написали.

1
2
3
4
5
6
7
8
9
@Bean
    public Step csvFileToDatabaseStep() {
        return stepBuilderFactory.get("csvFileToDatabaseStep")
                .<AnimeDTO, AnimeDTO>chunk(1)
                .reader(csvAnimeReader())
                .processor(csvAnimeProcessor())
                .writer(csvAnimeWriter())
                .build();
    }

4.5 — Работа

Работа состоит из шагов. Мы передаем параметр в задание ниже, потому что мы хотим отслеживать завершение задания.

1
2
3
4
5
6
7
8
9
@Bean
    Job csvFileToDatabaseJob(JobCompletionNotificationListener listener) {
        return jobBuilderFactory.get("csvFileToDatabaseJob")
                .incrementer(new RunIdIncrementer())
                .listener(listener)
                .flow(csvFileToDatabaseStep())
                .end()
                .build();
    }

5 — слушатель уведомления о завершении работы

Класс ниже автоматически связывает JdbcTemplate, потому что мы уже установили источник данных и хотим легко выполнить наш запрос. Результатом нашего запроса является список объектов AnimeDTO. Для каждого возвращенного объекта мы создадим сообщение в нашей консоли, чтобы показать, что элемент был записан в базу данных.

01
02
03
04
05
06
07
08
09
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
package com.michaelcgood;
 
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.listener.JobExecutionListenerSupport;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Component;
 
@Component
public class JobCompletionNotificationListener extends JobExecutionListenerSupport {
 
    private static final Logger log = LoggerFactory.getLogger(JobCompletionNotificationListener.class);
 
    private final JdbcTemplate jdbcTemplate;
 
    @Autowired
    public JobCompletionNotificationListener(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
 
    @Override
    public void afterJob(JobExecution jobExecution) {
        if(jobExecution.getStatus() == BatchStatus.COMPLETED) {
            log.info("============ JOB FINISHED ============ Verifying the results....\n");
 
            List<AnimeDTO> results = jdbcTemplate.query("SELECT id, title, description FROM animes", new RowMapper<AnimeDTO>() {
                @Override
                public AnimeDTO mapRow(ResultSet rs, int row) throws SQLException {
                    return new AnimeDTO(rs.getString(1), rs.getString(2), rs.getString(3));
                }
            });
 
            for (AnimeDTO AnimeDTO : results) {
                log.info("Discovered <" + AnimeDTO + "> in the database.");
            }
 
        }
    }
     
}

6 — SQL

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

1
2
3
4
5
6
DROP TABLE animes IF EXISTS;
CREATE TABLE animes  (
    id VARCHAR(10),
    title VARCHAR(400),
    description VARCHAR(999)
);

6 — Главный

Это стандартный класс с main (). Как говорится в документации Spring, @SpringBootApplication — это удобная аннотация, включающая @Configuration , @EnableAutoConfiguration , @EnableWebMvc и @ComponentScan .

01
02
03
04
05
06
07
08
09
10
11
12
package com.michaelcgood;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 
@SpringBootApplication
public class SpringBatchCsvApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(SpringBatchCsvApplication.class, args);
    }
}

7 — Демо

7.1 — Конвертация

FieldSet подается через процессор, и «Converting» выводится на консоль.

7.2 — Обнаружение новых предметов в базе данных

Когда Spring Batch Job завершен, мы выбираем все записи и распечатываем их по отдельности.

7.3 — Пакетный процесс завершен

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

1
2
Job: [FlowJob: [name=csvFileToDatabaseJob]] completed with the following parameters: [{run.id=1, -spring.output.ansi.enabled=always}] and the following status: [COMPLETED]
Started SpringBatchCsvApplication in 36.0 seconds (JVM running for 46.616)

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

Spring Batch основывается на подходе разработки на основе POJO и удобстве Spring Framework, что облегчает разработчикам создание пакетной обработки корпоративного уровня.

Исходный код есть на Github

Опубликовано на Java Code Geeks с разрешения Майкла Гуда, партнера нашей программы JCG . Смотреть оригинальную статью здесь: Spring Batch CSV Processing

Мнения, высказанные участниками Java Code Geeks, являются их собственными.