---
title: 【Spring Advent Calendar 2013 1日目 Javaバッチフレームワークの決定版。jBatch(JSR-352) on Spring Batch 3 spadc13
tags: []
categories: ["Programming", "Java", "Spring", "AdventCalendar", "2013"]
date: 2013-12-01T02:40:39Z
updated: 2013-12-01T02:40:39Z
---
今年も[Spring Advent Calendar][1]が始まりました。一日目の今日はJSR-352 on Spring Batch 3について話します。ハッシュタグは#spadc13でお願いします。
Java EE7でjBatchというバッチ処理(JSR-352)がサポートされました。
内容はというとSpring Batchの劣○版ですね。はい。
APIは決まりましたが、実際にバッチ処理を書くためのサポートがほとんどないです。
ただ、強いてメリットを上げるならAPIがシンプルで学習コストが低そうに見えることでしょうか。
処理方式は大きく分けて2つ
* 単純に処理を書き下していくBatchletパターン
* ETL処理をそれぞれ分けたItemReader/ItemProcessor/ItemWriterパターン
前者はコマンドパターン、後者をETLパターンとでも言いますか。
JSR-352の元になったのはSpring Batchですが、Spring Batchはバージョン3からJSR-352に(半分ほど)対応します。
Spring Batch 3を使うとJava標準のAPIでバッチ処理を書けるようになりますし、バッチ処理をAPサーバー上じゃなくてもスタンドアローンで実行できるようになります(もちろんAPサーバー上でもOK)。さらにはSpring Batchで用意されている様々なコンポーネント群(File連携、DB連携、MQ連携、Validationなど)を利用できるようになります。
これはJavaのバッチフレームワークの決定版ではないかと思います。
今回はSpring Batch3を使ったJSR-352のサンプルをいくつかあげてみます。
### プロジェクト設定
* pom.xml
現時点では3はまだ正式リリース前なのでSpring Batchは3.0.0.M2を使用します。またデフォルト設定ではcommons-dbcpとhsqldbが必要です。
4.0.0
com.example.jsr352
hello-jsr352-spring
1.0.0-SNAPSHOT
false
spring-milestones
Spring Milestones
http://repo.spring.io/milestone
maven-compiler-plugin
2.5.1
1.7
1.7
org.springframework
spring-context
3.2.5.RELEASE
org.springframework
spring-tx
3.2.5.RELEASE
org.springframework
spring-jdbc
3.2.5.RELEASE
org.springframework.batch
spring-batch-core
3.0.0.M2
org.springframework.batch
spring-batch-infrastructure
3.0.0.M2
commons-dbcp
commons-dbcp
1.4
javax.inject
javax.inject
1
org.slf4j
jcl-over-slf4j
1.7.5
org.slf4j
slf4j-api
1.7.5
ch.qos.logback
logback-classic
1.0.13
org.hsqldb
hsqldb
2.2.9
### EntryPointの作成
バッチ処理を書く前にバッチをキックするエントリポイントとなるmainクラスを作成します。
package com.example.jsr352;
import javax.batch.operations.JobOperator;
import javax.batch.runtime.BatchRuntime;
public class EntryPoint {
public static void main(String[] args) {
String jobId;
if (args.length > 0) {
jobId = args[0];
} else {
jobId = "hellojob";
}
JobOperator jobOperator = BatchRuntime.getJobOperator();
System.out.println("start jobId=" + jobId);
long executionId = jobOperator.start(jobId, null);
System.out.println("executionId=" + executionId);
}
}
`JobOperator`を取得して、`start`メソッドに対象のジョブIDを渡すだけです。簡単。
### 最も簡単なBatchletの作成
最初はBatchlet方式のサンプルを説明します。
#### Batcheletクラス
package com.example.jsr352.job.hello;
import javax.batch.api.AbstractBatchlet;
import javax.inject.Named;
@Named
public class HelloBatchlet extends AbstractBatchlet {
@Override
public String process() throws Exception {
System.out.println("hello world");
return null;
}
}
簡単ですね。`process`メソッドに処理を書き下すだけです。返り値は終了状態ですが、ここでは特に使用しないので`null`を返します。
ここまでSpringは現れません。
#### ジョブ定義ファイル
クラスパス以下ののMETA-INF/batch-jobs/[jobId].xmlが読み込まれます。ここではjobIdをhellojobとします。
* META-INF/batch-jobs/hellojob.xml
SpringのBean定義ファイルの中にJSR-352の``タグが含まれています。実は``タグだけでも使えるのですが、後々Springのサポートが必要になってくると思うのであえて初めからSpringの設定を入れています。ここでは`@Named`によるコンポーネントスキャンを有効にするために``を設定しておきます。
``のなかに複数の``を定義できます。``の実現には、先ほど挙げた2パターンを選ぶことができます。ここでは``を設定して、Batchlet方式を選び`ref`にBean IDを指定します。`HelloBatchlet`に`@Named`がついているのでデフォルトでは`helloBatchlet`がBean IDになります。`@Named`をつけない場合はFQCNを指定すればよいです。
先ほどの`EntryPoint`の引数にjobIdとして`hellojob`を渡して実行しましょう。
$ mvn -q exec:java -Dexec.mainClass=com.example.jsr352.EntryPoint -Dexec.arguments="hellojob"
start jobId=hellojob
hello world
executionId=0
簡単ですね。
次はDIをつかってロジック(?)を外だしてみます。
#### サービスクラス
package com.example.jsr352.job.hello;
import javax.inject.Named;
@Named
public class HelloService {
public String hello(String name) {
return "hello " + name;
}
}
#### Batchletクラス
package com.example.jsr352.job.hello;
import javax.batch.api.AbstractBatchlet;
import javax.inject.Inject;
import javax.inject.Named;
@Named
public class HelloBatchlet extends AbstractBatchlet {
@Inject
HelloService helloService;
@Override
public String process() throws Exception {
System.out.println(helloService.hello("world"));
return null;
}
}
実行結果はさっきと同じです。このサンプルまでである程度出来そうなことが想像できるのではないでしょうか。ここまでプログラム中にSpringのパッケージは出てこないですね。
サービスのメソッドをトランザクション境界にしてみましょう。
``を追加します。
package com.example.jsr352.job.hello;
import javax.inject.Named;
import org.springframework.transaction.annotation.Transactional;
@Named
public class HelloService {
@Transactional
public String hello(String name) {
return "hello " + name;
}
}
サービスのメソッドまたはクラスに`@Transactonal`をつけます。ここでSpringのアノテーションがでてきました。現状SpringではJTA1.2に対応していないので、Springのアノテーションを使わざるをえないですが、Spring4になるとJTAの`@Tranasctional`が使用できるはずなので、ここも将来的にはJava標準APIだけで書けるでしょう(別にSpringのアノテーションでも良いと思うが)。
ちなみにバッチ起動時に`DataSource`や`TransactionManager`が作られています。spring-batch-core-3.0.0.M2.jar内のbaseContext.xmlに色々定義されています。
### ItemReader/ItemProcessor/ItemWriter方式の簡単なジョブを作成
次にETLパターンを試します。
* Extract(抽出) -> ItemReader
* Transform(変換) -> ItemProcessor
* Load(書出) -> ItemWriter
が対応します。
簡単な例としてファイルの内容を読みこみ、偶数行は小文字に、奇数行は大文字の変換して、標準出力に書き込む例を実装してみます。
### 入力ファイル
* input.txt
aaa
bbb
ccc
ddd
eee
fff
ggg
(中略)
sss
ttt
uuu
vvv
www
xxx
yyy
zzz
* LineItemクラス
ファイル行に対応したオブジェクトを作っておきます。
package com.example.jsr352.job.file;
public class LineItem {
private final int lineNumber;
private final String content;
public LineItem(int lineNumber, String content) {
this.lineNumber = lineNumber;
this.content = content;
}
public int getLineNumber() {
return lineNumber;
}
public String getContent() {
return content;
}
@Override
public String toString() {
return "LineItem [lineNumber=" + lineNumber + ", content=" + content
+ "]";
}
}
#### ItemReader
package com.example.jsr352.job.file;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.Serializable;
import javax.batch.api.chunk.AbstractItemReader;
import javax.inject.Named;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Named
public class FileItemReader extends AbstractItemReader {
private static final Logger logger = LoggerFactory
.getLogger(FileItemReader.class);
BufferedReader reader;
int lineNumber = 0;
@Override
public void open(Serializable checkpoint) throws Exception {
logger.debug("open");
reader = new BufferedReader(new FileReader("input.txt"));
}
@Override
public Object readItem() throws Exception {
String content = reader.readLine();
return content == null ? null : new LineItem(++lineNumber, content);
}
@Override
public void close() throws Exception {
logger.debug("close");
if (reader != null) {
reader.close();
}
}
}
`open`でリソースを開き、`readItem`でリソースを任意の単位で読み込み行オブジェクトを作成します(StringでもOK)。`close`でリソースを閉じます。
`readItem`の返り値が`null`の場合に読み込みを終了します。
#### ItemProcessor
ItemReaderの`readItem`で作成したオブジェクトが`processItem`に渡ってきます。`Object`型なのはきもいですが、気にしないでおきましょう。
package com.example.jsr352.job.file;
import javax.batch.api.chunk.ItemProcessor;
import javax.inject.Named;
@Named
public class FileItemProcessor implements ItemProcessor {
@Override
public Object processItem(Object item) throws Exception {
LineItem lineItem = (LineItem) item;
String content = lineItem.getContent();
return (lineItem.getLineNumber() % 2 == 0) ? content.toLowerCase()
: content.toUpperCase();
}
}
ここで`LineItem`オブジェクトが`String`に変換されます。
#### ItemWriter
ItemProcessorで変換されたオブジェクトが**チャンク単位**で渡ってきます。
package com.example.jsr352.job.file;
import java.util.List;
import javax.batch.api.chunk.AbstractItemWriter;
import javax.inject.Named;
@Named
public class FileItemWriter extends AbstractItemWriter {
@Override
public void writeItems(List