Skip to content

Manual Spring Batch

Erick Souza edited this page May 8, 2018 · 43 revisions

Manual: criando Jobs com Spring Batch

O objetivo deste manual é descrever os passos para a criação de Jobs com Spring Batch. As ferramentas utilizadas serão:

  • VSCode
  • JDK
  • Maven
  • Spring
  • PostgreSQL

Criando uma máquina virtual

  1. Instale o Oracle VirtualBox

  2. Faça o download do Lubuntu

  3. Crie uma nova máquina virtual e siga os passos para instalação do Lubuntu

  4. No terminal, execute os seguintes comandos:

$ sudo apt-get update
$ sudo apt-get upgrade
$ sudo apt-get install gcc
$ sudo apt-get install make
  1. Na máquina virtual, acesse o menu Dispositivos -> Inserir imagem do CD dos adicionais para convidado...

  2. No terminal, navegue até o diretório montado e digite

$ sudo ./VBoxLinuxAdditions.run
  1. Reinicie a VM

Configurando o ambiente de desenvolvimento

  1. Crie um diretório e navegue para dentro dele
$ mkdir Tools
$ cd Tools
  1. Dentro do diretório, crie o script configure.sh com o seguinte conteúdo
# Instalação e configuração do JDK
sudo apt-get install -y python-software-properties
sudo add-apt-repository ppa:webupd8team/java
sudo apt-get update
sudo apt-get install -y oracle-java8-installer

# Instalação do Maven
apt-cache search maven
sudo apt-get install maven

# Instalação e configuração do PostgreSQL
sudo apt-get install -q -y postgresql pgadmin3
sudo -u postgres psql -c "ALTER USER postgres WITH PASSWORD 'postgres';"
POSTGRESQL_DIR="$(find /etc/postgresql -type d | grep main)"
POSTGRESQL_CONF=$POSTGRESQL_DIR"/postgresql.conf"
sudo chmod 777 $POSTGRESQL_CONF
sudo sed -i "59s/.*/listen_addresses = '*'/" $POSTGRESQL_CONF
PG_HBA_CONF=$POSTGRESQL_DIR"/pg_hba.conf"
sudo chmod 777 $PG_HBA_CONF
sudo sed -i "85s/.*/local all postgres trust/" $PG_HBA_CONF
sudo echo "host all all all password" >> $PG_HBA_CONF
sudo /etc/init.d/postgresql restart

# Instalação do editor VSCode
wget -O VSCode.tar.gz https://go.microsoft.com/fwlink/?LinkID=620884; 
tar -xvzf VSCode.tar.gz; 
rm VSCode.tar.gz;
sudo ln -s $(pwd)/VSCode-linux-x64/code /usr/local/bin/code;
sudo apt-get install libgconf-2-4

cd ..
sudo chmod -R 777 Tools
  1. Execute o script
$ sudo sh configure.sh | tee -a logfile

Criando a primeira aplicação

  1. Dentro da pasta do projeto, crie a seguinte estrutura de diretório:
└── src
    └── main
        └── java
            └── hello
$ mkdir -p src/main/java/hello
  1. Na raiz do projeto, crie o arquivo pom.xml com o seguinte conteúdo:
<?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>org.springframework</groupId>
    <artifactId>gs-batch-processing</artifactId>
    <version>0.1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.1.RELEASE</version>
    </parent>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-batch</artifactId>
        </dependency>
        <dependency>
            <groupId>org.hsqldb</groupId>
            <artifactId>hsqldb</artifactId>
        </dependency>
    </dependencies>


    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

O arquivo pom.xml contém as dependências do projeto que serão baixadas e configuradas pelo Maven.

  1. Configuração da persistência de dados

Inicialmente, a persistência de dados será realizada utilizando o HSQLDB (HyperSQL DataBase). Por padrão, o Spring utiliza o HSQLDB armazenando os dados em memória.

  • Crie o arquivo src/main/resources/sample-data.csv com o seguinte conteúdo:
Jill,Doe
Joe,Doe
Justin,Doe
Jane,Doe
John,Doe

Este arquivo contém os registros de nome e sobrenome que serão carregados em uma tabela de banco de dados de pessoas.

  • Crie o arquivo src/main/resources/schema-all.sql com o seguinte conteúdo:
DROP TABLE people IF EXISTS;

CREATE TABLE people  (
    person_id BIGINT IDENTITY NOT NULL PRIMARY KEY,
    first_name VARCHAR(20),
    last_name VARCHAR(20)
);

Os dados do arquivo anterior serão carregados nessa tabela.

Em sua configuração padrão, o Spring executa schema-@@platforma@@.sql para gerar o esquema. Ao utilizar -all, informamos ao Spring que o esquema poderá ser gerado em qualquer plataforma. Neste momento, será utilizado o esquema do HSQLDB.

  1. Criação das classes do projeto
  • Crie a classe src/main/java/hello/Person.java para encapsular os dados de pessoas vindos do banco.
package hello;

public class Person {

    private String lastName;
    private String firstName;

    public Person() {
    }

    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    @Override
    public String toString() {
        return "firstName: " + firstName + ", lastName: " + lastName;
    }

}
  • Crie um ItemProcessor para o job em src/main/java/hello/PersonItemProcessor.java com o seguinte conteúdo:
package hello;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.batch.item.ItemProcessor;

public class PersonItemProcessor implements ItemProcessor<Person, Person> {

    private static final Logger log = LoggerFactory.getLogger(PersonItemProcessor.class);

    @Override
    public Person process(final Person person) throws Exception {
        final String firstName = person.getFirstName().toUpperCase();
        final String lastName = person.getLastName().toUpperCase();

        final Person transformedPerson = new Person(firstName, lastName);

        log.info("Converting (" + person + ") into (" + transformedPerson + ")");

        return transformedPerson;
    }

}

O Processor é um componente que realiza a lógica de negócio para um Step. Por sua vez, o Step é um objeto que representa um passo individual de um Job. Em Xml, a estrutura ficaria da seguinte forma:

<job id="peopleJob">
  <step id="step1">
    <tasklet>
      <chunk processor="personItemProcessor"... />
    </tasklet>
 </step>
</job>

A lógica de negócio da classe PersonItemProcessor consiste em transformar o objeto em outro com os dados em caixa alta.

  • Crie uma classe para configurar a execução do Job em src/main/java/hello/BatchConfiguration.java:
package hello;

import javax.sql.DataSource;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecutionListener;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
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.database.BeanPropertyItemSqlParameterSourceProvider;
import org.springframework.batch.item.database.JdbcBatchItemWriter;
import org.springframework.batch.item.database.builder.JdbcBatchItemWriterBuilder;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder;
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 org.springframework.jdbc.core.JdbcTemplate;

@Configuration
@EnableBatchProcessing
public class BatchConfiguration {

    @Autowired
    public JobBuilderFactory jobBuilderFactory;

    @Autowired
    public StepBuilderFactory stepBuilderFactory;

    // tag::readerwriterprocessor[]
    @Bean
    public FlatFileItemReader<Person> reader() {
        return new FlatFileItemReaderBuilder<Person>()
            .name("personItemReader")
            .resource(new ClassPathResource("sample-data.csv"))
            .delimited()
            .names(new String[]{"firstName", "lastName"})
            .fieldSetMapper(new BeanWrapperFieldSetMapper<Person>() {{
                setTargetType(Person.class);
            }})
            .build();
    }

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

    @Bean
    public JdbcBatchItemWriter<Person> writer(DataSource dataSource) {
        return new JdbcBatchItemWriterBuilder<Person>()
            .itemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<>())
            .sql("INSERT INTO people (first_name, last_name) VALUES (:firstName, :lastName)")
            .dataSource(dataSource)
            .build();
    }
    // end::readerwriterprocessor[]

    // tag::jobstep[]
    @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<Person> writer) {
        return stepBuilderFactory.get("step1")
            .<Person, Person> chunk(10)
            .reader(reader())
            .processor(processor())
            .writer(writer)
            .build();
    }
    // end::jobstep[]
}

Vamos começar analisando as anotações utilizadas:

  • @Configuration indica que a classe contém definições de beans, ou seja, métodos anotados com @Bean. Uma classe anotada com @Configuration é implicitamente anotada com um @Component. A diferença que existe quando se utiliza @Configuration é que um método anotado com @Bean pode ser chamado por outro método anotado com @Bean.

  • @EnableBatchProcessing habilita features do Spring Batch, tais como o banco de dados baseado em memória (HSQLDB) utilizado nesse exemplo, além de configurações básicas para a criação do Job. Isso permite que o programador se preocupe apenas com as regras de negócio.

  • @Bean cria um bean de configuração gerenciado pelo container.

  • @Autowired permite que o Spring injete dependências na classe. Neste exemplo, jobBuilderFactory e stepBuilderFactory serão instanciados pelo container.

Métodos:

  • reader() lê dos dados do arquivo sample-data.csv para o processamento do Job.

  • processor() realiza o processamento das regras do Job. Aqui é executado o método process() de PersonItemProcessor criado no passo anterior.

  • writer(DataSource dataSource) grava os dados processados em uma tabela por meio de um DataSource injetado pela anotação @EnableBatchProcessing.

  • importUserJob(JobCompletionNotificationListener listener, Step step1) define o Job, indicando quais Steps serão executados. Neste exemplo, temos apenas um Step, definido no método seguinte, mas poderíamos ter vários. Nesta definição é necessário um incrementer, pois os jobs utilizam uma base de dados para manter o estado de execução.

  • step1(JdbcBatchItemWriter writer) define um Step, que contém um reader, um processor e um writer. A chamada chunk(10) indica quantos dados serão escritos por vez.

  • Crie a classe src/main/java/hello/JobCompletionNotificationListener.java para configurar notificações após a execução do Job:

package hello;

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.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! Time to verify the results");

			jdbcTemplate.query("SELECT first_name, last_name FROM people",
				(rs, row) -> new Person(
					rs.getString(1),
					rs.getString(2))
			).forEach(person -> log.info("Found <" + person + "> in the database."));
		}
	}
}

O objeto jdbcTemplate é injetado para permitir a execução da consulta Sql que irá verificar os registros inseridos na base de dados após a execução do Job, indicado pelo status BatchStatus.COMPLETED. Outras ações poderiam ser configuradas em caso de falha na execução do Job, cujo status seria BatchStatus.FAILED.

  • Crie a classe src/main/java/hello/Application.java para executar a aplicação:
package hello;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

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

Empacotando e executando a aplicação:

  1. Para compilar o projeto:
$ mvn compile
  1. Para empacotar o projeto em um jar:
$ mvn package
  1. Para executar a aplicação:
$ java -jar target/gs-batch-processing-0.1.0.jar

A saída da execução deve conter:

Converting (firstName: Jill, lastName: Doe) into (firstName: JILL, lastName: DOE)
Converting (firstName: Joe, lastName: Doe) into (firstName: JOE, lastName: DOE)
Converting (firstName: Justin, lastName: Doe) into (firstName: JUSTIN, lastName: DOE)
Converting (firstName: Jane, lastName: Doe) into (firstName: JANE, lastName: DOE)
Converting (firstName: John, lastName: Doe) into (firstName: JOHN, lastName: DOE)
Found <firstName: JILL, lastName: DOE> in the database.
Found <firstName: JOE, lastName: DOE> in the database.
Found <firstName: JUSTIN, lastName: DOE> in the database.
Found <firstName: JANE, lastName: DOE> in the database.
Found <firstName: JOHN, lastName: DOE> in the database.

Customizando a aplicação

Até o momento utilizamos as configurações padronizadas do Spring Boot. Vamos realizar algumas alterações na configuração e no exemplo:

  • Alterar o banco de HSQLDB para PostgreSql, realizando a persistência em disco.
  1. Crie o banco de dados testbatch utilizando o pgadmin e execute o seguinte script:
CREATE TABLE people  (
    person_id Bigserial PRIMARY KEY NOT NULL,
    first_name VARCHAR(20),
    last_name VARCHAR(20)
);
  1. Crie o arquivo src/main/resources/application.properties com o seguinte conteúdo:
spring.datasource.url=jdbc:postgresql://localhost:5432/testbatch
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.username=postgres
spring.datasource.password=postgres
spring.datasource.platform=postgresql
spring.batch.initialize-schema=always
  1. No arquivo pom.xml atualize as dependências para:
    <dependencies>
    	       <dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-batch</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-jdbc</artifactId>
		</dependency>	

		<dependency>
			<groupId>org.postgresql</groupId>
			<artifactId>postgresql</artifactId>
			<scope>runtime</scope>
		</dependency>
    </dependencies>
  1. Compile, empacote e execute novamente a aplicação.
  • Criar um novo step para copiar os dados da tabela people para a new_people.
  1. Crie a tabela new_people no pgadmin:
CREATE TABLE new_people  (
    person_id Bigserial PRIMARY KEY NOT NULL,
    first_name VARCHAR(20),
    last_name VARCHAR(20)
);
  1. Inclua os métodos abaixo na classe BatchConfiguration:
    @Bean
    public ItemReader<Person> reader2(DataSource dataSource) {
        JdbcCursorItemReader<Person> databaseReader = new JdbcCursorItemReader<>();
 
        databaseReader.setDataSource(dataSource);
        databaseReader.setSql("SELECT * FROM people");
        databaseReader.setRowMapper(new BeanPropertyRowMapper<>(Person.class));
 
        return databaseReader;
    }

    @Bean
    public JdbcBatchItemWriter<Person> writer2(DataSource dataSource) {

        return new JdbcBatchItemWriterBuilder<Person>()
            .itemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<>())
            .sql("INSERT INTO new_people (first_name, last_name) VALUES (:firstName, :lastName)")
            .dataSource(dataSource)
            .build();
    }

    @Bean
    public Step step2(JdbcBatchItemWriter<Person> writer2,DataSource dataSource) {
        return stepBuilderFactory.get("step2")
            .<Person, Person> chunk(10)
            .reader(reader2(dataSource))            
            .writer(writer2)
            .build();
    }

O novo step (step2) utilizará reader2 e writer2. O processor não é necessário e será omitido, pois não há processamento dos dados. Será necessário importar as classes abaixo, que serão utilizadas pelo reader para ler os dados da tabela no banco:

import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.database.JdbcCursorItemReader;
  1. Altere o método importUserJob para
    @Bean
    public Job importUserJob(JobCompletionNotificationListener listener, Step step1, Step step2) {
        return jobBuilderFactory.get("importUserJob")
            .incrementer(new RunIdIncrementer())
            .listener(listener)
            //.flow(step1)            
            .start(step1)
            .next(step2)
            //.end()
            .build();
    }

Repare que o método flow é substituído por start, que permite a execução de vários Steps dentro do Job utilizando o método next.

  1. Compile, empacote e execute novamente a aplicação.
  • Executar o Job via URL
  1. Inclua a seguinte linha no arquivo application.properties:
spring.batch.job.enabled=false
  1. Acrescente a dependência no pom.xml:
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
  1. Crie a classe src/main/java/hello/WebController.java
package hello;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class WebController {
	@Autowired
	JobLauncher jobLauncher;

	@Autowired
	Job job;

	@RequestMapping("/runjob")
	public String handle() throws Exception {
		Logger logger = LoggerFactory.getLogger(this.getClass());
		try {
			JobParameters jobParameters = new JobParametersBuilder().addLong("time", System.currentTimeMillis())
					.toJobParameters();
			jobLauncher.run(job, jobParameters);
		} catch (Exception e) {
			logger.info(e.getMessage());
		}
		return "Done! Check Console Window for more details";
	}
}
  1. Compile, empacote e execute novamente a aplicação.

  2. Abra o navegador e acesse o endereço http://localhost:8080/runjob

A mensagem Done! Check Console Window for more details deverá surgir na página. Confira a execução do Job no console.

Utilização do DTP Infra Batch

Referências