30
votes

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.

2
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-2169Michael Minella
@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
@hoserdude, sorry, I've been really busy these times... I'll try to accept your pull request as soon as I canGéraud
I updated the related Jira issue for this. I'll provide an answer once I confirm one outstanding question with the core Spring team.Michael Minella

2 Answers

52
votes

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.

0
votes

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;
}