--- 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 items) throws Exception { for (Object item : items) { System.out.println(item); } } } `List`であることがミソですね。一行ずつ書き込むのではなく、まとまった単位(チャンク)で効率よく書き込めます。 デフォルトのチャンクサイズは10です。 #### ジョブ定義ファイル * META-INF/batch-jobs/filejob.xml Batchletの場合と大差ないですが、``の中身が``、``、``になります。それぞれ先ほど作成したクラスのBean IDを指定します。 チャンクサイズは``のように設定できます。 実行してみます。 $ 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 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 { @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 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はやはり基盤として優秀 ということがお分かりいただけたのではないでしょうか。 この記事のソースコードは[こちら](https://github.com/making/hello-jsr352-spring)。 jBatchに関しては[上妻さんのSlideShare](http://www.slideshare.net/agetsuma/glassfish-jp20131-agetsuma)が詳しいです。 またSpring Batch3連携としては[このサンプル](https://github.com/mminella/jsr352-springbatch-and-you)が参考になりました。 またの機会にエラーハンドリング、リスタート、リトライ、マルチスレッド処理等扱ってみたいと思います。 [Spring Advent Calendar](http://www.adventar.org/calendars/153)ではあまり本には載っていないマニアックな内容を紹介していく予定なのでチェックよろしくです。 明日は[@eiryu](https://twitter.com/eiryu)さんですね。よろしくです。 [1]: http://www.adventar.org/calendars/153