今年もSpring Advent Calendarが始まりました。一日目の今日は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が必要です。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example.jsr352</groupId> <artifactId>hello-jsr352-spring</artifactId> <version>1.0.0-SNAPSHOT</version> <repositories> <repository> <snapshots> <enabled>false</enabled> </snapshots> <id>spring-milestones</id> <name>Spring Milestones</name> <url>http://repo.spring.io/milestone</url> </repository> </repositories> <build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>2.5.1</version> <configuration> <source>1.7</source> <target>1.7</target> </configuration> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>3.2.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>3.2.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>3.2.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.batch</groupId> <artifactId>spring-batch-core</artifactId> <version>3.0.0.M2</version> </dependency> <dependency> <groupId>org.springframework.batch</groupId> <artifactId>spring-batch-infrastructure</artifactId> <version>3.0.0.M2</version> </dependency> <dependency> <groupId>commons-dbcp</groupId> <artifactId>commons-dbcp</artifactId> <version>1.4</version> </dependency> <dependency> <groupId>javax.inject</groupId> <artifactId>javax.inject</artifactId> <version>1</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>1.7.5</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.5</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.0.13</version> </dependency> <dependency> <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> <version>2.2.9</version> </dependency> </dependencies> </project>
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
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:batch="http://www.springframework.org/schema/batch" xsi:schemaLocation="http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="com.example.jsr352.job.hello" /> <job id="samplejob" xmlns="http://xmlns.jcp.org/xml/ns/javaee" version="1.0"> <step id="step1"> <batchlet ref="helloBatchlet"></batchlet> </step> </job> </beans>
SpringのBean定義ファイルの中にJSR-352の
<job>
タグが含まれています。実は<job>
タグだけでも使えるのですが、後々Springのサポートが必要になってくると思うのであえて初めからSpringの設定を入れています。ここでは@Named
によるコンポーネントスキャンを有効にするために<context:component-scan>
を設定しておきます。<job>
のなかに複数の<step>
を定義できます。<step>
の実現には、先ほど挙げた2パターンを選ぶことができます。ここでは<batchlet>
を設定して、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のパッケージは出てこないですね。
サービスのメソッドをトランザクション境界にしてみましょう。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:batch="http://www.springframework.org/schema/batch"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<context:component-scan base-package="com.example.jsr352.job.hello" />
<tx:annotation-driven/>
<job id="samplejob" xmlns="http://xmlns.jcp.org/xml/ns/javaee"
version="1.0">
<step id="step1">
<batchlet ref="helloBatchlet2"></batchlet>
</step>
</job>
</beans>
<tx:annotation-driven />
を追加します。
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<Object> items) throws Exception {
for (Object item : items) {
System.out.println(item);
}
}
}
List<Object>
であることがミソですね。一行ずつ書き込むのではなく、まとまった単位(チャンク)で効率よく書き込めます。
デフォルトのチャンクサイズは10です。
ジョブ定義ファイル
META-INF/batch-jobs/filejob.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:batch="http://www.springframework.org/schema/batch" xsi:schemaLocation="http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="com.example.jsr352.job.file" /> <job id="filejob" xmlns="http://xmlns.jcp.org/xml/ns/javaee" version="1.0"> <step id="step1"> <chunk> <reader ref="fileItemReader" /> <processor ref="fileItemProcessor" /> <writer ref="fileItemWriter" /> </chunk> </step> </job> </beans>
Batchletの場合と大差ないですが、
<chunk>
の中身が<reader>
、<processor>
、<writer>
になります。それぞれ先ほど作成したクラスのBean IDを指定します。チャンクサイズは
<chunk item-count="20">
のように設定できます。
実行してみます。
$ mvn -q exec:java -Dexec.mainClass=com.example.jsr352.EntrryPoint -Dexec.arguments="filejob"
AAA
bbb
CCC
ddd
EEE
fff
GGG
(中略)
SSS
ttt
UUU
vvv
WWW
xxx
YYY
zzz
これも理解しやすいのではないでしょうか。
Spring Batchの機能を使ってみる
jBatchではItemReader
、ItemProcessor
、ItemWriter
のAPIは規定していますが実装はユーザー任せです。
この概念はもともとSpring Batchのものであり、Spring Batchにはもちろん沢山の実装クラスが用意されています。
ここではDBからデータをカーソルで読み込むItemReader
を使ってみます。
ジョブ定義ファイル
まず始めにジョブ定義ファイルから見ていきます。
META-INF/batch-jobs/dbjob.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:batch="http://www.springframework.org/schema/batch" xsi:schemaLocation="http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="com.example.jsr352.job.db" /> <bean id="dbReader" class="org.springframework.batch.item.database.JdbcCursorItemReader"> <property name="sql" value="SELECT name FROM testdata" /> <property name="rowMapper" ref="dbRowMapper" /> <property name="dataSource" ref="dataSource" /> </bean> <job id="dbjob" xmlns="http://xmlns.jcp.org/xml/ns/javaee" version="1.0"> <step id="step1"> <chunk> <reader ref="dbReader" /> <writer ref="dbWriter" /> </chunk> </step> </job> </beans>
Readerは用意されているものを定義するだけです。Spring側でbean定義し、jBatch側で参照できます。
SpringBatchで用意されているReaderは
org.springframework.batch.item.ItemReader
を実装しており、実はjBatchのjavax.batch.api.chunk.ItemReader
を実装していません。なぜ使えるかというと、内部ではjBatchのItemReader
は使っておらず、Spring BatchのItemReader
を使っており、jBatchのItemReader
はAdaptorを介してSpring BacthのItemReader
として扱われるからです。DbRowMapper
JdbcCursorItemReader
が使用するRowMapper
を作成します。ResultSet
をオブジェクトに変換するクラスで、当然Springのクラスです。package com.example.jsr352.job.db; import java.sql.ResultSet; import java.sql.SQLException; import javax.inject.Named; import org.springframework.jdbc.core.RowMapper; @Named public class DbRowMapper implements RowMapper<String> { @Override public String mapRow(ResultSet rs, int rowNum) throws SQLException { return rs.getString(1); } }
ItemWriter
ItemProcessor
は省略して、変換なしでItemWriter
に渡します。ItemWriter
も超手抜き。
package com.example.jsr352.job.db;
import java.util.List;
import javax.batch.api.chunk.AbstractItemWriter;
import javax.inject.Named;
@Named
public class DbWriter extends AbstractItemWriter {
@Override
public void writeItems(List<Object> items) throws Exception {
System.out.println(items);
}
}
DDL
Spring BatchはデフォルトでHSQLを使用し、起動時にクラスパスのorg/springframework/batch/core/schema-hsqldb.sqlを読み込みます。もちろん変更可能ですが、今回はここにスクリプトを書きます。超適当な例を貼っておきます。
CREATE TABLE testdata(name VARCHAR(10));
INSERT INTO testdata(name) VALUES('a');
INSERT INTO testdata(name) VALUES('b');
INSERT INTO testdata(name) VALUES('c');
(中略)
INSERT INTO testdata(name) VALUES('x');
INSERT INTO testdata(name) VALUES('y');
INSERT INTO testdata(name) VALUES('z');
実行してみます。
$ mvn -q exec:java -Dexec.mainClass=com.example.jsr352.EntrryPoint -Dexec.arguments="dbjob"
start jobId=dbjob
[a, b, c, d, e, f, g, h, i, j]
[k, l, m, n, o, p, q, e, s, t]
[u, v, w, x, y, z]
チャンク数がわかりますね。
ということでjBatch(JSR-352) on Spring Batch3の基本的な使い方を説明してきましたが、
- jBatchもSpring Batchの力を借りれば使えそう
- Springはやはり基盤として優秀
ということがお分かりいただけたのではないでしょうか。
この記事のソースコードはこちら。
jBatchに関しては上妻さんのSlideShareが詳しいです。
またSpring Batch3連携としてはこのサンプルが参考になりました。
またの機会にエラーハンドリング、リスタート、リトライ、マルチスレッド処理等扱ってみたいと思います。
Spring Advent Calendarではあまり本には載っていないマニアックな内容を紹介していく予定なのでチェックよろしくです。
明日は@eiryuさんですね。よろしくです。