バッチ処理も実はAPI
環境作成
しかしJakartaの標準的な方法は『Cron』が起動し『API』を叩きます。
通常のAPIと違うのは『非同期』という点だけです。
<!-- バッチ処理 -->
<dependency>
<groupId>jakarta.platform</groupId>
<artifactId>jakarta.jakartaee-api</artifactId>
<version>10.0.0</version>
<scope>provided</scope>
</dependency>
パッケージ『api.batch』に作ります。
package api.batch;
import java.util.Properties;
import jakarta.batch.operations.JobOperator;
import jakarta.batch.runtime.BatchRuntime;
import jakarta.enterprise.context.RequestScoped;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Response;
@Path("/batch")
@RequestScoped
public class BatchResource {
@POST
@Path("/start")
public Response startJob() {
// バッチ操作用のオブジェクトを取得
JobOperator jobOperator = BatchRuntime.getJobOperator();
// my-job.xml で定義したバッチを非同期で開始
long executionId = jobOperator.start("my-job", new Properties());
// ジョブIDを返して即座にレスポンス(同期実行でクライアントを待たせない)
return Response.accepted("Job started with execution ID: " + executionId).build();
}
}
※Path『api』は《@ApplicationPath("/api")》で指定しています 流れとしては、まず『JobOperator』型のインスタンスを作成します。
そしてインスタンスに対し『start』メソッドを発行します。
非同期処理なので、即座にレスポンスが返却されます。 ここで『start』メソッドの第1引数に注目して下さい。
文字列『my-job』とは何でしょう?
実はこれ『src/main/resources/META-INF/batch-jobs/my-job.xml』の省略形何です。
※「my-job」以外は《パス》《拡張子》全て固定となります
<?xml version="1.0" encoding="UTF-8"?>
<job id="my-job" xmlns="https://jakarta.ee/xml/ns/jakartaee" version="2.0">
<step id="batch_1">
<batchlet ref="myBatchJobFirst"/>
</step>
</job>
batchletタグの『ref属性』に指定したのが発火するクラス名となります。
サンプルでは上記XMLファイルを元に解説していきますが、実はこのXMLは柔軟な設定が可能です。
XMLファイルの詳細設定
それではシチュエーションに合わせたサンプルをご紹介します。
<?xml version="1.0" encoding="UTF-8"?>
<job id="my-job" xmlns="https://jakarta.ee/xml/ns/jakartaee" version="2.0">
<!-- ① 最初に実行される処理 -->
<step id="step1" next="step2">
<batchlet ref="downloadBatchlet"/>
</step>
<!-- ② 2番目に実行される処理 -->
<step id="step2" next="step3">
<batchlet ref="processDataBatchlet"/>
</step>
<!-- ③ 最後に実行される処理 -->
<step id="step3">
<batchlet ref="uploadBatchlet"/>
</step>
</job>
この場合[downloadBatchlet] > [processDataBatchlet] > [uploadBatchlet]で処理クラスが順番に発火します。
<?xml version="1.0" encoding="UTF-8"?>
<job id="my-job" xmlns="https://jakarta.ee/xml/ns/jakartaee" version="2.0">
<!-- ① ファイル取得処理 -->
<step id="downloadStep">
<batchlet ref="downloadBatchlet"/>
<!-- 正常終了(COMPLETED)なら加工処理へ -->
<next on="COMPLETED" to="processStep"/>
<!-- エラー(FAILED)ならリカバリ処理へ -->
<next on="FAILED" to="errorNotificationStep"/>
</step>
<!-- ② 正常時の加工処理 -->
<step id="processStep">
<batchlet ref="processDataBatchlet"/>
</step>
<!-- ③ エラー時の通知処理 -->
<step id="errorNotificationStep">
<batchlet ref="sendErrorMailBatchlet"/>
</step>
</job>
また失敗の場合は『sendErrorMailBatchlet』クラスが発火します。
バッチ実行クラス作成
場所は何処でも良いですが、エンドポイントのパッケージを『api.batch』としたので、
実行クラスは『api.job』とし『MyBatchJobFirst』を作成します。
package api.job;
import jakarta.batch.api.AbstractBatchlet;
import jakarta.batch.runtime.BatchStatus;
import jakarta.enterprise.context.Dependent;
import jakarta.inject.Named;
@Named("myBatchJobFirst")
@Dependent
public class MyBatchJobFirst extends AbstractBatchlet {
@Override
public String process() throws Exception {
// ここにバッチのメイン処理を記述
System.out.println("myBatchJobFirst の処理を実行中...");
// 正常終了時は "COMPLETED"(または独自の終了ステータス文字列)を返す
return BatchStatus.COMPLETED.toString();
}
@Override
public void stop() throws Exception {
// 非同期キャンセル要求時のクリーンアップ処理(任意)
System.out.println("処理の中断要求を受け取りました。");
}
}
URLは『http://localhost:8080/sample-pj/api/batch/start』
パラメータを渡す方法
でもちょっと待ってください。
折角『CRON』から直接APIを叩けるのに、動的にパラメータを付与するとなると『Shell』が必要になります。
本当にそのパラメータは必要ですか? バッチは本来『夜間データ整理』等の単純な処理を担当するものです。
必要なパラメータってせいぜい日付くらいで、しかも日付は発火した後にJAVA側で普通に取得できます。 ただし、例えば大量のデータがあり、午前1時はDBの奇数行の処理、午前5時は偶数みたいな場合には、
それぞれの『CRON』にパラメータ『1』や『5』を付加するのはありかもしれませんね。
curl -X POST "http://localhost:8080/sample-pj/api/batch/start?name_cd=foo"
package api.batch;
import java.util.Properties;
import jakarta.batch.operations.JobOperator;
import jakarta.batch.runtime.BatchRuntime;
import jakarta.enterprise.context.RequestScoped;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Response;
@Path("/batch")
@RequestScoped
public class BatchResource {
@POST
@Path("/start")
public Response startJob(@QueryParam("name_cd") String nameCd) {
// JobOperator に渡す Properties オブジェクトを作成
Properties jobParameters = new Properties();
// パラメータが存在する場合のみ設定(未指定時のデフォルト動作を担保するため)
if (nameCd != null && !nameCdnameCd.isEmpty()) {
jobParameters.setProperty("name_cd", nameCd);
}
// バッチ操作用のオブジェクトを取得
JobOperator jobOperator = BatchRuntime.getJobOperator();
// my-job.xml で定義したバッチを非同期で開始
long executionId = jobOperator.start("my-job", jobParameters);
// ジョブIDを返して即座にレスポンス(同期実行でクライアントを待たせない)
return Response.accepted("Job started with execution ID: " + executionId).build();
}
}
『String nameCd』は、JAVAコード内で扱う変数名です。
『jobParameters』型のインスタンスを生成し、値をセットします。
バッチ実行メソッド『jobOperator.start』の第2引数に渡します。
<?xml version="1.0" encoding="UTF-8"?>
<job id="my-job" xmlns="https://jakarta.ee/xml/ns/jakartaee" version="2.0">
<step id="batch_1">
<batchlet ref="myBatchJobFirst">
<properties>
<property name="name_cd" value="#{jobParameters['name_cd']}"/>
</properties>
</batchlet>
</step>
</job>
したがって、該当ファイルにもパラメータをセットする必要があります。
このクラスでパラメータを取得しなければ意味がありませんよね。
package com.example.batch;
import jakarta.batch.api.AbstractBatchlet;
import jakarta.batch.api.BatchProperty;
import jakarta.batch.runtime.BatchStatus;
import jakarta.enterprise.context.Dependent;
import jakarta.inject.Inject;
import jakarta.inject.Named;
@Named("myBatchJobFirst")
@Dependent
public class MyBatchJobFirst extends AbstractBatchlet {
@Inject
@BatchProperty(name = "name_cd")
private String nameCd;
@Override
public String process() throws Exception {
// ここにバッチのメイン処理を記述
System.out.println("myBatchJobFirst の処理を実行中...");
// 正常終了時は "COMPLETED"(または独自の終了ステータス文字列)を返す
return BatchStatus.COMPLETED.toString();
}
@Override
public void stop() throws Exception {
// 非同期キャンセル要求時のクリーンアップ処理(任意)
System.out.println("処理の中断要求を受け取りました。");
}
}
本来のJakarta EEの思想からは外れますが『XML』ファイルにパラメータを記述しない方法もあります。
先ずは『@Inject』の部分を以下の様に変更します。
private JobContext jobContext;
JobOperator jobOperator = BatchRuntime.getJobOperator();
Properties jobParams = jobOperator.getParameters(executionId);
String nameCd = jobParams.getProperty("name_cd");
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.core.MediaType;
~ 省略 ~
@POST
@Path("/start")
@Consumes(MediaType.APPLICATION_JSON)
public Response startJob(Map<String, String> requestParams) {
Properties jobParameters = new Properties();
// パラメータが存在する場合のみ設定(未指定時のデフォルト動作を担保するため)
if (requestParams != null ) {
requestParams.forEach((key, value) -> {
if (value != null) {
jobParameters.setProperty(key, value);
}
});
}
~ 省略 ~
Batchlet方式によるバッチ処理
後述する『Chunk方式』とは違い、Batchlet方式は1つのタスクを単一の処理として実行する方式です。
因みに前述のXMLファイルの設定方法は『Batchlet方式』となります。
サンプルコードは以下となります。
package api.job;
import jakarta.batch.api.AbstractBatchlet;
import jakarta.batch.runtime.BatchStatus;
import jakarta.enterprise.context.Dependent;
import jakarta.inject.Named;
@Named("myBatchJobFirst")
@Dependent
public class MyBatchJobFirst extends AbstractBatchlet {
private boolean stoped = false;
@Override
public String process() throws Exception {
// 正常終了時は "COMPLETED"(または独自の終了ステータス文字列)を返す
return this.stoped ? BatchStatus.STOPPED.toString() : BatchStatus.COMPLETED.toString();
}
@Override
public void stop() throws Exception {
// ジョブの中断要求があった場合の処理(必要に応じて実装)
this.stoped = true;
}
}
『stop()』メソッドは任意ですが、設置する場合はフラグで状態を判断できる様にします。
サンプルでは、フラグが立っている場合は《STOPPED》を返却する様にしています。
[COMPLETED][STOPPED]の他に、以下定数が用意されています。
・BatchStatus.STARTED.toString()
・BatchStatus.STOPPING.toString()
・BatchStatus.COMPLETED.toString()
・BatchStatus.FAILED.toString()
・BatchStatus.ABANDONED.toString()
・BatchStatus.FAILING.toString()
Chunk方式によるバッチ処理
全件を一括処理するのではなく、設定した件数(例:1000件)毎にトランザクションをコミットするため、
メモリの消費を抑えつつ、万が一途中でエラーが発生しても途中経過を保持・リカバリしやすい特徴があります。
JAVAコード的な特徴としては、3つのクラスを作成してバッチ処理します。 基本的な構造([エンドポイント] > [XMLファイル] > [Java処理])は同じですが、少し勝手が違うのでエンドポイントからの説明となります。
package api.batch;
import jakarta.batch.operations.JobOperator;
import jakarta.batch.runtime.BatchRuntime;
import jakarta.enterprise.context.RequestScoped;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Response;
@Path("/batch")
@RequestScoped
public class BatchResource {
@POST
@Path("/chunk")
public Response chunkJob() {
// バッチ操作用のオブジェクトを取得
JobOperator jobOperator = BatchRuntime.getJobOperator();
// chunk-job.xml で定義したバッチを非同期で開始
long executionId = jobOperator.start("chunk-job", null);
// ジョブIDを返して即座にレスポンス(同期実行でクライアントを待たせない)
return Response.accepted("Job started with execution ID: " + executionId).build();
}
}
次にXMLファイルを作成します。
<?xml version="1.0" encoding="UTF-8"?>
<job id="chunk-job" xmlns="https://jakarta.ee/xml/ns/jakartaee" version="2.0">
<step id="chunkStep">
<!-- item-count: 3件ごとにコミットを行う -->
<chunk item-count="3">
<reader ref="myReader" />
<processor ref="myProcessor" />
<writer ref="myWriter" />
</chunk>
</step>
</job>
また以下役割を持つクラスを宣言します。
・processor 処理担当クラス myProcessor
・writer 書き込み担当クラス myWriter
package api.job;
import jakarta.batch.api.chunk.AbstractItemReader;
import jakarta.enterprise.context.Dependent;
import jakarta.inject.Named;
@Named("myReader")
@Dependent
class MyReader extends AbstractItemReader {
private int count = 0;
@Override
public Object readItem() throws Exception {
count++;
// 例: 10件まで読み込む
if (count <= 10) {
System.out.println("1件読込");
return "Item" + count;
}
// これ以上データがない場合は null を返す
return null;
}
}
1件単位で読込み、次のクラスへ処理を渡します。
※本来はここでDB一覧のデータを読込んで『for文』で回すイメージです。
package api.job;
import jakarta.batch.api.chunk.ItemProcessor;
import jakarta.enterprise.context.Dependent;
import jakarta.inject.Named;
@Named("myProcessor")
@Dependent
class MyProcessor implements ItemProcessor {
@Override
public Object processItem(Object item) throws Exception {
String input = (String) item;
// データを加工して返す
System.out.println("データを加工して返す");
return input.toUpperCase();
}
}
サンプルでは単純に渡されたデータを大文字変換しています。 さてここで問題です。
『return』はどのクラスに返却されるでしょうか?
正解は『3件』溜まるまでは『myReader』へ返却されます。
そしてこの『3』は、XMLファイルで設定した『chunk item-count="3"』となります。
package api.job;
import java.util.List;
import jakarta.batch.api.chunk.AbstractItemWriter;
import jakarta.enterprise.context.Dependent;
import jakarta.inject.Named;
@Named("myWriter")
@Dependent
class MyWriter extends AbstractItemWriter {
@Override
public void writeItems(List<Object> items) throws Exception {
// まとめて一括書き込み(例: DBバッチインサートやファイル追記)
System.out.println("データ数:" + items.size());
for (Object item : items) {
System.out.println("Writing: " + item);
}
}
}
ここでは最終的な結果を処理します。 具体的には、DB登録や更新等を処理します。
※『for文』内で登録処理を記述します
Batchlet方式による管理は、以下の様な通常の設定となるので特に意識せずにコードを記述できます。
@Transactional(value = Transactional.TxType.REQUIRED, rollbackOn = {Exception.class})
Chunkの場合は『Chunkコンテナ』が自動的にトランザクションを発行するためバッティングします。
手動でロールバックするには、単純に『RuntimeException』や『Exception』を投げます。





URLを見て「おや」と思った方いませんか?
@ApplicationPath("/api") またPostManが手元にない場合は、Windowsのターミナルから叩いても連携できますよ。そう『api』なんて設定何処でと思っていませんか。
この設定は通常のAPIの章で作成した『RestApiService』クラスに付与した以下アノテーションです。
curl -X POST "http://localhost:8080/sample-pj/api/batch/start" -H "Content-Type: application/json" -d "{}"