Access execution context from StaxWriterCallback



I’m implementing a job that will load data from a database and write the result to xml files. The xml files will have a header that is based on some attributes that are stored in the execution context. Therefore, I want to acces the execution context. I’ve done this in other beans in this job, but I can’t figure out how to access the execution context from within my StaxWriterCallback.

In the job my writer is currently implemented as:

@Bean(name = "writer")
@StepScope
public StaxEventItemWriter<Data> writer(@Value("#{jobParameters['path']}") String path) {
        
    StaxEventItemWriter<Data> itemWriter = new StaxEventItemWriter<Data>();
    itemWriter.setVersion("1.0");
    itemWriter.setEncoding("UTF-8");
    itemWriter.setStandalone(false);
        
    itemWriter.setHeaderCallback(headerCallback());
    itemWriter.setFooterCallback(new CustomXMLFooterCallback());
        
    itemWriter.setMarshaller(new XStreamMarshaller());
                
    FileSystemResource file = new FileSystemResource(path + "test.xml");
    itemWriter.setResource(file);
        
    return itemWriter;
}

The header callback is implemented as:

@Component
public class CustomXMLHeaderCallback implements StaxWriterCallback {
    
    private StepExecution stepExecution;

    @BeforeStep
    public void beforeStep(StepExecution stepExecution) {
        this.stepExecution = stepExecution;
    }

    @Override
    public void write(XMLEventWriter writer) throws IOException {
        
        String currentFile = stepExecution.getExecutionContext().getString("currentFile");

        try {

            XMLEventFactory eventFactory = XMLEventFactory.newInstance();
            XMLEvent end = eventFactory.createDTD("n");
            XMLEvent tab = eventFactory.createDTD("t");

            writer.add(eventFactory.createStartElement("", "", "file"));
            writer.add(eventFactory.createCharacters(currentFile.substring(19, 31)));
            writer.add(eventFactory.createEndElement("", "", "file"));
            writer.add(end);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

The header callback is added to the batch job with:

@Bean
@StepScope
public CustomXMLHeaderCallback headerCallback() {
    return new CustomXMLHeaderCallback();
}

Unfortunately I get the following error:

java.lang.NullPointerException: Cannot invoke "org.springframework.batch.core.StepExecution.getExecutionContext()" because "this.stepExecution" is null
    at com.test.xml.callback.CustomXMLHeaderCallback.write(CustomXMLHeaderCallback.java:39) ~[classes/:na]
    at com.test.xml.callback.CustomXMLHeaderCallback$$FastClassBySpringCGLIB$$e0795ecb.invoke(<generated>) ~[classes/:na]
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.3.3.jar:5.3.3]
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:779) ~[spring-aop-5.3.3.jar:5.3.3]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.3.3.jar:5.3.3]
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750) ~[spring-aop-5.3.3.jar:5.3.3]
    at org.springframework.aop.support.DelegatingIntroductionInterceptor.doProceed(DelegatingIntroductionInterceptor.java:137) ~[spring-aop-5.3.3.jar:5.3.3]
    at org.springframework.aop.support.DelegatingIntroductionInterceptor.invoke(DelegatingIntroductionInterceptor.java:124) ~[spring-aop-5.3.3.jar:5.3.3]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.3.jar:5.3.3]
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750) ~[spring-aop-5.3.3.jar:5.3.3]
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:692) ~[spring-aop-5.3.3.jar:5.3.3]
    at com.test.xml.callback.CustomXMLHeaderCallback$$EnhancerBySpringCGLIB$$e9083311.write(<generated>) ~[classes/:na]
    at org.springframework.batch.item.xml.StaxEventItemWriter.open(StaxEventItemWriter.java:447) ~[spring-batch-infrastructure-4.3.1.jar:4.3.1]
    at org.springframework.batch.item.xml.StaxEventItemWriter$$FastClassBySpringCGLIB$$d105dd1.invoke(<generated>) ~[spring-batch-infrastructure-4.3.1.jar:4.3.1]
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.3.3.jar:5.3.3]
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:779) ~[spring-aop-5.3.3.jar:5.3.3]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.3.3.jar:5.3.3]
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750) ~[spring-aop-5.3.3.jar:5.3.3]
    at org.springframework.aop.support.DelegatingIntroductionInterceptor.doProceed(DelegatingIntroductionInterceptor.java:137) ~[spring-aop-5.3.3.jar:5.3.3]
    at org.springframework.aop.support.DelegatingIntroductionInterceptor.invoke(DelegatingIntroductionInterceptor.java:124) ~[spring-aop-5.3.3.jar:5.3.3]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.3.jar:5.3.3]
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750) ~[spring-aop-5.3.3.jar:5.3.3]
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:692) ~[spring-aop-5.3.3.jar:5.3.3]
    at org.springframework.batch.item.xml.StaxEventItemWriter$$EnhancerBySpringCGLIB$$599f2f97.open(<generated>) ~[spring-batch-infrastructure-4.3.1.jar:4.3.1]
    at org.springframework.batch.item.support.CompositeItemStream.open(CompositeItemStream.java:104) ~[spring-batch-infrastructure-4.3.1.jar:4.3.1]
    at org.springframework.batch.core.step.tasklet.TaskletStep.open(TaskletStep.java:311) ~[spring-batch-core-4.3.1.jar:4.3.1]
    at org.springframework.batch.core.step.AbstractStep.execute(AbstractStep.java:205) ~[spring-batch-core-4.3.1.jar:4.3.1]
    at org.springframework.batch.core.job.SimpleStepHandler.handleStep(SimpleStepHandler.java:148) ~[spring-batch-core-4.3.1.jar:4.3.1]
    at org.springframework.batch.core.job.AbstractJob.handleStep(AbstractJob.java:411) ~[spring-batch-core-4.3.1.jar:4.3.1]
    at org.springframework.batch.core.job.SimpleJob.doExecute(SimpleJob.java:136) ~[spring-batch-core-4.3.1.jar:4.3.1]
    at org.springframework.batch.core.job.AbstractJob.execute(AbstractJob.java:320) ~[spring-batch-core-4.3.1.jar:4.3.1]
    at org.springframework.batch.core.launch.support.SimpleJobLauncher$1.run(SimpleJobLauncher.java:147) ~[spring-batch-core-4.3.1.jar:4.3.1]
    at org.springframework.core.task.SyncTaskExecutor.execute(SyncTaskExecutor.java:50) ~[spring-core-5.3.3.jar:5.3.3]
    at org.springframework.batch.core.launch.support.SimpleJobLauncher.run(SimpleJobLauncher.java:140) ~[spring-batch-core-4.3.1.jar:4.3.1]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
    at java.base/java.lang.reflect.Method.invoke(Method.java:564) ~[na:na]
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344) ~[spring-aop-5.3.3.jar:5.3.3]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:198) ~[spring-aop-5.3.3.jar:5.3.3]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.3.3.jar:5.3.3]
    at org.springframework.batch.core.configuration.annotation.SimpleBatchConfiguration$PassthruAdvice.invoke(SimpleBatchConfiguration.java:128) ~[spring-batch-core-4.3.1.jar:4.3.1]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.3.jar:5.3.3]
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215) ~[spring-aop-5.3.3.jar:5.3.3]
    at com.sun.proxy.$Proxy62.run(Unknown Source) ~[na:na]
    at org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner.execute(JobLauncherApplicationRunner.java:199) ~[spring-boot-autoconfigure-2.4.2.jar:2.4.2]
    at org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner.executeLocalJobs(JobLauncherApplicationRunner.java:173) ~[spring-boot-autoconfigure-2.4.2.jar:2.4.2]
    at org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner.launchJobFromProperties(JobLauncherApplicationRunner.java:160) ~[spring-boot-autoconfigure-2.4.2.jar:2.4.2]
    at org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner.run(JobLauncherApplicationRunner.java:155) ~[spring-boot-autoconfigure-2.4.2.jar:2.4.2]
    at org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner.run(JobLauncherApplicationRunner.java:150) ~[spring-boot-autoconfigure-2.4.2.jar:2.4.2]
    at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:795) ~[spring-boot-2.4.2.jar:2.4.2]
    at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:785) ~[spring-boot-2.4.2.jar:2.4.2]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:333) ~[spring-boot-2.4.2.jar:2.4.2]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1311) ~[spring-boot-2.4.2.jar:2.4.2]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1300) ~[spring-boot-2.4.2.jar:2.4.2]
    at com.test.main(MainOutboundDatins.java:18) ~[classes/:na]

Answer

If you want the beforeStep method to be called before the step in your CustomXMLHeaderCallback, you need to register that component as a listener in your step with one of the step builder’s listener methods. This is explained in the docs here: Intercepting Step Execution.

Otherwise, you can inject the step execution with @Value:

@Value("#{StepExecution}")
private StepExecution stepExecution;

A third option is to inject the required attribute from the step execution context:

// define your header as follows:
public class CustomXMLHeaderCallback implements StaxWriterCallback {

   private String currentFile;

   public CustomXMLHeaderCallback(String currentFile) {
      this.currentFile = currentFile;
   }

   @Override
   public void write(XMLEventWriter writer) throws IOException {
      // use this.currentFile as needed here
   }
   

}

// decalre your bean as follows:
@Bean
@StepScope
public CustomXMLHeaderCallback headerCallback(@Value("#{stepExecutionContext['currentFile']}") String currentFile) {
    return new CustomXMLHeaderCallback(currentFile);
}

I recommend the third option as it injects only the required attribute and not the whole step execution object.



Source: stackoverflow