Helidon with JBatch Guide

This guide describes how Helidon and Jakarta Batch (JBatch) can be used together to execute batch jobs in environments that do not fully support EE environments.

What You Need

For this 20 minute tutorial, you will need the following:

A Helidon {upper-case-flavor} ApplicationYou can use your own application or use the Helidon {upper-case-flavor} Quickstart to create a sample application.
Java SE 11 (Open JDK 11)Helidon requires Java 11+.
Maven 3.6.1+Helidon requires Maven 3.6.1+.
Docker 18.09+You need Docker if you want to build and deploy Docker containers.
Kubectl 1.16.5+If you want to deploy to Kubernetes, you need kubectl and a Kubernetes cluster (you can install one on your desktop).
Verify Prerequisites
java -version
mvn --version
docker --version
kubectl version --short
Copied
Setting JAVA_HOME
# On Mac
export JAVA_HOME=`/usr/libexec/java_home -v 11`

# On Linux
# Use the appropriate path to your JDK
export JAVA_HOME=/usr/lib/jvm/jdk-11
Copied

This guide assumes you are familiar with the Jakarta Batch project specification from the Ecplipse Foundation project site.

Dependencies

For this example, add the IBM JBatch implementation and the derby embedded DB (since JPA and JPA are not available by default) dependencies to the testing module:

Maven dependencies
<dependencies>
  <dependency>
     <groupId>com.ibm.jbatch</groupId>
     <artifactId>com.ibm.jbatch.container</artifactId>
 </dependency>
  <dependency>
     <groupId>org.apache.derby</groupId>
     <artifactId>derby</artifactId>
  </dependency>
<dependencies>
Copied

Add Sample Jobs

In this demonstration you will first create sample input and output records and then the following jobs: MyItemReader, MyItemProcessor and MyItemWriter. Finally you will create MyBatchlet to demonstrate all possible usages of JBatch.

1. Create a unit of input information

MyInputRecord
public class MyInputRecord {
    private int id;


    public MyInputRecord(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return "MyInputRecord: " + id;
    }
}
Copied

2. Create a unit of output information

MyOutputRecord
public class MyOutputRecord {

    private int id;

    public MyOutputRecord(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return "MyOutputRecord: " + id;
    }
}
Copied

3. Create MyItemReader to extend AbstractItemReader

MyItemReader should look like this:

MyItemReader
public class MyItemReader extends AbstractItemReader {

    private final StringTokenizer tokens;

    public MyItemReader() {
        tokens = new StringTokenizer("1,2,3,4,5,6,7,8,9,10", ",");
    }

    /**
     * Perform read Item.
     * @return Stage result.
     */
    @Override
    public MyInputRecord readItem() {
        if (tokens.hasMoreTokens()) {
            return new MyInputRecord(Integer.valueOf(tokens.nextToken()));
        }
        return null;
    }
}
Copied

4. Create MyItemProcessor to implement ItemProcessor

The MyItemProcessor will perform some simple operations:

MyItemProcessor
public class MyItemProcessor implements ItemProcessor {

    @Override
    public MyOutputRecord processItem(Object t) {
        System.out.println("processItem: " + t);

        return (((MyInputRecord) t).getId() % 2 == 0) ? null : new MyOutputRecord(((MyInputRecord) t).getId() * 2);
    }
}
Copied

5. Create MyItemWriter to extend AbstractItemWriter

MyItemWriter prints the result:

MyItemWriter
public class MyItemWriter extends AbstractItemWriter {

    @Override
    public void writeItems(List list) {
        System.out.println("writeItems: " + list);
    }
}
Copied

6. Create MyBatchlet to extentd AbstractBatchlet

MyBatchlet simply completes the process:

MyBatchlet
public class MyBatchlet extends AbstractBatchlet {

    @Override
    public String process() {
        System.out.println("Running inside a batchlet");

        return "COMPLETED";
    }

}
Copied

Update the Descriptor File

Add this code to your job descriptor.xml file:

Updated descriptor file
<job id="myJob" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/jobXML_1_0.xsd" version="1.0">
    <step id="step1" next="step2">
        <chunk item-count="3"> 
            <reader ref="io.helidon.jbatch.example.jobs.MyItemReader"/>
            <processor ref="io.helidon.jbatch.example.jobs.MyItemProcessor"/>
            <writer ref="io.helidon.jbatch.example.jobs.MyItemWriter"/>
        </chunk>
    </step>
    <step id="step2" > 
        <batchlet ref="io.helidon.jbatch.example.jobs.MyBatchlet"/>
    </step>
</job>
Copied
  • The first step of the job includes MyItemReader, MyItemProcessor and MyItemWriter.
  • The second step of the job includes MyBatchlet.

You must specify the fully qualified names in the ref properties, like “io.helidon.jbatch.example.jobs.MyItemReader”, otherwise it will not work.

Create an Endpoint

Create a small endpoint to activate the job:

new endpoint
@Path("/batch")
@ApplicationScoped
public class BatchResource {
    private static final JsonBuilderFactory JSON = Json.createBuilderFactory(Collections.emptyMap());

    private JobOperator jobOperator;

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public JsonObject executeBatch() {

        BatchSPIManager batchSPIManager = BatchSPIManager.getInstance();
        batchSPIManager.registerPlatformMode(BatchSPIManager.PlatformMode.SE);
        batchSPIManager.registerExecutorServiceProvider(new HelidonExecutorServiceProvider());

        jobOperator = getJobOperator();
        Long executionId = jobOperator.start("myJob", new Properties());

        return JSON.createObjectBuilder()
                .add("Started a job with Execution ID: ", executionId)
                .build();
    }


    @GET
    @Path("/status/{execution-id}")
    public JsonObject status(@PathParam("execution-id") Long executionId){
        JobExecution jobExecution = jobOperator.getJobExecution(executionId);

        List<StepExecution> stepExecutions = jobOperator.getStepExecutions(executionId);
        List<String> executedSteps = new ArrayList<>();
        for (StepExecution stepExecution : stepExecutions) {
            executedSteps.add(stepExecution.getStepName());
        }

        return JSON.createObjectBuilder()
                .add("Steps executed", Arrays.toString(executedSteps.toArray()))
                .add("Status", jobExecution.getBatchStatus().toString())
                .build();
    }
}
Copied

Helidon specifies to JBatch that it should run in Standalone (SE) mode. It will also register the HelidonExecutorServiceProvider which is actually relatively small. For our example we need something really small, like a FixedTheadPool with 2 threads. This provider is used to tell our JBatch engine exactly which ExecutorSevice to use.

HelidonExecutorServiceProvider
public class HelidonExecutorServiceProvider implements ExecutorServiceProvider {
    @Override
    public ExecutorService getExecutorService() {
        return ThreadPoolSupplier.builder().corePoolSize(2).build().get();
    }
}
Copied

Run the Code

mvn package
java -jar target/helidon-jbatch-example.jar
Copied

Call the Endpoint

curl -X GET http://localhost:8080/batch
Copied

You should receive the following log:

processItem: MyInputRecord: 1
processItem: MyInputRecord: 2
processItem: MyInputRecord: 3
writeItems: [MyOutputRecord: 2, MyOutputRecord: 6]
processItem: MyInputRecord: 4
processItem: MyInputRecord: 5
processItem: MyInputRecord: 6
writeItems: [MyOutputRecord: 10]
processItem: MyInputRecord: 7
processItem: MyInputRecord: 8
processItem: MyInputRecord: 9
writeItems: [MyOutputRecord: 14, MyOutputRecord: 18]
processItem: MyInputRecord: 10
Running inside a batchlet
Copied

and the following result:

{"Started a job with Execution ID: ":1}
Copied

This indicates that the batch job was called and executed successfully.

Check the Status

curl -X GET http://localhost:8080/batch/status/1
Copied

In this example the job ID is 1, but make sure that you enter your specfic job ID in the string.

The results should look something like this:

{"Steps executed":"[step1, step2]","Status":"COMPLETED"}
Copied

Summary

This guide demonstrated how to use Helidon with JBatch even though Helidon is not a full EE container.