Search code examples
javaspring-bootspring-batch

The writer invoked when using Spring Batch is all the same


I am using Spring Batch for the first time, and originally I just timed a task that fetches data from a database and writes it to an Excel via ExcelWriter after some processing, and that worked.

However, when I try to realize: each time the result of the timed task processing is corresponding to the generation of an Excel this does not work. When I printed the logs, I noticed that the ExcelWriter used by the timed tasks seemed to be the same, even though I recreated the ExcelWriter instance once for each timed task.

Finally, my question is: how to solve this problem: always calling the same ExcelWriter.

This is my core code:

Let me know if any core code is missing, here are all the file names:

BatchConfig.java

BatchJobCompletionListener.java

BatchScheduler.java

CustomSkipListener.java

ExcelWriter.java

JobParametersHolder.java

Transaction.java

TransactionProcessor.java

TransactionQueryConfig.java

TransactionRowMapper.java

@Configuration
@EnableBatchProcessing
public class BatchConfig {
    private final DataSource dataSource;
    private final JobBuilderFactory jobBuilderFactory;
    private final StepBuilderFactory stepBuilderFactory;

    public BatchConfig(DataSource dataSource, JobBuilderFactory jobBuilderFactory, StepBuilderFactory stepBuilderFactory) {
        this.dataSource = dataSource;
        this.jobBuilderFactory = jobBuilderFactory;
        this.stepBuilderFactory = stepBuilderFactory;
    }

    @Bean
    public Job transactionExportJob(Step exportStep, BatchJobCompletionListener listener) {
        return jobBuilderFactory.get("transactionExportJob")
                .incrementer(new RunIdIncrementer())
                .start(exportStep)
                .listener(listener)
                .build();
    }

    @Bean
    public Step exportStep(ItemReader<Transaction> reader, ExcelWriter excelWriter, PlatformTransactionManager transactionManager) {
        return stepBuilderFactory.get("exportStep")
                .<Transaction, Transaction>chunk(5000)
                .reader(reader)
                .processor(new TransactionProcessor()) // handle data
                .writer(excelWriter)
                .faultTolerant() // enabling fault tolerance
                .retryLimit(3) 
                .retry(Exception.class)
                .skip(Exception.class)
                .skipLimit(100)
                .listener(new CustomSkipListener()) // listing skip
                .transactionManager(transactionManager)
                .build();
    }

    @Bean
    public JdbcPagingItemReader<Transaction> reader(PagingQueryProvider transactionQueryProvider) {
        JdbcPagingItemReader<Transaction> reader = new JdbcPagingItemReader<>();
        reader.setDataSource(dataSource);
        reader.setFetchSize(5000);
        reader.setRowMapper(new TransactionRowMapper());
        reader.setQueryProvider(transactionQueryProvider);

        return reader;
    }
}


@Component
public class ExcelWriter implements ItemWriter<Transaction> {
    private final String filePath;
    private final Workbook workbook;
    private final Sheet sheet;
    private int rowCount = 1;

    public ExcelWriter() {
        this.filePath = "transactions_" + UUID.randomUUID().toString() + ".xlsx";
        this.workbook = new XSSFWorkbook();
        this.sheet = workbook.createSheet("Transactions");
        createHeaderRow();
    }

    @Override
    public void write(List<? extends Transaction> items) throws Exception {
        System.out.println("filePath: " + filePath);
        for (Transaction transaction : items) {
            Row row = sheet.createRow(rowCount++);
            row.createCell(0).setCellValue(transaction.getUniqueTxId());
            row.createCell(1).setCellValue(transaction.getCreatedTime().toString());
        }
        try (FileOutputStream out = new FileOutputStream(filePath)) {
            workbook.write(out);
        } catch (IOException e) {
            System.err.println("Error closing Excel file: " + e.getMessage());
            throw new IOException("Failed to close workbook", e);
        }
    }

    private void createHeaderRow() {
        Row header = sheet.createRow(0);
        header.createCell(0).setCellValue("Unique Tx Id");
        header.createCell(1).setCellValue("Created Time");
    }

    public void closeWorkbook() throws IOException {
        workbook.close();
    }
}


@Component
@EnableScheduling
public class BatchScheduler {
    private final JobLauncher jobLauncher;
    private final Job transactionExportJob;
    private final JobParametersHolder jobParametersHolder;


    public BatchScheduler(JobLauncher jobLauncher, Job transactionExportJob, JobParametersHolder jobParametersHolder) {
        this.jobLauncher = jobLauncher;
        this.transactionExportJob = transactionExportJob;
        this.jobParametersHolder = jobParametersHolder;
    }

    @Scheduled(fixedRate = 60000, initialDelay = 60000, zone = "Asia/Shanghai")
    public void runBatchJob() {
        try {
            ExcelWriter excelWriter = new ExcelWriter();
            LocalDateTime startTime = LocalDateTime.now().minusDays(1).withHour(22);
            LocalDateTime endTime = LocalDateTime.now().withHour(22);

            JobParameters jobParameters = new JobParametersBuilder()
                    .addDate("startTime", Date.from(startTime.atZone(ZoneId.of("Asia/Shanghai")).toInstant()))
                    .addDate("endTime", Date.from(endTime.atZone(ZoneId.of("Asia/Shanghai")).toInstant()))
                    .toJobParameters();

            System.out.println("jobParameters: " + jobParameters);
            jobParametersHolder.setJobParameters(jobParameters);

            System.out.println("Batch Job started");
            JobExecution execution = jobLauncher.run(transactionExportJob, jobParameters);
            System.out.println("Batch job completed with status: {}" + execution.getStatus());
            excelWriter.closeWorkbook();
        } catch (Exception e) {
            System.out.println("Batch job execution failed: {}" + e.getMessage() + e);
        }
    }
}

----- Edit Content -----

Thanks to @lance-java, after I removed the @Component annotation from ExcelWriter, using the @JobScope annotation to identify ExcelWriter would cause a corresponding ExcelWriter instance to be recreated each time the Job is run, thus achieving what I was trying to achieve.


Solution

  • The @Component annotation on ExcelWriter defines a bean in spring's application context

    @Component
    public class ExcelWriter implements ItemWriter<Transaction> {
       ...
    }
    

    You inject the ExcelWriter instance (created above) into the "exportStep" bean

    @Configuration
    @EnableBatchProcessing
    public class BatchConfig {
        ...
        @Bean
        public Step exportStep(..., ExcelWriter excelWriter, ...) {
           ...
        }
        ...
    }
    

    The ExcelWriter instance created in BatchScheduler.runBatchJob() is irrelevant, this is not used anywhere

    @Component
    @EnableScheduling
    public class BatchScheduler {
        @Scheduled(fixedRate = 60000, initialDelay = 60000, zone = "Asia/Shanghai")
        public void runBatchJob() {
            try {
                ExcelWriter excelWriter = new ExcelWriter();
                ...
                excelWriter.closeWorkbook();
            }
            ...
        }
    }