1
votes

I'm currently in the midst of a real pain of a problem that I can't seem to figure out. I'm hoping someone on here can provide some guidance. It all revolves around StepScope, bean autowiring, and bean creation.

Configuration One (Autowiring Step Scope Beans):

Config.java

(I've reduced my configuration class for simplicity.)

@Configuration
@EnableBatchProcessing
public class Config {
        @Bean
        @StepScope
        public StepScopeBeanOne stepScopeBeanOne() {
            return new StepScopeBeanOne();
        }
    
    
        @Bean
        @StepScope
        public StepScopeBeanTwo stepScopeBeanTwo () {
            return new StepScopeBeanTwo();
        }
    
        @Bean
        @StepScope
        public Processor processor() {
            return new Processor();
        }
}

Processor.java

public class Processor implements ItemProcessor<Incoming, Outgoing> {

    //The two injected step scope beans...
    @Autowired
    private StepScopeBeanOne stepScopeBeanOne;

    @Autowired
    private StepScopeBeanTwo stepScopeBeanTwo;

    @Override
    public Outgoing process(Incoming incoming) {
       //do process...
    }

    public void setStepScopeBeanTwo(StepScopeBeanTwo stepScopeBeanTwo) {
        this.stepScopeBeanTwo = stepScopeBeanTwo;
    }

    public void setStepScopeBeanOne(StepScopeBeanOne stepScopeBeanOne) {
        this.stepScopeBeanOne = stepScopeBeanOne;
    }

    //rest of class...
}

Result

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'scopedTarget.scopedTarget.Processor': Unsatisfied dependency expressed through field 'stepScopeBeanOne'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'StepScopeBeanOne' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

My Thoughts

I believe it has something to do with the bean StepScopeBeanOne not being in the ApplicationContext by the time the Processor bean is being resolved.

Configuration Two (Setter injection of Step Scope Beans)

Config.java

@Configuration
@EnableBatchProcessing
public class Config {
        @Bean
        @StepScope
        public StepScopeBeanOne stepScopeBeanOne() {
            return new StepScopeBeanOne();
        }
    
    
        @Bean
        @StepScope
        public StepScopeBeanTwo stepScopeBeanTwo () {
            return new StepScopeBeanTwo();
        }
    
        @Bean
        @StepScope
        public Processor processor() {
            Processor processor = new Processor();
            processor.setStepScopeBeanOne(stepScopeBeanOne());
            processor.setStepScopeBeanTwo(stepScopeBeanTwo());
            return processor;
        }
}

Processor is the same as Configuration One

Result

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'scopedTarget.scopedTarget.Processor' defined in class path resource [Config.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [Processor]: Factory method 'processor' threw exception; nested exception is java.lang.IllegalStateException: @Bean method Config.stepScopeBeanOne called as bean reference for type [StepScopeBeanOne] but overridden by non-compatible bean instance of type [com.sun.proxy.$Proxy149]. Overriding bean of same name declared in: BeanDefinition defined in class path resource [Config.class]

My Thoughts

I've ran into this issue before, and my workaround was to create an interface for the classes I was injecting.

public interface StepScopeBeanOne {
    //methods
}

public class StepScopeBeanOneImpl implements StepScopeBeanOne {
   //methods
}

I'd prefer to not go that route again, because I believe it adds complexity to the code. If anyone has a different solution please share.

Configuration Three (The one that works)

Removing Step Scope from each of these beans results in a fine and dandy autowiring.

Config.class

@Configuration
@EnableBatchProcessing
public class Config {
        @Bean
        public StepScopeBeanOne stepScopeBeanOne() {
            return new StepScopeBeanOne();
        }
    
    
        @Bean
        public StepScopeBeanTwo stepScopeBeanTwo () {
            return new StepScopeBeanTwo();
        }
    
        @Bean
        public Processor processor() {
            Processor processor = new Processor();
            return processor;
        }
}

Processor is the same

Results

Beans are injected via @Autowiring annotation.

My Thoughts

You may be asking yourself, why don't you just remove @StepScope? Well, I can't. I need step scope on each of these beans to allow for JobParameters to be injected correctly. We use the step name in our job parameters to provide fine tuned step configuration.

Overall

My goal is to get Configuration One working. Outside of that goal, a better understanding of bean creation in Spring would be much appreciated.

1
You didn't share a complete minimal example that reproduces the issue, but I tried on my end and was not able to reproduce the problem based on what you shared. I added an answer with an example. Hope it helps. - Mahmoud Ben Hassine

1 Answers

0
votes

My goal is to get Configuration One working.

Here is a complete example based on "Configuration One":

import java.util.Arrays;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
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.configuration.annotation.StepScope;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.support.ListItemReader;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableBatchProcessing
public class MyJobConfiguration {

    @Bean
    @StepScope
    public StepScopeBeanOne stepScopeBeanOne() {
        return new StepScopeBeanOne();
    }

    @Bean
    @StepScope
    public StepScopeBeanTwo stepScopeBeanTwo () {
        return new StepScopeBeanTwo();
    }

    @Bean
    @StepScope
    public Processor processor() {
        return new Processor();
    }

    @Bean
    public Job job(JobBuilderFactory jobs, StepBuilderFactory steps) {
        return jobs.get("job")
                .start(steps.get("step")
                        .<Integer, Integer>chunk(5)
                        .reader(new ListItemReader<>(Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)))
                        .processor(processor())
                        .writer(items -> items.forEach(System.out::println))
                        .build())
                .build();
    }

    public static void main(String[] args) throws Exception {
        ApplicationContext context = new AnnotationConfigApplicationContext(MyJobConfiguration.class);
        JobLauncher jobLauncher = context.getBean(JobLauncher.class);
        Job job = context.getBean(Job.class);
        jobLauncher.run(job, new JobParameters());
    }
    
    static class StepScopeBeanOne {
        
    }

    static class StepScopeBeanTwo {

    }

    static class Processor implements ItemProcessor<Integer, Integer> {

        @Autowired
        private StepScopeBeanOne stepScopeBeanOne;

        @Autowired
        private StepScopeBeanTwo stepScopeBeanTwo;
        
        @Override
        public Integer process(Integer item) throws Exception {
            return item + 1;
        }

        public void setStepScopeBeanOne(StepScopeBeanOne stepScopeBeanOne) {
            this.stepScopeBeanOne = stepScopeBeanOne;
        }

        public void setStepScopeBeanTwo(StepScopeBeanTwo stepScopeBeanTwo) {
            this.stepScopeBeanTwo = stepScopeBeanTwo;
        }
    }
}

This prints the following output without any issue regarding step-scoped beans or dependency injection:

[main] WARN org.springframework.batch.core.configuration.annotation.DefaultBatchConfigurer - No datasource was provided...using a Map based JobRepository
[main] WARN org.springframework.batch.core.configuration.annotation.DefaultBatchConfigurer - No transaction manager was provided, using a ResourcelessTransactionManager
[main] INFO org.springframework.batch.core.launch.support.SimpleJobLauncher - No TaskExecutor has been set, defaulting to synchronous executor.
[main] INFO org.springframework.batch.core.launch.support.SimpleJobLauncher - Job: [SimpleJob: [name=job]] launched with the following parameters: [{}]
[main] INFO org.springframework.batch.core.job.SimpleStepHandler - Executing step: [step]
1
2
3
4
5
6
7
8
9
10
[main] INFO org.springframework.batch.core.step.AbstractStep - Step: [step] executed in 73ms
[main] INFO org.springframework.batch.core.launch.support.SimpleJobLauncher - Job: [SimpleJob: [name=job]] completed with the following parameters: [{}] and the following status: [COMPLETED] in 119ms

@StepScope is equivalent to @Scope(value = "step", proxyMode = ScopedProxyMode.TARGET_CLASS) (See javadoc), so by default, step-scoped beans are class-based proxies (using cglib) and are not required to implement an interface.