35

I'm using Spring Batch version 2.2.4.RELEASE I tried to write a simple example with stateful ItemReader, ItemProcessor and ItemWriter beans.

public class StatefulItemReader implements ItemReader<String> {

    private List<String> list;

    @BeforeStep
    public void initializeState(StepExecution stepExecution) {
        this.list = new ArrayList<>();
    }

    @AfterStep
    public ExitStatus exploitState(StepExecution stepExecution) {
        System.out.println("******************************");
        System.out.println(" READING RESULTS : " + list.size());

        return stepExecution.getExitStatus();
    }

    @Override
    public String read() throws Exception {
        this.list.add("some stateful reading information");
        if (list.size() < 10) {
            return "value " + list.size();
        }
        return null;
    }
}

In my integration test, I'm declaring my beans in an inner static java config class like the one below:

@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
public class SingletonScopedTest {

    @Configuration
    @EnableBatchProcessing
    static class TestConfig {
        @Autowired
        private JobBuilderFactory jobBuilder;
        @Autowired
        private StepBuilderFactory stepBuilder;

        @Bean
        JobLauncherTestUtils jobLauncherTestUtils() {
            return new JobLauncherTestUtils();
        }

        @Bean
        public DataSource dataSource() {
            EmbeddedDatabaseBuilder embeddedDatabaseBuilder = new EmbeddedDatabaseBuilder();
            return embeddedDatabaseBuilder.addScript("classpath:org/springframework/batch/core/schema-drop-hsqldb.sql")
                    .addScript("classpath:org/springframework/batch/core/schema-hsqldb.sql")
                    .setType(EmbeddedDatabaseType.HSQL)
                    .build();
        }

        @Bean
        public Job jobUnderTest() {
            return jobBuilder.get("job-under-test")
                    .start(stepUnderTest())
                    .build();
        }

        @Bean
        public Step stepUnderTest() {
            return stepBuilder.get("step-under-test")
                    .<String, String>chunk(1)
                    .reader(reader())
                    .processor(processor())
                    .writer(writer())
                    .build();
        }

        @Bean
        public ItemReader<String> reader() {
            return new StatefulItemReader();
        }

        @Bean
        public ItemProcessor<String, String> processor() {
            return new StatefulItemProcessor();
        }

        @Bean
        public ItemWriter<String> writer() {
            return new StatefulItemWriter();
        }
    }

    @Autowired
    JobLauncherTestUtils jobLauncherTestUtils;

    @Test
    public void testStepExecution() {
        JobExecution jobExecution = jobLauncherTestUtils.launchStep("step-under-test");

        assertEquals(ExitStatus.COMPLETED, jobExecution.getExitStatus());
    }
}

This test passes.

But as soon as I define my StatefulItemReader as a step scoped bean (which is better for a stateful reader), the "before step" code is no longer executed.

...
    @Bean
    @StepScope
    public ItemReader<String> reader() {
        return new StatefulItemReader();
    }
...

And I notice the same issue with processor and my writer beans.

What's wrong with my code? Is it related to this resolved issue: https://jira.springsource.org/browse/BATCH-1230

My whole Maven project with several JUnit tests can be found on GitHub: https://github.com/galak75/spring-batch-step-scope

Thank you in advance for your answers.

4
  • This may be a bug. It seems that the proxied version of the reader/processor/writer isn't being registered as a StepListener automatically. I've created an issue in Jira to track work on it: jira.springsource.org/browse/BATCH-2169 Jan 21, 2014 at 15:23
  • @Géraud, I've added another test that exercises the CompositeItemProcessor scenario in the Singleton scope and it fails as well. I have sent you a pull request in GitHub with the change. I will add or amend a Jira for this.
    – hoserdude
    Feb 12, 2014 at 0:00
  • @hoserdude, sorry, I've been really busy these times... I'll try to accept your pull request as soon as I can
    – Géraud
    Feb 17, 2014 at 20:43
  • I updated the related Jira issue for this. I'll provide an answer once I confirm one outstanding question with the core Spring team. Feb 20, 2014 at 20:57

2 Answers 2

65

When you configure a bean as follows:

@Bean
@StepScope
public MyInterface myBean() {
    return new MyInterfaceImpl();
}

You are telling Spring to use the proxy mode ScopedProxyMode.TARGET_CLASS. However, by returning the MyInterface, instead of the MyInterfaceImpl, the proxy only has visibility into the methods on the MyInterface. This prevents Spring Batch from being able to find the methods on MyInterfaceImpl that have been annotated with the listener annotations like @BeforeStep. The correct way to configure this is to return MyInterfaceImpl on your configuration method like below:

@Bean
@StepScope
public MyInterfaceImpl myBean() {
    return new MyInterfaceImpl();
}

We have added a warning log message on startup that points out, as we look for the annotated listener methods, if the object is proxied and the target is an interface, we won't be able to find methods on the implementing class with annotations on them.

5
  • Thank you for your help Michael! BTW, This is what I was about to test!!! (We had problems with late binding values that we solved the same way). And it made me think about the way beans should be declared in a Java Config file: usually, it's a good practice to use interfaces in declaration statements; but a java config is a special class, in which we choose the implementation to use. So it may be a good practice to declare the actual returned implementation (BTW when using XML config, we only give the actual implementation class...) Do you agree?
    – Géraud
    Feb 21, 2014 at 23:07
  • 1
    I do agree that returning classes instead of interfaces can make sense in java config. I won't say it should be a hard rule, but unless you have a good grasp on how the proxying works under the hood, it will save you some headaches. I did add a warning message on startup for the scenario you were coming across to prevent future users from having the same issue. I hope that helps! Feb 22, 2014 at 5:36
  • Right. Thank you again for your help! And I saw the warning when using spring-batch release 2.2.5
    – Géraud
    Feb 24, 2014 at 21:03
  • 1
    I have just been struggling with a catch-22 with spring 3.2.13 that this post clarified for me. My reader classes would not work unless they used ScopedProxyMode.INTERFACES (possibly because I use generics?), but as interfaces the @BeforeStep annotations were not visible tot he annotation processor. I worked around the symptom by making my classes StepExecutionListeners.
    – pojo-guy
    Jan 29, 2015 at 14:39
  • Saved my day. Still relevant in 2023 with newest Spring Batch
    – ahgpoug
    Feb 15, 2023 at 22:34
1

as suggested by pojo-guy Solution is to implement StepExecutionListener and Override beforeStep method to set stepExecution

@Override
public void beforeStep(StepExecution stepExecution) {
    this.stepExecution = stepExecution;
}
1
  • This is due to an undocumented fact: the wiring is made in the SimpleStepBuilder.listener(Object listener) method that generically searches for methods annoatated with the annotation @BeforeRead (and several others: AfterRead, BeforeProcess, AfterProcess, BeforeWrite, AfterWrite, OnReadError, OnProcessError, OnWriteError), but there are overloaded methods for Item*Listener (Reader, Writer, Process) that don't do that wiring, on top of that FaultTolerantStepBuilder does a diferent wiring searching for other annotations like OnSkipInRead etc. Feb 1, 2023 at 9:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.