Lingue disponibili:

File a Lunghezza Fissa con Spring Batch 6.0: La “Gioia” dei Dati Legacy

Questo post è stato originariamente scritto in inglese. La traduzione potrebbe non riflettere il 100% delle idee originali dell'autore.

Se sei uno sviluppatore che ha gestito elaborazione di buste paga o riconciliazione bancaria/finanziaria in un’azienda che utilizza Spring, probabilmente hai lavorato con Spring Batch. Confesso di non esserne un grande fan; ha quella caratteristica verbosità e overhead dell’ecosistema Java, che fa sembrare che anche il lavoro più semplice richieda molta più struttura del necessario. Ma a cosa serve lamentarsi? La tecnologia che la tua azienda utilizza è ciò che garantisce la tua sopravvivenza (casa, cibo, vestiti). Quindi, lamentarsi non è l’argomento di oggi.

L’ambito di questo post è estremamente semplice: dato un file posizionale generato tramite un mainframe utilizzando il template qui sotto, creeremo un’applicazione Spring Batch (utilizzando la versione 6.0, la più recente ad oggi) per leggere facilmente il file, creare un oggetto e stampare il contenuto in console in formato JSON. La parte chiave qui è leggere il file e trasformarlo in un oggetto Java standard. Per quanto riguarda lo step di scrittura, puoi gestirlo come preferisci: salvarlo in un database, scriverlo in un altro file, qualsiasi cosa tu voglia fare.

Per questo, considereremo il template qui sotto:

CAMPOINIZIOFINELUNGHEZZAFORMATO (COBOL PIC)TIPO DATIDESCRIZIONE
COMIC-TITLE013030PIC X(30)ALFANUMERICOIl titolo del fumetto. Allineato a sinistra, riempito con spazi.
ISSUE-NUM313505PIC 9(05)NUMERICO (ZONED)Il numero di sequenza dell’uscita. Allineato a destra, riempito con zeri.
PUBLISHER365520PIC X(20)ALFANUMERICOIl nome dell’editore. Allineato a sinistra, riempito con spazi.
PUB-DATE566510PIC X(10)DATA (ISO)Data di pubblicazione in formato YYYY-MM-DD. Trattato come testo.
CVR-PRICE667207PIC 9(07)NUMERICO (ZONED)Prezzo di copertina. Allineato a destra, riempito con zeri. Nota: la gestione dei decimali dipende dalla logica di parsing.

E supponiamo di ricevere il file qui sotto sulla piattaforma distribuita. Nota che quando si tratta di formati numerici (PIC 9), riempiamo il campo con zeri iniziali, e per i campi alfanumerici (PIC X), riempiamo la lunghezza fissa con spazi finali.

Action Comics                 00001DC Comics           1938-04-180000.10
Detective Comics              00027DC Comics           1939-03-300000.10
Batman                        00001DC Comics           1940-04-240000.10
Superman                      00001DC Comics           1939-05-180000.10
Wonder Woman                  00001DC Comics           1942-07-220000.10
Flash Comics                  00001DC Comics           1940-01-010000.10
Green Lantern                 00001DC Comics           1941-07-010000.10
Amazing Fantasy               00015Marvel              1962-08-100000.12
The Incredible Hulk           00001Marvel              1962-05-010000.12
Fantastic Four                00001Marvel              1961-11-010000.10
Journey into Mystery          00083Marvel              1962-08-010000.12
Tales of Suspense             00039Marvel              1963-03-010000.12
The X-Men                     00001Marvel              1963-09-010000.12
The Avengers                  00001Marvel              1963-09-010000.12
Daredevil                     00001Marvel              1964-04-010000.12
Showcase                      00004DC Comics           1956-09-010000.10
Justice League of America     00001DC Comics           1960-10-010000.10
The Brave and the Bold        00028DC Comics           1960-02-010000.10
Swamp Thing                   00001DC Comics           1972-10-010000.20
Giant-Size X-Men              00001Marvel              1975-05-010000.50
Crisis on Infinite Earths     00001DC Comics           1985-04-010000.75
Watchmen                      00001DC Comics           1986-09-010001.50
The Dark Knight Returns       00001DC Comics           1986-02-010002.95
Maus                          00001Pantheon            1986-01-010003.50
Sandman                       00001Vertigo             1989-01-010001.50
Spawn                         00001Image Comics        1992-05-010001.95
Savage Dragon                 00001Image Comics        1992-06-010001.95
WildC.A.T.s                   00001Image Comics        1992-08-010001.95
Youngblood                    00001Image Comics        1992-04-010002.50
Hellboy: Seed of Destruction  00001Dark Horse          1994-03-010002.50
Sin City                      00001Dark Horse          1991-04-010002.25
Preacher                      00001Vertigo             1995-04-010002.50
The Walking Dead              00001Image Comics        2003-10-010002.99
Invincible                    00001Image Comics        2003-01-010002.99
Saga                          00001Image Comics        2012-03-140002.99
Paper Girls                   00001Image Comics        2015-10-070002.99
Monstress                     00001Image Comics        2015-11-040004.99
Descender                     00001Image Comics        2015-03-040002.99
East of West                  00001Image Comics        2013-03-270003.50
Ms. Marvel                    00001Marvel              2014-02-010002.99
Miles Morales: Spider-Man     00001Marvel              2018-12-120003.99
House of X                    00001Marvel              2019-07-240005.99
Powers of X                   00001Marvel              2019-07-310005.99
Batman: Court of Owls         00001DC Comics           2011-09-210002.99
Doomsday Clock                00001DC Comics           2017-11-220004.99
Immortal Hulk                 00001Marvel              2018-06-060004.99
Something is Killing Child    00001BOOM! Studios       2019-09-040003.99
Department of Truth           00001Image Comics        2020-09-300003.99
Nice House on the Lake        00001DC Black Label      2021-06-010003.99
Ultimate Spider-Man           00001Marvel              2024-01-100005.99

Iniziamo.

1. Configurazione iniziale che ogni sviluppatore Java odia

Per velocizzare la creazione della base, ho impostato un progetto utilizzando spring initializr. In pratica, ho creato un progetto Spring Boot con Maven, Java 21 e Spring Boot 4.0.0. Sì, l’ho creato senza alcuna dipendenza, dato che modificherò manualmente il pom.xml. Credo che Spring Boot 4.0 includa già Spring Batch 6 (se aggiunto come dipendenza), ma onestamente, ci sono cose che preferisco sistemare manualmente.

Con il progetto creato, costruiamo il nostro pom.xml:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>4.0.0</version>
        <relativePath/>
    </parent>

    <groupId>me.doismiu</groupId>
    <artifactId>spring-batch-bean-io</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-batch-bean-io</name>
    <description>Spring Batch 6 BeanIO Example</description>

    <properties>
        <java.version>21</java.version>
        <spring-batch.version>6.0.0</spring-batch.version>
        <spring-framework.version>7.0.0</spring-framework.version>
    </properties>

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

        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>org.beanio</groupId>
            <artifactId>beanio</artifactId>
            <version>2.1.0</version>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.batch</groupId>
                    <artifactId>spring-batch-core</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework.batch</groupId>
                    <artifactId>spring-batch-infrastructure</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework</groupId>
                    <artifactId>spring-beans</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
        </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>
    </dependencies>

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

    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <releases>
                <enabled>false</enabled>
            </releases>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </pluginRepository>
        <pluginRepository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <releases>
                <enabled>false</enabled>
            </releases>
        </pluginRepository>
    </pluginRepositories>
</project>

Come avrai notato, lavoreremo con le seguenti librerie in questo progetto:

  • Spring Batch 6.0

  • Spring Boot 4.0.0

  • Spring Boot Starter JDBC. Dato che dobbiamo configurare il TransactionManager per la connessione al database, avremo bisogno di questa libreria.

  • BeanIO 2.1.0. La libreria che esegue la magia di convertire campi posizionali in attributi di oggetti; inoltre, viene fornita con una serie di classi speciali per Spring Batch!

  • Librerie Jackson per convertire facilmente l’oggetto in una struttura JSON nel nostro Writer.

Ora, creeremo la nostra struttura Main e la connessione al database. Qui vediamo il primo grande cambiamento in Spring Batch 6: la cosiddetta "Infrastruttura batch senza risorse per impostazione predefinita". Chiunque abbia lavorato con Spring Batch sa che era quasi sempre necessario collegare il Batch a un database, poiché questa tecnologia memorizza i metadati per le esecuzioni di Job e Step, che sono di estrema importanza per il controllo dell’esecuzione. Tuttavia, nell’ultima versione, Batch non ha bisogno di essere collegato a nessun database per impostazione predefinita. Come ho detto, poiché questo è importante, lo configureremo comunque. Nel nostro scenario, il batch richiede la struttura qui sotto per funzionare:

In questo progetto, ho utilizzato PostgreSQL via Docker per semplificare le cose, ma dovrebbe funzionare con qualsiasi database, anche NoSQL come MongoDB. Per fare ciò, ho creato il seguente docker compose.

version: '3.8'
services:
  postgres:
    image: postgres:15-alpine
    container_name: comic_postgres
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: comicbatch
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Ma come ho detto, qualsiasi database andrà bene. Dopo aver avviato il docker compose, avremo un container chiamato comic_postgres in esecuzione sul nostro Docker locale. Con ciò, possiamo procedere a configurare il file application.properties del nostro progetto.

spring.application.name=spring-batch-bean-io
spring.datasource.url=jdbc:postgresql://localhost:5432/comicbatch
spring.datasource.username=user
spring.datasource.password=password
spring.datasource.driver-class-name=org.postgresql.Driver
logging.level.org.springframework.jdbc.core=DEBUG
logging.level.org.springframework.transaction=DEBUG
spring.batch.jdbc.initialize-schema=never

Qui ho configurato JDBC per non creare tabelle all’inizializzazione. TEORICAMENTE, Batch dovrebbe creare la struttura dei metadati senza problemi, ma ammetto di aver utilizzato estensivamente le versioni 4 e 5 di Spring Batch, e MAI ha creato la struttura correttamente. Questo porta a errori durante l’esecuzione dell’applicazione, molto probabilmente a causa di colonne mancanti o tipi errati. Ancora peggio sarebbe prendere uno script a caso da internet e creare le tabelle direttamente nel database. Invece, abbiamo una soluzione che legge la struttura corretta fornita dalla libreria stessa e la adatta al tipo di database che stiamo utilizzando. Ecco come appare la nostra classe Main, implementando questa logica del database:

package me.doismiu.fixedlength;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.datasource.init.DataSourceInitializer;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;

import javax.sql.DataSource;

@SpringBootApplication
public class ComicBatchApplication {

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

    @Bean
    public DataSourceInitializer databaseInitializer(DataSource dataSource) {
        ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
        populator.addScript(new ClassPathResource("org/springframework/batch/core/schema-postgresql.sql"));
        populator.setContinueOnError(true);
        DataSourceInitializer initializer = new DataSourceInitializer();
        initializer.setDataSource(dataSource);
        initializer.setDatabasePopulator(populator);
        return initializer;
    }

}

Ciò garantisce che alla prima esecuzione del Batch, crei la struttura appropriata prevista dal framework, poiché il file di creazione è fornito da Spring Batch stesso. Nota che ho incluso anche la riga populator.setContinueOnError(true). Ciò significa che ogni volta che ci sarà un errore nella struttura SQL, verrà ignorato, dato che questo script verrà eseguito ad ogni esecuzione del Batch. Ci sono modi più eleganti per gestire questo, o potresti anche rimuovere questo metodo dopo la prima esecuzione se preferisci. Ma per il nostro esempio, funziona bene.

Approfittiamone per finalizzare la configurazione del database creando una classe vuota, ma con annotazioni che specificano la configurazione del TransactionManager:

package me.doismiu.fixedlength.config;

import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.Isolation;

@Configuration
@EnableBatchProcessing
@EnableJdbcJobRepository(dataSourceRef = "dataSource", transactionManagerRef = "transactionManager", isolationLevelForCreate = Isolation.READ_COMMITTED, tablePrefix = "BATCH_")
public class JDBCJobRepositoryConfig {

}

2. L’oggetto con annotazioni BeanIO

Per questo progetto, dobbiamo creare un modello chiamato Comic contenente le informazioni del template posizionale che ho mostrato all’inizio del post. Possiamo farlo via XML o via annotazione, è una questione di gusti personali. Nell’esempio di questo progetto, utilizzerò la versione con annotazioni, poiché sento che organizza la struttura di un template posizionale in modo più semplice.

package me.doismiu.fixedlength.model;

import org.beanio.annotation.Field;
import org.beanio.annotation.Record;
import org.beanio.builder.Align;

import java.math.BigDecimal;
import java.time.LocalDate;

@Record
public class Comic {

    @Field(at = 0, length = 30, padding = ' ', align = Align.LEFT)
    private String title;

    @Field(at = 30, length = 5, padding = '0', align = Align.RIGHT)
    private int issueNumber;

    @Field(at = 35, length = 20, padding = ' ', align = Align.LEFT)
    private String publisher;

    @Field(at = 55, length = 10, format = "yyyy-MM-dd")
    private LocalDate publicationDate;

    @Field(at = 65, length = 7, padding = '0', align = Align.RIGHT)
    private BigDecimal coverPrice;

    // Getters and Setters
}

Per quanto riguarda il modello, alcuni punti meritano di essere evidenziati:

  • Gli attributi non devono necessariamente essere nell’ordine del template, ma raccomando vivamente di mantenerlo così per facilitare la manutenzione del codice.

  • Utilizziamo la logica padding + align per riempire con zeri iniziali o spazi finali, come stabilito nel layout del file. Inoltre, diciamo a BeanIO che il campo data previsto ha un formato definito. Ne parleremo più approfonditamente quando scriveremo il Reader dell’applicazione.

E questo è tutto, semplice così. Ora passiamo alla creazione del nostro Reader e Writer.

3. Un Reader che giustifica l’esistenza di questo post, un Writer che fa quello che diavolo vuole

Tipicamente, la logica Batch impone di creare un’applicazione nel seguente ordine: Job -> Step -> Reader/Processor/Writer. Ma poiché ogni pezzo dipende dal successivo, inizieremo dalla fine.

Nel nostro Reader, utilizzeremo il meglio che BeanIO offre per i Batch. Creeremo la nostra logica di lettura del file utilizzando lo standard StreamBuilder della libreria. Utilizzeremo anche la logica Skip di BeanIO per evitare di rileggere le voci del database quando si riprende un servizio che è stato interrotto. Per far funzionare questo, invece di utilizzare il Batch standard FileFlatItemReader, utilizzeremo AbstractItemCountingItemStreamItemReader. Questo ci consente di riprendere qualsiasi processo di lettura che è stato interrotto, combinato con la logica JumpItem che creiamo con BeanIO. In pratica, configureremo lo StreamBuilder nel costruttore della classe e gestiremo correttamente l’apertura e la chiusura degli stream. Questo ci dà il seguente reader:


package me.doismiu.fixedlength.reader;

import me.doismiu.fixedlength.handler.LocalDateTypeHandler;
import me.doismiu.fixedlength.model.Comic;
import org.beanio.BeanReader;
import org.beanio.StreamFactory;
import org.beanio.builder.FixedLengthParserBuilder;
import org.beanio.builder.StreamBuilder;

import org.springframework.batch.infrastructure.item.ItemStreamException;
import org.springframework.batch.infrastructure.item.support.AbstractItemCountingItemStreamItemReader;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;

import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;

public class ComicItemsReader extends AbstractItemCountingItemStreamItemReader<Comic> {

    private final Resource resource;
    private final StreamFactory streamFactory;
    private BeanReader beanReader;

    public ComicItemsReader(Resource resource) {
        this.resource = resource;

        setName("comicItemsReader");

        this.streamFactory = StreamFactory.newInstance();
        StreamBuilder builder = new StreamBuilder("comicStream")
                .format("fixedlength")
                .parser(new FixedLengthParserBuilder())
                .addTypeHandler(LocalDate.class, new LocalDateTypeHandler("yyyy-MM-dd"))
                .addRecord(Comic.class);
        this.streamFactory.define(builder);
    }

    @Override
    protected void jumpToItem(int itemIndex) throws Exception {
        if (beanReader != null) {
            beanReader.skip(itemIndex);
        }
    }

    @Override
    protected void doOpen() throws Exception {
        Assert.notNull(resource, "Input resource must be set");

        if (!resource.exists()) {
            throw new ItemStreamException("Input resource does not exist: " + resource);
        }

        this.beanReader = streamFactory.createReader(
                "comicStream",
                new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8));
    }

    @Override
    protected Comic doRead() throws Exception {
        if (beanReader == null) {
            return null;
        }
        return (Comic) beanReader.read();
    }

    @Override
    protected void doClose() throws Exception {
        if (beanReader != null) {
            beanReader.close();
            beanReader = null;
        }
    }
}

Nota che quando si implementa questa classe, il compilatore segnalerà un errore sulla seguente riga:

.addTypeHandler(LocalDate.class, new LocalDateTypeHandler("yyyy-MM-dd"))

Questo perché non abbiamo ancora creato il LocalDateTypeHandler. E perché abbiamo bisogno di un gestore specifico per le date? In un’applicazione di esempio, potrei ignorare un attributo LocalDate e gestire tutto come Stringhe e Interi. Ma come ho detto, uno dei principali tipi di file elaborati nei batch riguarda i pagamenti. E i pagamenti hanno date. Vogliamo essere in grado di manipolare questo attributo come una data. Se eseguiamo il reader senza la nostra logica TypeHandler, otterremo il seguente errore:

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'importComicJob' defined in class path resource [me/doismiu/fixedlength/job/ComicPrinterJob.class]: Unsatisfied dependency expressed through method 'importComicJob' parameter 1: Error creating bean with name 'printStep' defined in class path resource [me/doismiu/fixedlength/step/ComicPrinterStep.class]: Unsatisfied dependency expressed through method 'printStep' parameter 2: Error creating bean with name 'comicItemsReader' defined in class path resource [me/doismiu/fixedlength/step/ComicPrinterStep.class]: Failed to instantiate [me.doismiu.fixedlength.reader.ComicItemsReader]: Factory method 'comicItemsReader' threw exception with message: Invalid field 'publicationDate', in record 'comic', in stream 'comicStream': Type handler not found for type 'java.time.LocalDate'

In breve: BeanIO non fornisce un TypeHandler predefinito per LocalDate con il formato che vogliamo. Ecco perché creeremo una classe per risolvere questo.

package me.doismiu.fixedlength.handler;

import org.beanio.types.ConfigurableTypeHandler;
import org.beanio.types.TypeHandler;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Properties;

public class LocalDateTypeHandler implements ConfigurableTypeHandler {

    private final DateTimeFormatter formatter;

    public LocalDateTypeHandler(String pattern) {
        this.formatter = DateTimeFormatter.ofPattern(pattern);
    }

    @Override
    public TypeHandler newInstance(Properties properties) throws IllegalArgumentException {
        String format = properties.getProperty("format");
        if (format == null || format.isEmpty()) {
            return this;
        }
        return new LocalDateTypeHandler(format);
    }

    @Override
    public Object parse(String text) {
        if (text == null || text.trim().isEmpty()) {
            return null;
        }
        return LocalDate.parse(text, formatter);
    }

    @Override
    public String format(Object value) {
        if (value == null) {
            return null;
        }
        return ((LocalDate) value).format(formatter);
    }

    @Override
    public Class<?> getType() {
        return LocalDate.class;
    }
}

In pratica, stiamo registrando il nostro tipo di formato personalizzato, semplice così. Ora abbiamo finalmente creato il nostro LocalDateTypeHandler e collegato al nostro Reader. È così facile.

Per il nostro Writer, come specificato, l’ambito di questo progetto è solo mostrare l’output del file in console come JSON per dimostrare che stiamo utilizzando Oggetti reali. Qui, semplicemente implementare ItemWriter è sufficiente. Ma puoi fare quello che vuoi con i dati; dipende da te. Non mi soffermerò ulteriormente sul Writer.

package me.doismiu.fixedlength.writer;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import me.doismiu.fixedlength.model.Comic;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.infrastructure.item.Chunk;
import org.springframework.batch.infrastructure.item.ItemWriter;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class ComicPrinterWriter implements ItemWriter<Comic> {

    private static final Logger LOGGER = LoggerFactory.getLogger(ComicPrinterWriter.class);
    private final ObjectMapper objectMapper;

    public ComicPrinterWriter() {
        this.objectMapper = new ObjectMapper();
        this.objectMapper.registerModule(new JavaTimeModule());
        this.objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        this.objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
    }

    @Override
    public void write(Chunk<? extends Comic> chunk) throws Exception {
        LOGGER.info("--- Writing Batch of {} items as JSON ---", chunk.size());
        List<? extends Comic> items = chunk.getItems();
        String jsonOutput = objectMapper.writeValueAsString(items);
        System.out.println(jsonOutput);
    }
}

4. Il Job e lo Step mancanti per finalizzare il progetto

Con il Reader e il Writer creati, generiamo lo Step che orchestrerà entrambi. Niente di complesso qui, solo uno Step standard con un po’ di gestione degli errori e dove specifichiamo la Resource per il nostro file TXT. Se stai utilizzando questo esempio per la Produzione, la gestione dovrebbe essere più specifica, così come la logica per restituire correttamente il codice di uscita dell’applicazione. Ma per il nostro esempio, il codice qui sotto funziona:

package me.doismiu.fixedlength.step;

import me.doismiu.fixedlength.model.Comic;
import me.doismiu.fixedlength.reader.ComicItemsReader;
import me.doismiu.fixedlength.writer.ComicPrinterWriter;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.Step;
import org.springframework.batch.core.step.builder.StepBuilder;
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 org.springframework.transaction.PlatformTransactionManager;

@Configuration
public class ComicPrinterStep {

    @Value("${input.file.path:data/comics.txt}")
    private String inputFilePath;

    @Bean
    public ComicItemsReader comicItemsReader() {
        return new ComicItemsReader(new FileSystemResource(inputFilePath));
    }

    @Bean
    public Step printStep(JobRepository jobRepository,
            PlatformTransactionManager transactionManager,
            ComicItemsReader reader,
            ComicPrinterWriter writer) {
        return new StepBuilder("comicPrinterStep", jobRepository)
                .<Comic, Comic>chunk(10)
                .transactionManager(transactionManager)
                .reader(reader)
                .writer(writer)
                .faultTolerant()
                .skipLimit(1)
                .skip(IllegalArgumentException.class)
                .build();
    }
}

E con ciò, abbiamo creato il nostro Job.

package me.doismiu.fixedlength.job;

import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository;
import org.springframework.batch.core.job.Job;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.job.parameters.RunIdIncrementer;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.Step;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.Isolation;

@Configuration
public class ComicPrinterJob {
    @Bean
    public Job importComicJob(JobRepository jobRepository, Step printStep) {
        return new JobBuilder("importComicJob", jobRepository)
                .incrementer(new RunIdIncrementer())
                .start(printStep)
                .build();
    }
}

E con ciò, abbiamo creato il nostro Job.Con ciò, abbiamo l’intera struttura impostata.

5. Testare ciò che deve essere testato

Ora siamo pronti per eseguire la nostra applicazione finale.

Applicazione in esecuzione

Possiamo osservare il JSON generato nella console e la connessione al database stabilita con successo. E poiché abbiamo creato una struttura di metadati per controllare l’esecuzione, possiamo andare direttamente al database per verificare i dettagli dell’esecuzione:

Tabelle del database

GRANDE SUCCESSO!

6. Conclusione

Con questo, ora abbiamo la logica di base per leggere file posizionali a larghezza fissa. Non è nulla di complesso, ma poiché questo blog mira a essere un backup delle mie conoscenze, almeno lascio una traccia qui per non dimenticarlo di nuovo.

Banner Gitlab

Alla prossima!

Ciao Antonella!