M12i.

学術書・マンガ・アニメ・映画の消費活動とプログラミングについて

Spring Batchの"Getting Started"を読む

引き続きSpringキャンペーン中につき。今度はSpring Batchです。あまり深入りはしないつもりですが・・・。

まずは例によって"Getting Started"を読んでみましょう。ビルドはMavenでする前提で、不要と判断した箇所は中略しています。原典はSpring公式サイトの"Getting Started - Creating a Batch Service"です(2014/12/29取得)。

          * * *

バッチ・サービスをつくる

このガイドでは簡単なバッチ駆動のソリューションを作成する手順を見ていきます。

何をつくるのか

CSV形式のスプレッドシートからデータを取込み、カスタムメイドのプログラム・コードでデータを変換し、最終的な結果をデータベースに格納するというサービスをつくります。

何が必要か

  • だいたい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.

まとめ

〔・・・中略・・・〕

          * * *

原典はSpring公式サイトの"Getting Started - Creating a Batch Service"です(2014/12/29取得)。



Spring関連の訳出記事まとめ - M12i.