Search code examples
javaspringspring-bootspring-batch

spring batch dependency cycle


i'm testing spring batch and i'm getting dependency cycle. I couldn't understand what is the issue here.

package org.id.demo.test.dao;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.core.step.tasklet.TaskletStep;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.LineMapper;
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.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.transaction.PlatformTransactionManager;


@Configuration
@EnableBatchProcessing
public class SpringBatchConfig {

    @Autowired
    private JobRepository jobRepository;
    @Autowired
    private PlatformTransactionManager platformTransactionManager;
    @Autowired
    private ItemWriter<BankTransaction> bankTransactionItemWriter;
    @Autowired
    private ItemProcessor<BankTransaction, BankTransaction> bankTransactionItemProcessor;
    @Autowired
    private ItemReader<BankTransaction> flatFileItemReader;


    @Bean
    public Job bankJob() {
        TaskletStep step1 = new StepBuilder("step-load-data", jobRepository)
                .<BankTransaction, BankTransaction>chunk(100, platformTransactionManager)
                .reader(flatFileItemReader)
                .processor(bankTransactionItemProcessor)
                .writer(bankTransactionItemWriter)
                .build();

        return new JobBuilder("ETL-job", jobRepository)
                .start(step1)
                .build();
    }

    @Bean
    public FlatFileItemReader<BankTransaction> flatFileItemReader(@Value("${inputFile}") Resource inputFile) {
        FlatFileItemReader<BankTransaction> fileItemReader = new FlatFileItemReader<BankTransaction>();
        fileItemReader.setName("just a reader name");
        fileItemReader.setLinesToSkip(1);
        fileItemReader.setResource(inputFile);
        fileItemReader.setLineMapper(lineMapper());

        return fileItemReader;


    }

    @Bean
    public LineMapper<BankTransaction> lineMapper() {
        DefaultLineMapper<BankTransaction> lineMapper = new DefaultLineMapper<>();

        DelimitedLineTokenizer lineTokenizer = new DelimitedLineTokenizer();
        lineTokenizer.setDelimiter(DelimitedLineTokenizer.DELIMITER_COMMA);
        lineTokenizer.setStrict(false);
        lineTokenizer.setNames("id", "accountID", "strTransactionDate", "transactionType", "amount");

        lineMapper.setLineTokenizer(lineTokenizer);

        BeanWrapperFieldSetMapper<BankTransaction> fieldSetMapper = new BeanWrapperFieldSetMapper<>();
        fieldSetMapper.setTargetType(BankTransaction.class);
        lineMapper.setFieldSetMapper(fieldSetMapper);
        return lineMapper;
    }

}

I'm always getting this error message:

The dependencies of some of the beans in the application context form a cycle:

   jobRestController (field private org.springframework.batch.core.Job org.id.demo.test.dao.JobRestController.job)
┌─────┐
|  springBatchConfig (field private org.springframework.batch.item.ItemReader org.id.demo.test.dao.SpringBatchConfig.flatFileItemReader)
└─────┘

Note: if i move the ItemReader to another class everything works fine.

I want to undertand what is the issue and how to fix it without put reader in another class.


Solution

  • The reason behind circular dependencies:

    • Spring tries to create SpringBatchConfig first because it's marked as @Configuration.

    • But SpringBatchConfig has @Autowired private ItemReader flatFileItemReader;

    • Spring now looks for a @Bean that provides flatFileItemReader and finds it inside the same class (SpringBatchConfig).

    • But the problem is SpringBatchConfig isn't fully initialized yet, so Spring can't create the flatFileItemReader bean.

    • This creates a circular dependency—Spring can't finish SpringBatchConfig without flatFileItemReader, but flatFileItemReader is inside SpringBatchConfig.

    There are couple of ways to fix this:

    Option 1: Instead of using @Autowired on ItemReader flatFileItemReader, let spring provides it when calling bankJob():

    @Bean
    public Job bankJob(FlatFileItemReader<BankTransaction> flatFileItemReader) { // Inject here
            TaskletStep step1 = new StepBuilder("step-load-data", jobRepository)
                    .<BankTransaction, BankTransaction>chunk(100, platformTransactionManager)
                    .reader(flatFileItemReader)
                    .processor(bankTransactionItemProcessor)
                    .writer(bankTransactionItemWriter)
                    .build();
        
            return new JobBuilder("ETL-job", jobRepository)
                    .start(step1)
                    .build();
        }
    

    When you pass flatFileItemReader as a method parameter in bankJob(), Spring creates it first before injecting it.

    This guarantees that the reader exists before it is used in the bankJob.

    If you still want to use @Autowired on ItemReader flatFileItemReader then

    Option 2: Use @Lazy for delay Injection

    @Autowired
    @Lazy
    private ItemReader<BankTransaction> flatFileItemReader;
    

    This tells Spring to delay initializing flatFileItemReader until it's actually needed