Search code examples
spring-bootspring-batchjunit5

Question about retrieving JobParameters and retrieving variables from StepExecutionContext in Spring batch 5


I am trying to test my writer and reader that I created with Springbatch 5.0. I noticed that when I add @Value("#{jobParameters['filename']}" to my writer or even my processor, I get this error message, "No Scope registered for scope name 'step'". However, if I removed it, it works. Not sure this has anything to do with Springbatch 5 changes so bringing it here for help.

Here is what I have:

application-test.properties

outputLocation=src/test/resources/output
inputLocation=src/test/resources/input
inputFileName=employee.txt
outputFileName=employees.csv

EmployeeRecord.java

@Getter
@Setter
@NoArgsConstructor
public class EmployeeRecord implements Serializable {
    private Long id;
    private String employeeNumber;
    private String firstName;
    private String lastName;
    private String role;
    private String salary;
    private LocalDateTime joiningDate;
    private String fileName;

    @Override
    public String toString() {
        return new StringJoiner(", ", EmployeeRecord.class.getSimpleName() + "[", "]")
                .add("id=" + id)
                .add("employeeNumber='" + employeeNumber + "'")
                .add("firstName='" + firstName + "'")
                .add("lastName='" + lastName + "'")
                .add("role='" + role + "'")
                .add("salary='" + salary + "'")
                .add("joiningDate=" + joiningDate + "'")
                .add("fileName=" + fileName)
                .toString();
    }

}

Employee.java

@Getter
@Setter
@NoArgsConstructor
public class Employee implements Serializable {
    private Long id;
    private String employeeNumber;
    private String firstName;
    private String lastName;
    private String role;
    private String salary;
    private LocalDateTime joiningDate;

    @Override
    public String toString() {
        return new StringJoiner(", ", Employee.class.getSimpleName() + "[", "]")
                .add("id=" + id)
                .add("employeeNumber='" + employeeNumber + "'")
                .add("firstName='" + firstName + "'")
                .add("lastName='" + lastName + "'")
                .add("role='" + role + "'")
                .add("salary='" + salary + "'")
                .add("joiningDate=" + joiningDate)
                .toString();
    }

}

ReaderWriter.java

@Slf4j
@Configuration
public class ReaderWriter {
    @Value("${inputLocation}")
    private String inputLocation;
    @Value("${outputLocation}")
    private String outputLocation;
    @Value("${inputFileName}")
    private String inputFileName;
    @Value("${outputFileName}")
    private String outputFileName;
    @Autowired
    private FieldSetMapper<Employee> employeeFieldSetMapper;

    @Bean
    @StepScope
    public FlatFileItemReader<Employee> flatFileItemReader() {
        log.info("reading from -> {} ", inputLocation);
        log.debug("reading from -> {} ", inputLocation);
        return new FlatFileItemReaderBuilder<Employee>()
                .name("employeeItemReader")
                .resource(new FileSystemResource(inputLocation+"/"+inputFileName))
                .linesToSkip(1)
                .fieldSetMapper(employeeFieldSetMapper)
                .lineMapper(lineMapper())
                .build();
    }

    @Bean
    @StepScope
    public FlatFileItemWriter<EmployeeRecord> flatFileItemWriter(@Value("#{jobParameters['filename']}") String fileName) throws MalformedURLException {
        log.info("writing to -> {} ", outputLocation);
        String file = outputLocation+"/"+outputFileName;
        return new FlatFileItemWriterBuilder<EmployeeRecord>()
                .name("employeeItemWriter")
                .resource(new FileSystemResource(file))
                .lineAggregator(lineAggregator())
                .headerCallback(header -> header.write(fileName))
                .build();
    }

    private DefaultLineMapper<Employee> lineMapper() {
        DefaultLineMapper<Employee> lineMapper = new DefaultLineMapper<>();
        lineMapper.setLineTokenizer(tokenizer());
        lineMapper.setFieldSetMapper(employeeFieldSetMapper);
        return lineMapper;
    }

    private LineTokenizer tokenizer() {
        DelimitedLineTokenizer delimitedLineTokenizer = new DelimitedLineTokenizer();
        delimitedLineTokenizer.setDelimiter("|");
        delimitedLineTokenizer.setNames("id", "employeeNumber", "firstName", "lastName", "role", "salary", "joiningDate");
        return delimitedLineTokenizer;
    }

    private LineAggregator<EmployeeRecord> lineAggregator() {
        DelimitedLineAggregator<EmployeeRecord> delimitedLineAggregator = new DelimitedLineAggregator<>();
        delimitedLineAggregator.setDelimiter(",");
        delimitedLineAggregator.setFieldExtractor(extractor());
        return delimitedLineAggregator;
    }

    private FieldExtractor<EmployeeRecord> extractor() {
        BeanWrapperFieldExtractor<EmployeeRecord> beanWrapperFieldExtractor = new BeanWrapperFieldExtractor<>();
        beanWrapperFieldExtractor.setNames(new String[]{
                "id",
                "employeeNumber",
                "firstName",
                "lastName",
                "role",
                "salary",
                "joiningDate",
                "fileName"
        });
        beanWrapperFieldExtractor.afterPropertiesSet();
        return beanWrapperFieldExtractor;
    }

}

EmployeeProcessor.java

@StepScope
@Slf4j
@Component("employeeProcessor")
public class EmployeeProcessor implements ItemProcessor<Employee, EmployeeRecord> {
    @Value("#{jobParameters['filename']}")
    private String fileName;

    @Override
    public EmployeeRecord process(Employee employee) throws Exception {
        Long id = employee.getId();
        String employeeNumber = employee.getEmployeeNumber();
        String firstName = employee.getFirstName();
        String lastName = employee.getLastName();
        String role = employee.getRole();
        String salary = employee.getSalary();
        LocalDateTime joiningDate = employee.getJoiningDate();

        EmployeeRecord employeeProcessed = new EmployeeRecord();
        employeeProcessed.setId(id);
        employeeProcessed.setFirstName(firstName);
        employeeProcessed.setLastName(lastName);
        employeeProcessed.setEmployeeNumber(employeeNumber);
        employeeProcessed.setSalary(salary);
        employeeProcessed.setJoiningDate(joiningDate);
        employeeProcessed.setFileName(fileName);
        employeeProcessed.setRole(role);
        return employeeProcessed;
    }
}

EmployeeStepListener.java

@Slf4j
@Component("employeeStepListener")
public class EmployeeStepListener implements StepExecutionListener {
    @Override
    public void beforeStep(StepExecution stepExecution) {
        log.info("Called beforeStep -> {} ", stepExecution.getJobParameters().getString("filename"));
    }

    @Override
    public ExitStatus afterStep(StepExecution stepExecution) {
        log.info("Called afterStep");
        return stepExecution.getExitStatus();
    }
}

BatchConfig.java

@Slf4j
@Configuration
public class BatchConfig {

    @Bean
    public Job employeeJob(JobRepository jobRepository,@Qualifier("employeeStep") Step employeeStep,@Qualifier("employeeJobListener") JobExecutionListener listener) {
        log.debug("inside employeeJob");
        return new JobBuilder("employeeJob", jobRepository)
                .incrementer(new RunIdIncrementer())
                .listener(listener)
                .start(employeeStep)
                .build();
    }

}

EmployeeFieldSetMapper.java

@Component("employeeFieldSetMapper")
public class EmployeeFieldSetMapper implements FieldSetMapper<Employee> {

    @Override
    public Employee mapFieldSet(FieldSet fieldSet) {
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("dd-MM-yyyy:HH:mm:ss");
        Employee employee = new Employee();
        employee.setId(fieldSet.readLong("id"));
        employee.setFirstName(fieldSet.readString("firstName"));
        employee.setLastName(fieldSet.readString("lastName"));
        employee.setRole(fieldSet.readString("role"));
        employee.setSalary(fieldSet.readString("salary"));
        String dateTime = fieldSet.readString("joiningDate");
        employee.setJoiningDate(LocalDateTime.parse(dateTime,dateTimeFormatter));

        return employee;
    }
}

EmployeeCommonConfig.java

@Configuration
public class EmployeeCommonConfig {
    @Qualifier("batchDataSource")
    @Autowired
    private DataSource dataSource;

    @Bean
    protected JobRepository jobRepository() throws Exception {
        JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean();
        factory.setDataSource(dataSource);
        factory.setTransactionManager(new DataSourceTransactionManager(dataSource));
        factory.setIncrementerFactory(new DefaultDataFieldMaxValueIncrementerFactory(dataSource));
        factory.setIsolationLevelForCreateEnum(Isolation.READ_COMMITTED);
        factory.setTablePrefix("BATCH_");
        factory.afterPropertiesSet();

        return factory.getObject();
    }

    @Bean
    protected JobLauncher jobLauncher() throws Exception {
        TaskExecutorJobLauncher launcher = new TaskExecutorJobLauncher();
        launcher.setJobRepository(jobRepository());
        launcher.setTaskExecutor(new SyncTaskExecutor());
        launcher.afterPropertiesSet();
        return launcher;
    }
}

StepConfig.java

@Slf4j
@Configuration
public class StepConfig {
    @Value("${inputLocation}")
    private String inputLocation;
    @Value("${outputLocation}")
    private String outputLocation;
    @Value("${inputFileName}")
    private String inputFileName;
    @Value("${outputFileName}")
    private String outputFileName;
    @Autowired
    private ItemReader<Employee> flatFileItemReader;
    @Autowired
    private ItemWriter<EmployeeRecord> flatFileItemWriter;
    @Autowired
    private EmployeeProcessor employeeProcessor;


    @Bean
    public Step employeeStep(PlatformTransactionManager transactionManager, JobRepository jobRepository) {
        log.debug("debug: in employeeStep");
        log.info("info: in employeeStep");
        log.debug("inputLocation -> {} ", inputLocation);
        log.debug("outputLocation -> {} ", outputLocation);
        return new StepBuilder("employeeStep",jobRepository)
                .<Employee,EmployeeRecord>chunk(2,transactionManager)
                .reader(flatFileItemReader)
                .processor(employeeProcessor)
                .writer(flatFileItemWriter)
                .listener("employeeStepListener")
                .build();
    }

}

DbConfig.java

@Configuration
public class DbConfig {

    @ConfigurationProperties(prefix = "spring.datasource")
    @Bean("batchDataSource")
    public HikariDataSource dataSource() {
        return DataSourceBuilder
                .create()
                .type(HikariDataSource.class)
                .build();
    }

}

DatabaseTestConfig.java

@Configuration
public class DatabaseTestConfig {

    @Bean(name = "batchDataSource")
    public DataSource batchDataSource() {
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .build();
    }
    @Bean(name = "batchTransactionManager")
    public DataSourceTransactionManager batchTransactionManager() {
        return new DataSourceTransactionManager(batchDataSource());
    }
}

EmployeeTestConfig.java

@Configuration
@ComponentScan(basePackages = {"com.test"},
        excludeFilters = {@ComponentScan .Filter (type = FilterType.ASSIGNABLE_TYPE, classes = {BatchConfig.class})})
public class EmployeeTestConfig {
    @Autowired
    private JobRepository jobRepository;

    @Bean
    public Job employeeJob(@Qualifier("employeeStep") Step employeeStep) {
        return new JobBuilder("employeeJob", jobRepository)
                .incrementer(new RunIdIncrementer())
                .start(employeeStep)
                .build();
    }
}

EmployeeExecutionTest.java

@SpringBatchTest
@SpringJUnitConfig({EmployeeTestConfig.class, DatabaseTestConfig.class})
@TestPropertySource(locations = "classpath:application-test.properties")
@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:data-h2.sql")
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
public class EmployeeExecutionTest {
    @Autowired
    private JobLauncherTestUtils jobLauncherTestUtils;
    @Test
    public void employeeJobExecutionTest() throws Exception {
        JobParameters jobParameter = new JobParametersBuilder()
                .addString("filename", "testname").toJobParameters();
        JobExecution jobExecution = jobLauncherTestUtils.launchJob(jobParameter);
        assertEquals(BatchStatus.COMPLETED, jobExecution.getStatus());

    }
}

Stacktrace:

2023.03.22 20:41:29.930 [INFO] org.springframework.batch.core.job.SimpleStepHandler: Executing step: [employeeStep] 2023.03.22 20:41:29.937 [ERROR] org.springframework.batch.core.step.AbstractStep: Encountered an error executing step employeeStep in job employeeJob java.lang.IllegalStateException: No Scope registered for scope name 'step' at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:359) ~[spring-beans-6.0.6.jar:6.0.6] at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[spring-beans-6.0.6.jar:6.0.6] at org.springframework.aop.target.SimpleBeanTargetSource.getTarget(SimpleBeanTargetSource.java:35) ~[spring-aop-6.0.6.jar:6.0.6] at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:686) ~[spring-aop-6.0.6.jar:6.0.6] at org.springframework.batch.item.file.FlatFileItemWriter$$SpringCGLIB$$0.open() ~[spring-batch-infrastructure-5.0.1.jar:5.0.1] at org.springframework.batch.item.support.CompositeItemStream.open(CompositeItemStream.java:124) ~[spring-batch-infrastructure-5.0.1.jar:5.0.1] at org.springframework.batch.core.step.tasklet.TaskletStep.open(TaskletStep.java:293) ~[spring-batch-core-5.0.1.jar:5.0.1] at org.springframework.batch.core.step.AbstractStep.execute(AbstractStep.java:224) ~[spring-batch-core-5.0.1.jar:5.0.1] at org.springframework.batch.core.job.SimpleStepHandler.handleStep(SimpleStepHandler.java:153) ~[spring-batch-core-5.0.1.jar:5.0.1] at org.springframework.batch.core.job.AbstractJob.handleStep(AbstractJob.java:417) ~[spring-batch-core-5.0.1.jar:5.0.1] at org.springframework.batch.core.job.SimpleJob.doExecute(SimpleJob.java:132) ~[spring-batch-core-5.0.1.jar:5.0.1] at org.springframework.batch.core.job.AbstractJob.execute(AbstractJob.java:316) ~[spring-batch-core-5.0.1.jar:5.0.1] at org.springframework.batch.core.launch.support.SimpleJobLauncher$1.run(SimpleJobLauncher.java:157) ~[spring-batch-core-5.0.1.jar:5.0.1] at org.springframework.core.task.SyncTaskExecutor.execute(SyncTaskExecutor.java:50) ~[spring-core-6.0.6.jar:6.0.6] at org.springframework.batch.core.launch.support.SimpleJobLauncher.run(SimpleJobLauncher.java:148) ~[spring-batch-core-5.0.1.jar:5.0.1] at org.springframework.batch.core.launch.support.TaskExecutorJobLauncher.run(TaskExecutorJobLauncher.java:70) ~[spring-batch-core-5.0.1.jar:5.0.1] at org.springframework.batch.test.JobLauncherTestUtils.launchJob(JobLauncherTestUtils.java:146) ~[spring-batch-test-5.0.1.jar:5.0.1] at com.codi.springbatch.springbatchapp.EmployeeExecutionTest.employeeJobExecutionTest(EmployeeExecutionTest.java:27) ~[test-classes/:?] at jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[?:?] at java.lang.reflect.Method.invoke(Method.java:578) ~[?:?] at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:727) ~[junit-platform-commons-1.9.2.jar:1.9.2] at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:147) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:86) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:217) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:213) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:138) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:68) ~[junit-jupiter-engine-5.9.2.jar:5.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2] at java.util.ArrayList.forEach(ArrayList.java:1511) ~[?:?] at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2] at java.util.ArrayList.forEach(ArrayList.java:1511) ~[?:?] at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54) ~[junit-platform-engine-1.9.2.jar:1.9.2] at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:220) ~[junit-platform-launcher-1.3.1.jar:1.3.1] at org.junit.platform.launcher.core.DefaultLauncher.lambda$execute$6(DefaultLauncher.java:188) ~[junit-platform-launcher-1.3.1.jar:1.3.1] at org.junit.platform.launcher.core.DefaultLauncher.withInterceptedStreams(DefaultLauncher.java:202) ~[junit-platform-launcher-1.3.1.jar:1.3.1] at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:181) ~[junit-platform-launcher-1.3.1.jar:1.3.1] at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:128) ~[junit-platform-launcher-1.3.1.jar:1.3.1] at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invokeAllTests(JUnitPlatformProvider.java:150) ~[surefire-junit-platform-2.22.2.jar:2.22.2] at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invoke(JUnitPlatformProvider.java:124) ~[surefire-junit-platform-2.22.2.jar:2.22.2] at org.apache.maven.surefire.booter.ForkedBooter.invokeProviderInSameClassLoader(ForkedBooter.java:384) ~[surefire-booter-2.22.2.jar:2.22.2] at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:345) ~[surefire-booter-2.22.2.jar:2.22.2] at org.apache.maven.surefire.booter.ForkedBooter.execute(ForkedBooter.java:126) ~[surefire-booter-2.22.2.jar:2.22.2] at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:418) ~[surefire-booter-2.22.2.jar:2.22.2] 2023.03.22 20:41:29.948 [INFO] org.springframework.batch.core.step.AbstractStep: Step: [employeeStep] executed in 16ms 2023.03.22 20:41:29.952 [ERROR] org.springframework.batch.core.step.AbstractStep: Exception while closing step execution resources in step employeeStep in job employeeJob java.lang.IllegalStateException: No Scope registered for scope name 'step'


Solution

  • This is not related to Spring Batch 5. I could not run your sample and reproduce the issue, but the error means that a step-scoped component is invoked outside the scope of a step (ie not while the step is running).

    Another note about the code you shared. I see that you are adding the attribute TOTAL_RECORDS in the step execution context after the step (ie in method StepExecutionListener#afterStep), but reading it in the item writer bean definition writer @Value("#stepExecutionContext['TOTAL_RECORDS']}") long recordCount. This is not correct as the item writer is invoked before the listener's afterStep method.

    EDIT: I could not reproduce the issue with the shared code. Here is a minimal complete verifiable example:

    File pom.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <groupId>com.example</groupId>
        <artifactId>so75784914</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>so75784914</name>
        <description>Demo project for SO 75784914</description>
        <properties>
            <java.version>17</java.version>
        </properties>
        <dependencies>
            <dependency>
                <groupId>org.springframework.batch</groupId>
                <artifactId>spring-batch-core</artifactId>
                <version>5.0.1</version>
            </dependency>
    
            <dependency>
                <groupId>com.h2database</groupId>
                <artifactId>h2</artifactId>
                <scope>runtime</scope>
                <version>2.1.214</version>
            </dependency>
            <dependency>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-simple</artifactId>
                <version>2.0.6</version>
            </dependency>
    
            <dependency>
                <groupId>org.junit.jupiter</groupId>
                <artifactId>junit-jupiter</artifactId>
                <version>5.9.2</version>
                <scope>test</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.batch</groupId>
                <artifactId>spring-batch-test</artifactId>
                <version>5.0.1</version>
                <scope>test</scope>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.10.1</version>
                    <configuration>
                        <release>17</release>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    
    </project>
    
    

    File src/main/java/com/example/so75784914/JobConfig.java

    package com.example.so75784914;
    
    import java.util.Arrays;
    import java.util.List;
    
    import javax.sql.DataSource;
    
    import org.springframework.batch.core.Job;
    import org.springframework.batch.core.JobParameters;
    import org.springframework.batch.core.JobParametersBuilder;
    import org.springframework.batch.core.Step;
    import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
    import org.springframework.batch.core.configuration.annotation.StepScope;
    import org.springframework.batch.core.job.builder.JobBuilder;
    import org.springframework.batch.core.launch.JobLauncher;
    import org.springframework.batch.core.repository.JobRepository;
    import org.springframework.batch.core.step.builder.StepBuilder;
    import org.springframework.batch.item.ItemReader;
    import org.springframework.batch.item.ItemWriter;
    import org.springframework.batch.item.file.FlatFileItemWriter;
    import org.springframework.batch.item.file.builder.FlatFileItemWriterBuilder;
    import org.springframework.batch.item.support.ListItemReader;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.core.io.FileSystemResource;
    import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
    import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
    import org.springframework.jdbc.support.JdbcTransactionManager;
    
    @Configuration
    @EnableBatchProcessing
    public class JobConfig {
    
        @Bean
        public ListItemReader<Person> itemReader() {
            List<Person> items = Arrays.asList(
                    new Person(1, "foo1")
                    , new Person(2, "foo2")
                    , new Person(3, "bar1")
                    , new Person(4, "bar2")
            );
            return new ListItemReader<>(items);
        }
    
        @Bean
        @StepScope
        public FlatFileItemWriter<Person> itemWriter(@Value("#{jobParameters['fileName']}") String fileName) {
            return new FlatFileItemWriterBuilder<Person>()
                    .name("personItemWriter")
                    .resource(new FileSystemResource(fileName))
                    .delimited()
                    .names("id", "name")
                    .build();
        }
    
        @Bean
        public Step step(JobRepository jobRepository, JdbcTransactionManager transactionManager,
                         ItemReader<Person> reader, ItemWriter<Person> writer) {
            return new StepBuilder("step", jobRepository)
                    .<Person, Person>chunk(2, transactionManager)
                    .reader(reader)
                    .writer(writer)
                    .build();
        }
    
        @Bean
        public Job job(JobRepository jobRepository, Step step) {
            return new JobBuilder("job", jobRepository)
                    .start(step)
                    .build();
        }
    
        @Bean
        public DataSource dataSource() {
            return new EmbeddedDatabaseBuilder()
                    .setType(EmbeddedDatabaseType.H2)
                    .addScript("/org/springframework/batch/core/schema-h2.sql")
                    .build();
        }
    
        @Bean
        public JdbcTransactionManager transactionManager(DataSource dataSource) {
            return new JdbcTransactionManager(dataSource);
        }
    
        public static void main(String[] args) throws Exception {
            ApplicationContext context = new AnnotationConfigApplicationContext(JobConfig.class);
            JobLauncher jobLauncher = context.getBean(JobLauncher.class);
            Job job = context.getBean(Job.class);
            JobParameters jobParameters = new JobParametersBuilder()
                    .addString("fileName", "persons.csv")
                    .toJobParameters();
            jobLauncher.run(job, jobParameters);
        }
    
        public static class Person {
            private int id;
            private String name;
    
            public Person() {
            }
    
            public Person(int id, String name) {
                this.id = id;
                this.name = name;
            }
    
            public int getId() {
                return id;
            }
    
            public void setId(int id) {
                this.id = id;
            }
    
            public String getName() {
                return name;
            }
    
            public void setName(String name) {
                this.name = name;
            }
    
            public String toString() {
                return "Person{id=" + id + ", name='" + name + '\'' + '}';
            }
        }
    
    }
    

    File src/test/java/com/example/so75784914/So75784914ApplicationTests.java

    package com.example.so75784914;
    
    import org.junit.jupiter.api.Assertions;
    import org.junit.jupiter.api.Test;
    
    import org.springframework.batch.core.BatchStatus;
    import org.springframework.batch.core.JobExecution;
    import org.springframework.batch.core.JobParameters;
    import org.springframework.batch.core.JobParametersBuilder;
    import org.springframework.batch.test.JobLauncherTestUtils;
    import org.springframework.batch.test.context.SpringBatchTest;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
    
    @SpringBatchTest
    @SpringJUnitConfig({JobConfig.class})
    
    class So75784914ApplicationTests {
    
        @Autowired
        private JobLauncherTestUtils jobLauncherTestUtils;
    
        @Test
        public void employeeJobExecutionTest() throws Exception {
            JobParameters jobParameter = new JobParametersBuilder()
                    .addString("fileName", "output.csv")
                    .toJobParameters();
            JobExecution jobExecution = jobLauncherTestUtils.launchJob(jobParameter);
            Assertions.assertEquals(BatchStatus.COMPLETED, jobExecution.getStatus());
    
        }
    
    }
    

    This example uses a step-scoped item writer where the output file name is configured from job parameters. When I run the test, the output file is written correctly without any issue. The sample uses Spring Batch version 5.0.1 as in the question.