4
votes

I am currently setting up a file import service for an application, which would allow a user to upload a csv file via REST. The import of the job will be done via Spring Batch as these jobs may be long running, based on future processing requirements and checks.

According to me the below setup is correct for Spring Boot and Spring Batch, but the code won't compile.

The main BatchConfiguration file:

@Configuration
@EnableBatchProcessing
public class PatientBatchConfiguration {

    @Autowired
    private JobBuilderFactory jobBuilders;

    @Autowired
    private StepBuilderFactory stepBuilders;

    @Autowired
    private PatientFieldSetMapper fieldSetMapper;

    @Autowired
    private PatientItemWriter writer;

    @Bean
    public Job importPatientsFromUpload(){
        return jobBuilders.get("importPatientsFromUpload")
                .start(step())
                .build();
    }

    @Bean
    public Step step(){
        return stepBuilders.get("step")
                .<Patient,Patient>chunk(1)
                .reader(reader(null))
                .writer(writer)
                .build();
    }

    @Bean
    @StepScope
    public ItemReader<Patient> reader(@Value("#{jobParameters['fileName']}") String filePath) {
        FlatFileItemReader<Patient> itemReader = new FlatFileItemReader<Patient>();
        itemReader.setLineMapper(lineMapper());
        itemReader.setResource(new FileSystemResource(filePath));
        return itemReader;
    }

    private LineMapper<Patient> lineMapper() {
        DefaultLineMapper<Patient> lineMapper = new DefaultLineMapper<Patient>();
        DelimitedLineTokenizer lineTokenizer = new DelimitedLineTokenizer();
        lineTokenizer.setNames(new String[]{"name","surname","idNumber","dob", "email", "cell"});
        lineMapper.setLineTokenizer(lineTokenizer);
        lineMapper.setFieldSetMapper(fieldSetMapper);
        return lineMapper;
    }

}

FieldSetMapper code:

@Component
public class PatientFieldSetMapper implements FieldSetMapper<Patient> {

    @Override
    public Patient mapFieldSet(FieldSet fieldSet) throws BindException {

        if(fieldSet == null){
            return null;
        }

        Patient patient = new Patient();
        patient.setName(fieldSet.readString("name"));
        patient.setSurname(fieldSet.readString("surname"));
        patient.setIdNo(fieldSet.readString("idNumber"));
        patient.setDob(0L);
        patient.setEmail(fieldSet.readString("email"));
        patient.setCell(fieldSet.readString("cell"));

        return patient;
    }
}

PatientItemWriter code:

@Component
public class PatientItemWriter implements ItemWriter<Patient> {

    @Autowired
    PatientRepository patientRepository;

    @Override
    public void write(List<? extends Patient> list) throws Exception {

        for(Patient patient: list) {
            patientRepository.save(patient);
        }
    }
}

Stacktrace:

Caused by: java.lang.IllegalArgumentException: Path must not be null
    at org.springframework.util.Assert.notNull(Assert.java:115) ~[spring-core-4.2.4.RELEASE.jar:4.2.4.RELEASE]
    at org.springframework.core.io.FileSystemResource.<init>(FileSystemResource.java:75) ~[spring-core-4.2.4.RELEASE.jar:4.2.4.RELEASE]
    at com.example.batch.patient.PatientBatchConfiguration.reader(PatientBatchConfiguration.java:59) ~[classes/:na]
    at com.example.batch.patient.PatientBatchConfiguration.step(PatientBatchConfiguration.java:49) ~[classes/:na]

And finally the application.properties file

spring.batch.job.enabled=false

The reason spring.batch.job.enabled=false is located in the properties file, is that the job importPatientsFromUpload will be called from a controller after the user has upload a file, and without it, jobs will run at startup.

The problem I am having is that the FileSystemResource is failing to be created because the path can't be null. However I understood that as soon as one annotates a method with @StepScope, a proxy bean will be created, which would allow me to use the jobParameter passed through at runtime to create a new file system resource. I have seen various examples online of using jobParameters this way, but for some reason, the bean seems not to be created correctly. I am not sure whether this is related to the fact that I am using Spring Boot, or some other error.

Any help would be appreciated. Thanks in advance.

UPDATE in Reply to Gaël

PatientItemReader code:

@Component
@StepScope
public class PatientItemReader implements ItemReader<Patient> {

    @Autowired
    private PatientFieldSetMapper fieldSetMapper;

    private FlatFileItemReader<Patient> itemReader;

    @Override
    public Patient read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException {
        return itemReader.read();
    }

    public PatientItemReader(@Value("#{jobParameters[filePath]}") String filePath) {
            itemReader = new FlatFileItemReader<Patient>();
            DefaultLineMapper<Patient> lineMapper = new DefaultLineMapper<Patient>();
            DelimitedLineTokenizer lineTokenizer = new DelimitedLineTokenizer();
            lineTokenizer.setNames(new String[]{"name","surname","idNumber","dob", "email", "cell"});
            lineMapper.setLineTokenizer(lineTokenizer);
            lineMapper.setFieldSetMapper(fieldSetMapper);
            itemReader.setLineMapper(lineMapper);
            itemReader.setResource(new FileSystemResource(filePath));
    }
}
2
When creating your step, your are calling .reader(reader(null)) with an explicit null value.Gaël J
Hi Gaël that is correct, the reader method requires a String variable, so I have to pass something, hence the null value. It shouldn't be a problem as the variable is supposed to be late binded to the Spring Batch job parameter, hence the null will be overwritten when the proxyied bean gets called, or not? The reader(string) method should only be executed when I call the Batch job, but compilation doesn't succeed. I followed this blog post blog.codecentric.de/en/2013/06/… by Tobias Flohre. Any advice?andrewbroekman

2 Answers

2
votes

I guess the problem is you are creating the reader outside of Spring and passing it a null value when doing this:

@Bean
public Step step(){
    ...
    .reader(reader(null))
    ...
}

You should do something like this in order to user Spring capabilities like the late binding:

@Autowired
private ItemReader<Patient> reader;

@Bean
public Step step(){
    ...
    .reader(reader)
    ...
}

As you did for your writer.

0
votes

when you using @Bean, it's meaning you want Spring container to manage your instance. So, in this step:

@Bean
public Step step() {...}

When you want to get a reader instance to build an instance of step, you can't use like reader(null). You should set the reader as a paramater of step, then Spring container will inject the reader instance. This is the rigth code:

@Bean
public Step step(ItemReader<Patient> reader){
    return stepBuilders.get("step")
            .<Patient,Patient>chunk(1)
            .reader(reader)
            .writer(writer)
            .build();
}