Helidon and JBatch

原文はこちら。
The original article was written by Mitia Alexandrov (a software developer at Oracle, working on Project Helidon).
https://medium.com/helidon/helidon-and-jbatch-2826eec478a2

この記事では、EE環境を完全にはサポートしていない環境で、HelidonとJBatchを併用してバッチジョブを実行する方法を紹介します。

Jakarta Batch(JBatch)の公式サイトの説明によると、JBatchの仕様は以下のように定められています。

The Jakarta Batch project describes the XML-based job specification language (JSL), Java programming model, and runtime environment for batch applications for the Java platform.

The specification ties together the Java API and the JSL (XML) allowing a job designer to compose a job in XML from Java application artifacts and conveniently parameterize them with values for an individual job. This structure promotes application reuse of artifacts across different jobs.

The specification allows the flexibility for batch jobs to be scheduled or orchestrated in any number of ways, and stops short of defining any APIs or constructs regarding scheduling or orchestration of multiple or repeated jobs.

(Jakarta Batchプロジェクトでは、XMLベースのジョブ仕様言語(JSL)、Javaプログラミングモデル、およびJavaプラットフォーム用のバッチアプリケーション用ランタイム環境を説明しています。

この仕様は、Java APIとJSL(XML)を結びつけることで、ジョブ設計者がJavaアプリケーションの成果物からXMLでジョブを構成し、個々のジョブ用の値でそれらを便利にパラメータ化できるようにしています。この構造により、異なるジョブ間でのアプリケーションの成果物の再利用を促進します。

この仕様では、バッチジョブのスケジューリングやオーケストレーションの方法に柔軟性を持たせており、複数のジョブや繰り返されるジョブのスケジューリングやオーケストレーションに関するAPIや構造を定義することは控えています。)

https://projects.eclipse.org/projects/ee4j.batch

JBatchの用途は非常に多岐にわたります。実質的にエンタープライズ(EE)の世界では、何百万ものサーバーで何百万ものバッチジョブが実行されています。JBatch仕様は、このようなタイプのタスクを、Java/Jakarta EEの世界のエンタープライズ・ソリューション間で可搬性を持たせるために作成されました。

そして、これは単なる仕様であり、完全な実装ではありません。各ベンダーは独自の実装を提供しなければなりませんが、仕様自体は独立したものではありません。非常に具体的で、例えばJTAやJPAなどの他の仕様に大きく依存しています。つまり、JBatchジョブを実行したいのであれば、完全なEE仕様をサポートするEnterprise Serverが必要になります。

しかし、すべてがマイクロサービスに移行し、そこでは起動時間とリソース消費が重要です。そのため、EE仕様の完全なサポートは事実上不可能です。でもそうでしょうか?

例えばHelidonは、Nettyを中心に構築されており、Jakartaの仕様を完全にはサポートしていませんが、マイクロサービスの世界では申し分ありません。

もし、EE環境を完全にはサポートしていない環境でバッチジョブを実行する必要があるとしたら?実はHelidonには、そのためのソリューションがあるのです。

説明のためのちょっとしたサンプルを作ってみましょう。

Let’s create a small example demonstrating this

Helidonは完全なEEコンテナではないため、どうすればjBatchを利用すればよいのでしょうか。これには、JBatchのいわゆるスタンドアロン(SE)実装の1つを使用します。これは最も一般的な方法とはいえませんが、今回の場合には有効な方法です。この例では、IBM JBatchの実装を使用します。

<dependency>            
     <groupId>com.ibm.jbatch</groupId>                 
     <artifactId>com.ibm.jbatch.container</artifactId>              
     <version>1.0.3</version>        
</dependency>

JPAはデフォルトでは利用できないため、組み込みDBの derby も必要です。

<dependency>            
     <groupId>org.apache.derby</groupId>                 
     <artifactId>derby</artifactId>            
     <version>10.13.1.1</version>        
</dependency>

ではジョブを追加していきましょう。このサンプルでは、以下の  MyItemReaderMyItemProcessor、そしてMyItemWriterを使います。JBatchの可能なすべての使用法を示すために、1つのMyBatchletも使用します。

ここでは、入力情報のユニットを作成します。

public class MyInputRecord {
    private int id;

    public MyInputRecord() {
    }

    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;
    }
}

出力情報の単位である MyOutputRecord は同じように見えます。

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", ",");
    }

    @Override
    public MyInputRecord readItem() {
        if (tokens.hasMoreTokens()) {
            return new MyInputRecord(Integer
                  .valueOf(tokens.nextToken()));
        }
        return null;
    }
}

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);
    }
}

そしてMyItemWriterは結果を出力します。

public class MyItemWriter extends AbstractItemWriter {

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

最後に、ただ完了するだけのMyBatchletです。

public class MyBatchlet extends AbstractBatchlet {

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

        return "COMPLETED";
    }

}

これらをすべてジョブ記述子のxmlファイルに記述します。

<?xml version="1.0" encoding="UTF-8"?>

<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.MyItemReader"/>
            <writer ref="io.helidon.jbatch.example.jobs.MyItemWriter"/>
        </chunk>    
    </step>
    <step id="step2" >
        <batchlet ref="io.helidon.jbatch.example.jobs.MyBatchlet"/>
    </step>
</job>

ご覧のように、2ステップのジョブができあがります。最初のステップでは、MyItemReaderMyItemProcessor、そしてMyItemWriterを使い、最後のステップではMyBatchletを使っています。

また、ここでEEとスタンドアロンの実装の大きな違いがわかります。refプロパティでは、io.helidon.jbatch.example.jobs.MyItemReader のように完全修飾名を指定する必要があります。そうしないと動作しません。

では、上記のジョブを起動するエンドポイントを作りましょう。

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

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

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

        JobOperator jobOperator = getJobOperator();
        Long executionId = 
                 jobOperator.start("myJob", new Properties();
        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();
    }
}

HelidonはJBatchに対して、スタンドアロン(SE)モードで実行することを指定します。また、HelidonExecutorServiceProvider を登録しますが、これは以下のように非常に小さなものです。

public class HelidonExecutorServiceProvider implements ExecutorServiceProvider {
    @Override
    public ExecutorService getExecutorService() {
        return Executors.newFixedThreadPool(2);
    }
}

この例では、2つのスレッドを持つFixedTheadPoolのような非常に小さなものでOKです。このプロバイダーを使って、JBatch エンジンにどの ExecutorSevice を使用するかを正確に伝えます。

これで完成です。

Let’s run the code

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

エンドポイントを呼び出します。

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

以下のようなログが出力されます。

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

そして、以下の結果が得られます。

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

この結果から、バッチジョブの呼び出しおよび実行が成功したことがわかります。

Conclusion

これで、Helidonが完全なEEコンテナでなくてもJBatchを利用できることがおわかりいただけたかと思います。

このコードでもっと遊んでみたいという方は、以下のリポジトリからどうぞ。

Helidon + jBatch
https://github.com/dalexandrov/helidon-jbatch

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Google フォト

Google アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中