Spring Batchの"Getting Started"を読む
引き続きSpringキャンペーン中につき。今度はSpring Batchです。あまり深入りはしないつもりですが・・・。
まずは例によって"Getting Started"を読んでみましょう。ビルドはMavenでする前提で、不要と判断した箇所は中略しています。原典はSpring公式サイトの"Getting Started - Creating a Batch Service"です(2014/12/29取得)。
* * *
バッチ・サービスをつくる
このガイドでは簡単なバッチ駆動のソリューションを作成する手順を見ていきます。
何が必要か
- だいたい15分くらいの時間。
- お好みのテキスト・エディタやIDE。
- JDKのバージョン1.6かそれ以降。
- Gradleのバージョン1.11以降 もしくは Mavenのバージョン3.0以降。
- Springツール・スイート(STS)があればこのガイドを直接Webページとして閲覧するだけでなく、ガイドからコードをインポートして作業をはじめることもできます。
どうやってこのガイドの内容を学ぶか
〔・・・中略・・・〕
Gradleでビルドする
〔・・・中略・・・〕
Mavenでビルドする
〔・・・中略・・・〕
Create the directory structure
あなたの選んだプロジェクトのディレクトリの中に、次のディレクトリ構成をつくります。例えば、*nixシステムであればmkdir -p src/main/java/hello
コマンドで作成できます:
└── src └── main └── java └── hello
〔プロジェクト・ディレクトリ直下に以下のファイルを作成します。〕
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <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>org.springframework</groupId> <artifactId>gs-batch-processing</artifactId> <version>0.1.0</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.1.10.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-batch</artifactId> </dependency> <dependency> <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> </dependency> </dependencies> <properties> <start-class>hello.Application</start-class> </properties> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> <repositories> <repository> <id>spring-releases</id> <url>https://repo.spring.io/libs-release</url> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>spring-releases</id> <url>https://repo.spring.io/libs-release</url> </pluginRepository> </pluginRepositories> </project>
〔・・・中略・・・〕
Springツール・スイートでビルドする
〔・・・中略・・・〕
ビジネス・クラスをつくる
それでは、入力データと出力データの形式について検討し、データ行をあらわすコードを書きましょう。
src/main/java/hello/Person.java
package hello; public class Person { private String lastName; private String firstName; public Person() { } public Person(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } @Override public String toString() { return "firstName: " + firstName + ", lastName: " + lastName; } }
Person
クラスはファーストネームとラストネーム、あるいはそのいずれかをコンストラクタに渡すか、あるいは無名コンストラクタでインスタンスをつくった後でセッター・メソッドを介して設定することで初期化することができます。
中間処理プログラムをつくる
バッチ処理における共通のパラダイムは「データを取込み、それを変換し、どこかに出力する」というものです。ここではPerson
クラスのファーストネーム/ラストネームを大文字化するだけの簡単な変換プログラムをつくります。
src/main/java/hello/PersonItemProcessor.java
package hello; import org.springframework.batch.item.ItemProcessor; public class PersonItemProcessor implements ItemProcessor<Person, Person> { @Override public Person process(final Person person) throws Exception { final String firstName = person.getFirstName().toUpperCase(); final String lastName = person.getLastName().toUpperCase(); final Person transformedPerson = new Person(firstName, lastName); System.out.println("Converting (" + person + ") into (" + transformedPerson + ")"); return transformedPerson; } }
PersonItemProcessor
クラスはSpring Batchの ItemProcessor
インターフェースを実装しています。こうすることで簡単に何らかのプログラム・コードをバッチ・ジョブ──これについてはこのガイドの後の方で見ることになります──に結び付けることができます。インターフェースを確認しましょう。プログラム・コードはPerson
オブジェクトを受け取り、続いてその名前を大文字化したPerson
に変換しています。
NOTE: 入力データと出力データの型が同じである必要はありません。実際のところ、データと読み込んだあとで、アプリケーションのデータ・フロー上しばしば異なるデータ型への変換が発生します。
バッチ・ジョブを組み立てる
さて、バッチ・ジョブを組み立てましょう。Spring Batchでは、開発者がカスタムメイドのコードを書かずに済ませられるよう、多くのユーティリティ・クラスが提供されています。このお陰であなたはビジネス・ロジックに集中することができます。
src/main/java/hello/BatchConfiguration.java
package hello; import javax.sql.DataSource; import org.springframework.batch.core.Job; import org.springframework.batch.core.Step; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; import org.springframework.batch.core.launch.support.RunIdIncrementer; import org.springframework.batch.item.ItemProcessor; import org.springframework.batch.item.ItemReader; import org.springframework.batch.item.ItemWriter; import org.springframework.batch.item.database.BeanPropertyItemSqlParameterSourceProvider; import org.springframework.batch.item.database.JdbcBatchItemWriter; import org.springframework.batch.item.file.FlatFileItemReader; import org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper; import org.springframework.batch.item.file.mapping.DefaultLineMapper; import org.springframework.batch.item.file.transform.DelimitedLineTokenizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import org.springframework.jdbc.core.JdbcTemplate; @Configuration @EnableBatchProcessing public class BatchConfiguration { // tag::readerwriterprocessor[] @Bean public ItemReader<Person> reader() { FlatFileItemReader<Person> reader = new FlatFileItemReader<Person>(); reader.setResource(new ClassPathResource("sample-data.csv")); reader.setLineMapper(new DefaultLineMapper<Person>() {{ setLineTokenizer(new DelimitedLineTokenizer() {{ setNames(new String[] { "firstName", "lastName" }); }}); setFieldSetMapper(new BeanWrapperFieldSetMapper<Person>() {{ setTargetType(Person.class); }}); }}); return reader; } @Bean public ItemProcessor<Person, Person> processor() { return new PersonItemProcessor(); } @Bean public ItemWriter<Person> writer(DataSource dataSource) { JdbcBatchItemWriter<Person> writer = new JdbcBatchItemWriter<Person>(); writer.setItemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<Person>()); writer.setSql("INSERT INTO people (first_name, last_name) VALUES (:firstName, :lastName)"); writer.setDataSource(dataSource); return writer; } // end::readerwriterprocessor[] // tag::jobstep[] @Bean public Job importUserJob(JobBuilderFactory jobs, Step s1) { return jobs.get("importUserJob") .incrementer(new RunIdIncrementer()) .flow(s1) .end() .build(); } @Bean public Step step1(StepBuilderFactory stepBuilderFactory, ItemReader<Person> reader, ItemWriter<Person> writer, ItemProcessor<Person, Person> processor) { return stepBuilderFactory.get("step1") .<Person, Person> chunk(10) .reader(reader) .processor(processor) .writer(writer) .build(); } // end::jobstep[] @Bean public JdbcTemplate jdbcTemplate(DataSource dataSource) { return new JdbcTemplate(dataSource); } }
まずはじめに、@EnableBatchProcessing
アノテーションにより多くの重要なビーン──ジョブ実行を支援し、あなたの苦労を軽減させてくれる──が追加されます。この例ではインメモリのデータベース( @EnableBatchProcessing
により提供されている)を使用しています。つまりプログラムが完了すると、データはどこへともなく消えていきます。
部分ごとに見て行きましょう:
src/main/java/hello/BatchConfiguration.java
@Bean public ItemReader<Person> reader() { FlatFileItemReader<Person> reader = new FlatFileItemReader<Person>(); reader.setResource(new ClassPathResource("sample-data.csv")); reader.setLineMapper(new DefaultLineMapper<Person>() {{ setLineTokenizer(new DelimitedLineTokenizer() {{ setNames(new String[] { "firstName", "lastName" }); }}); setFieldSetMapper(new BeanWrapperFieldSetMapper<Person>() {{ setTargetType(Person.class); }}); }}); return reader; } @Bean public ItemProcessor<Person, Person> processor() { return new PersonItemProcessor(); } @Bean public ItemWriter<Person> writer(DataSource dataSource) { JdbcBatchItemWriter<Person> writer = new JdbcBatchItemWriter<Person>(); writer.setItemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<Person>()); writer.setSql("INSERT INTO people (first_name, last_name) VALUES (:firstName, :lastName)"); writer.setDataSource(dataSource); return writer; }
最初の一群のコードでプログラムの入力、変換、そして出力が定義されています。──reader()
は ItemReader
を生成します。このオブジェクトは sample-data.csv
という名前のファイルを〔クラスパス上から〕探し、ファイルの各行からPerson
オブジェクトを構成する情報を読み取ります。次にprocessor()
は先ほど定義したPersonItemProcessor
──データを大文字に変換する──を生成します。write(DataSource)
は ItemWriter
を生成します。この例ではオブジェクトはJDBCを出力先としています。@EnableBatchProcessing
により生成されたデータソースのコピーが自動で設定されます。単体のPerson
オブジェクトをINSERTするのに必要なSQLステートメントがJavaビーン・プロパティにより設定されています。
次の一群のコードはジョブのコンフィギュレーションを目的としています。
src/main/java/hello/BatchConfiguration.java
@Bean public Job importUserJob(JobBuilderFactory jobs, Step s1) { return jobs.get("importUserJob") .incrementer(new RunIdIncrementer()) .flow(s1) .end() .build(); } @Bean public Step step1(StepBuilderFactory stepBuilderFactory, ItemReader<Person> reader, ItemWriter<Person> writer, ItemProcessor<Person, Person> processor) { return stepBuilderFactory.get("step1") .<Person, Person> chunk(10) .reader(reader) .processor(processor) .writer(writer) .build(); }
最初のメソッドではジョブが定義され、次のメソッドでは単一のステップが定義されます。ジョブは複数のステップから構成されます。そしてそれぞれのステップは1つのリーダー、1つのプロセッサー、そして1つのライターから構成されます。
〔 JobBuilderFactory
を使ったジョブの定義を見ていきます。〕ジョブ定義の中で、まずインクリメンター〔incrementer〕が必要になります。これはジョブが自らの実行状態を管理するのにデータベースを使用しているからです。その後、各ステップを列挙しますが──このジョブにはただ1つのステップしかありません。そしてジョブの終了〔end()
メソッドの呼び出し〕。これで、Java APIが設定の完了したジョブを生成してくれます。
〔 StepBuilderFactory
を使ったステップの定義を見ていきます。〕ステップ定義の中で、1回ににどれだけのデータを書き込むかを決めます。今回のケースでは1回に10件のレコードを書き込みます。その後、すでに見てきたコードにより生成されたリーダー、プロセッサー、ライターの各オブジェクトを設定します。
NOTE: chunk()
メソッドはジェネリック・メソッドなので、メソッドの前に<Person,Person>
が付加されています。これは各回の処理で使用される入力と出力の型を示し、 ItemReader<Person>
とItemWriter<Person>
を関連付けます。
実行可能なプリケーションをつくる
バッチ処理はWARファイルに組み込むことも可能ですが、よりシンプルなアプローチとして以下ではスタンド・アローンのアプリケーションをつくる方法を示しています。すべては1つの、実行可能JARのなかにパッケージングされ、昔ながらのmain()
メソッドにより起動されます。
src/main/java/hello/Application.java
package hello; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.ComponentScan; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; @ComponentScan @EnableAutoConfiguration public class Application { public static void main(String[] args) { ApplicationContext ctx = SpringApplication.run(Application.class, args); List<Person> results = ctx.getBean(JdbcTemplate.class).query("SELECT first_name, last_name FROM people", new RowMapper<Person>() { @Override public Person mapRow(ResultSet rs, int row) throws SQLException { return new Person(rs.getString(1), rs.getString(2)); } }); for (Person person : results) { System.out.println("Found <" + person + "> in the database."); } } }
main()
メソッドはSpringApplication
ヘルパー・クラスに処理を委ね、Application.class
を引数としてrun()
メソッドを呼び出しています。これにより、プログラムはSpringに対してApplication
クラスからアノテーションで付与されたメタデータを読み取り、Springアプリケーション・コンテキストのコンポーネントとして管理するよう、指示しています。
@ComponentScan
アノテーションはSpringに対してhello
パッケージとそのサブ・パッケージを再帰的に検索して、Springの@Component
アノテーションにより直接・間接にマークされたクラスを見つけ出すように指示するものです。このディレクティブにより、Springが BatchConfiguration
を見つけて〔アプリケーション・コンテキストに〕登録することが確約されます。なんとなればこのクラスは@Configuration
アノテーションによりマークされており、このアノテーションはまた@Component
アノテーションによりマークされているからです。
@EnableAutoConfiguration
アノテーションはクラスパスの内容に基づき適切と判断されるデフォルトの振る舞いをさせるスイッチを入れます。例えば、このアノテーションによりSpring は CommandLineRunner
インターフェースを探しだしてその run()
メソッドを呼び出すように動作します。これにより、このガイドを通じてつくってきたデモ・コードが起動されるのです。
デモンストレーションのため、上記のコードでは JdbcTemplate
が生成され、データベースに対するクエリが発行され、そしてバッチ・ジョブによりINSERTされた人びとの名前の一覧が出力されます。〔ようするに、上記のコードのうち ApplicationContext ctx =...
の行よりも下はすべて本番稼働のアプリケーションであれば不要なコードです。〕
実行可能JARをつくる
もしあなたがGradleを使っているなら・・・〔中略〕。
もしあなたがMavenを使っているなら、 mvn spring-boot:run
コマンドでアプリケーションを実行できます。あるいはmvn clean package
コマンドで実行可能JARファイルをビルドし、次のコマンドよりJARを起動することもできます:
java -jar target/gs-batch-processing-0.1.0.jar
ジョブによって、変換された人名のリストが出力されるはずです〔これはバッチ・ジョブにより行われるもの〕。そしてジョブが実行されたあと、今度はデータベースから抽出された結果も見ることになります〔これは前述のデモンストレーションのためのコードによるもの〕。
Converting (firstName: Jill, lastName: Doe) into (firstName: JILL, lastName: DOE) Converting (firstName: Joe, lastName: Doe) into (firstName: JOE, lastName: DOE) Converting (firstName: Justin, lastName: Doe) into (firstName: JUSTIN, lastName: DOE) Converting (firstName: Jane, lastName: Doe) into (firstName: JANE, lastName: DOE) Converting (firstName: John, lastName: Doe) into (firstName: JOHN, lastName: DOE) Found <firstName: JILL, lastName: DOE> in the database. Found <firstName: JOE, lastName: DOE> in the database. Found <firstName: JUSTIN, lastName: DOE> in the database. Found <firstName: JANE, lastName: DOE> in the database. Found <firstName: JOHN, lastName: DOE> in the database.