Q:

Spring Batch job throwing StackOverflowError: null error when started

I have a simple controller that takes a JSON string of file paths and runs a spring batch job on these files. To implement spring batch I am following a tutorial that would eventually produce the code in https://github.com/michaelhoffmantech/patient-batch-loader.
@RestController
public class JobController {
    private static final Log _logger = LogFactory.getLog(JobController.class);
    @Autowired
    private JobLauncher jobLauncher;
    @Autowired
    private Job job;

    @RequestMapping(value = "/job/import", method = RequestMethod.POST)
    public void importAsset(@RequestBody String uploadedFiles) throws JobParametersInvalidException, JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException {
        _logger.debug("importAsset() >>");
        try{
            _logger.debug("Uploaded files: ");
            _logger.debug(uploadedFiles);

            _logger.debug("Putting values into the parameter map...");
            Map<String, JobParameter> parameterMap = new LinkedHashMap<>();
            parameterMap.put(Constants.JOB_PARAM_UPLOADED_FILES, new JobParameter(uploadedFiles));

            _logger.debug("Launching job [" + job.getName() + "]...");
            jobLauncher.run(job, new JobParameters(parameterMap));
            _logger.debug("<< importAsset()");
        }
        catch (Exception e){
            String errorMessage = "An error occured while importing a batch. " + e.getLocalizedMessage();
            _logger.error(errorMessage, e);
            throw e;
        }
    }
}

Batch configuration is:
@Configuration
@EnableBatchProcessing
public class BatchConfig implements BatchConfigurer {
    @Autowired
    private PlatformTransactionManager platformTransactionManager;

    @Autowired
    private DataSource dataSource;

    private JobRepository jobRepository;
    private JobExplorer jobExplorer;
    private JobLauncher jobLauncher;

    @Override
    public JobRepository getJobRepository() throws Exception {
        return this.jobRepository;
    }

    @Override
    public PlatformTransactionManager getTransactionManager() throws Exception {
        return this.platformTransactionManager;
    }

    @Override
    public JobLauncher getJobLauncher() throws Exception {
        return this.jobLauncher;
    }

    @Override
    public JobExplorer getJobExplorer() throws Exception {
        return this.jobExplorer;
    }

    protected JobLauncher createJobLauncher() throws Exception {
        SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
        jobLauncher.setJobRepository(this.jobRepository);
        jobLauncher.afterPropertiesSet();
        return jobLauncher;
    }

    protected JobRepository createJobRepository() throws Exception {
        JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean();
        factory.setDataSource(this.dataSource);
        factory.setTransactionManager(getTransactionManager());
        factory.afterPropertiesSet();
        return factory.getObject();
    }

    @PostConstruct
    public void afterPropertiesSet() throws Exception{
        this.jobRepository = createJobRepository();
        JobExplorerFactoryBean jobExplorerFactoryBean = new JobExplorerFactoryBean();
        jobExplorerFactoryBean.setDataSource(this.dataSource);
        jobExplorerFactoryBean.afterPropertiesSet();
        this.jobExplorer = jobExplorerFactoryBean.getObject();
        this.jobLauncher = createJobLauncher();
    }
}

And finally the job configuration is:
@Configuration
public class BatchJobConfig {
    private static final Log _logger = LogFactory.getLog(BatchJobConfig.class);

    @Autowired
    private JobBuilderFactory jobBuilderFactory;
    @Autowired
    private StepBuilderFactory stepBuilderFactory;

    @Bean
    JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor(JobRegistry jobRegistry){
        JobRegistryBeanPostProcessor postProcessor = new JobRegistryBeanPostProcessor();
        postProcessor.setJobRegistry(jobRegistry);
        return postProcessor;
    }

    @Bean
    public Step step() throws Exception{
        return this.stepBuilderFactory
                .get(Constants.STEP_NAME)
                .tasklet(new Tasklet() {
                    @Override
                    public RepeatStatus execute(StepContribution stepContribution, ChunkContext chunkContext) throws Exception {
                        _logger.debug("execute() >>");
                        _logger.debug("<< execute()");
                        return RepeatStatus.FINISHED;
                    }
                })
                .build();
    }

    @Bean
    public Job job(Step step){
        return this.jobBuilderFactory
                .get(Constants.JOB_NAME)
                .validator(validator())
                .start(step)
                .build();
    }

    public JobParametersValidator validator(){
        return new JobParametersValidator(){
            @Override
            public void validate(JobParameters parameters) throws JobParametersInvalidException {
                _logger.debug("validate() >>");

                String filePathsJsonStr = parameters.getString(Constants.JOB_PARAM_UPLOADED_FILES);
                if(StringUtils.isBlank(filePathsJsonStr)){
                    throw new JobParametersInvalidException("'" + Constants.JOB_PARAM_UPLOADED_FILES  + "' parameter is required for job '" + Constants.JOB_NAME + "'.");
                }

                Gson gson = new Gson();
                Type listType = new TypeToken<ArrayList<UploadFile>>(){}.getType();
                ArrayList<UploadFile> uploadedFiles = gson.fromJson(filePathsJsonStr, listType);

                for(UploadFile uploadFile: uploadedFiles){
                    File file = new File(uploadFile.getPath());
                    if(!file.exists()){
                        throw new JobParametersInvalidException("File '" + uploadFile.getPath() + "' did not exist or was not readable.");
                    }
                }

                _logger.debug("<< validate()");
            }
        };
    }
}

When I run the batch job through the REST method. I get the following in the log:
2018-09-29 20:00:51.680 DEBUG 10104 --- [nio-8102-exec-2] c.g.m.t.t.controllers.JobController      : importAsset() >>
2018-09-29 20:00:51.680 DEBUG 10104 --- [nio-8102-exec-2] c.g.m.t.t.controllers.JobController      : Uploaded files: 
2018-09-29 20:00:51.680 DEBUG 10104 --- [nio-8102-exec-2] c.g.m.t.t.controllers.JobController      : [{"path":"C:\\app-import-staging\\cat-pet-animal-domestic-104827.jpeg","username":"test"},{"path":"C:\\app-import-staging\\kittens-cat-cat-puppy-rush-45170.jpeg","username":"test"}]
2018-09-29 20:00:51.680 DEBUG 10104 --- [nio-8102-exec-2] c.g.m.t.t.controllers.JobController      : Putting values into the parameter map...
2018-09-29 20:00:51.681 DEBUG 10104 --- [nio-8102-exec-2] c.g.m.t.t.controllers.JobController      : Launching job [app-import]...
2018-09-29 20:00:51.745 ERROR 10104 --- [nio-8102-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.StackOverflowError] with root cause

java.lang.StackOverflowError: null
    at java.util.HashMap.putVal(HashMap.java:657) ~[na:1.8.0_181]
    at java.util.HashMap.put(HashMap.java:612) ~[na:1.8.0_181]
    at java.util.HashSet.add(HashSet.java:220) ~[na:1.8.0_181]
    at java.util.Collections$SynchronizedCollection.add(Collections.java:2035) ~[na:1.8.0_181]
    at java.lang.ClassLoader.checkPackageAccess(ClassLoader.java:508) ~[na:1.8.0_181]
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:333) ~[spring-aop-4.3.14.RELEASE.jar:4.3.14.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190) ~[spring-aop-4.3.14.RELEASE.jar:4.3.14.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.3.14.RELEASE.jar:4.3.14.RELEASE]
    at org.springframework.batch.core.configuration.annotation.SimpleBatchConfiguration$PassthruAdvice.invoke(SimpleBatchConfiguration.java:127) ~[spring-batch-core-3.0.8.RELEASE.jar:3.0.8.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.14.RELEASE.jar:4.3.14.RELEASE]
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213) ~[spring-aop-4.3.14.RELEASE.jar:4.3.14.RELEASE]
    at com.sun.proxy.$Proxy83.getTransaction(Unknown Source) ~[na:na]
    at sun.reflect.GeneratedMethodAccessor93.invoke(Unknown Source) ~[na:na]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_181]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_181]
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:333) ~[spring-aop-4.3.14.RELEASE.jar:4.3.14.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190) ~[spring-aop-4.3.14.RELEASE.jar:4.3.14.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.3.14.RELEASE.jar:4.3.14.RELEASE]
    at org.springframework.batch.core.configuration.annotation.SimpleBatchConfiguration$PassthruAdvice.invoke(SimpleBatchConfiguration.java:127) ~[spring-batch-core-3.0.8.RELEASE.jar:3.0.8.RELEASE]

What I noticed was that the part:
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.14.RELEASE.jar:4.3.14.RELEASE]
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213) ~[spring-aop-4.3.14.RELEASE.jar:4.3.14.RELEASE]
    at com.sun.proxy.$Proxy83.getTransaction(Unknown Source) ~[na:na]
    at sun.reflect.GeneratedMethodAccessor93.invoke(Unknown Source) ~[na:na]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_181]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_181]
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:333) ~[spring-aop-4.3.14.RELEASE.jar:4.3.14.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190) ~[spring-aop-4.3.14.RELEASE.jar:4.3.14.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.3.14.RELEASE.jar:4.3.14.RELEASE]
    at org.springframework.batch.core.configuration.annotation.SimpleBatchConfiguration$PassthruAdvice.invoke(SimpleBatchConfiguration.java:127) ~[spring-batch-core-3.0.8.RELEASE.jar:3.0.8.RELEASE]

goes on and on until it throws a StackOverflowError.
Any advice or help on what to change to fix this would be appreciated.

A:

I ended up simplifying the job by removing BatchJobConfig and modifying the BatchConfig class like below:
@Configuration
@EnableBatchProcessing
public class BatchConfig {
    private static final Log _logger = LogFactory.getLog(BatchConfig.class);

    @Bean
    public Job job(JobBuilderFactory jobBuilderFactory, StepBuilderFactory stepBuilderFactory){
        Step step = stepBuilderFactory.get(Constants.STEP_NAME)
                .tasklet(new Tasklet() {
                    @Override
                    public RepeatStatus execute(StepContribution stepContribution, ChunkContext chunkContext) throws Exception {
                        _logger.debug("execute() >>");
                        _logger.debug("<< execute()");
                        return RepeatStatus.FINISHED;
                    }
                })
                .build();

        return jobBuilderFactory.get(Constants.JOB_NAME)
                .incrementer(new RunIdIncrementer())
                .validator(validator())
                .start(step)
                .build();
    }

    public JobParametersValidator validator(){
        return new JobParametersValidator(){
            @Override
            public void validate(JobParameters parameters) throws JobParametersInvalidException {
                _logger.debug("validate() >>");

                String filePathsJsonStr = parameters.getString(Constants.JOB_PARAM_UPLOADED_FILES);
                if(StringUtils.isBlank(filePathsJsonStr)){
                    throw new JobParametersInvalidException("'" + Constants.JOB_PARAM_UPLOADED_FILES  + "' parameter is required for job '" + Constants.JOB_NAME + "'.");
                }

                Gson gson = new Gson();
                Type listType = new TypeToken<ArrayList<UploadFile>>(){}.getType();
                ArrayList<UploadFile> uploadedFiles = gson.fromJson(filePathsJsonStr, listType);

                for(UploadFile uploadFile: uploadedFiles){
                    File file = new File(uploadFile.getPath());
                    if(!file.exists()){
                        throw new JobParametersInvalidException("File '" + uploadFile.getPath() + "' did not exist or was not readable.");
                    }
                }

                _logger.debug("<< validate()");
            }
        };
    }
}

And after this, the job executed as expected.

