---
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が必要です。

        <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はやはり基盤として優秀

ということがお分かりいただけたのではないでしょうか。

この記事のソースコードは[こちら](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
