Géraud Géraud - 5 months ago 274
Java Question

Spring-batch @BeforeStep does not work with @StepScope

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.

Answer

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.