kagamihogeの日記

kagamihogeの日記です。

Spring Batch 3.0うごかす

2020/4/7 追記 本エントリは古い内容のため下記を参照してください。

kagamihoge.hatenablog.com


Spring Batchをためす。

やること

環境

ソースコードと説明など

チュートリアル的なことその1

pom.xml

spring-bootの設定、Spring Batch、REST、の依存性を追加する。

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.1.RELEASE</version>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-batch</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

ジョブの設定を作る。

SampleBatchApplication

@Configuration
@EnableBatchProcessing
public class SampleBatchApplication {

    @Autowired
    private JobBuilderFactory jobs;

    @Autowired
    private StepBuilderFactory steps;

    @Bean
    public Job job(JobBuilderFactory jobs, Step s1, Step s2) {
        return jobs
                .get("myJob")
                .incrementer(new RunIdIncrementer())
                .start(s1)
                .next(s2)
                .build();
    }

    @Bean(name = "s1")
    public Step step1() {
        return steps.get("step1").tasklet((stepContribution, chunkContext) -> {
            System.out.println("step 1");
            return RepeatStatus.FINISHED;
        }).build();
    }

    @Bean(name = "s2")
    public Step step2() {
        return steps.get("step2").tasklet((stepContribution, chunkContext) -> {
            System.out.println("step 2");
            return RepeatStatus.FINISHED;
        }).build();
    }
}

おおむねチュートリアル相当。変更点としては、まずはバッチを起動するのが目標なので、stepの中身はreader-processor-writerではなくシンプルなTaskletにした。

クラスにアノテーション@Configurationおよび@EnableBatchProcessingを付与することで、Spring Batch設定用のクラスとなる。以前はジョブをXMLファイルで記述していたものが、Javaのコードでも書けるようになっているもの、と理解すれば良い。

一つのJobがあり、その構成はjobメソッドで行っている。構成はただ単にSteps1 -> Step s2の2ステップだけ。各Stepの中身はデバッグ出力するだけのTaskletが一つだけある。

jobメソッドはJobを返しさえすれば、引数はなんでもよい。つまり、もう一つのサンプルにも出てくるが、シグネチャに渡すStepはゼロでもいいし何個でも良い。ここではStepが複数あるので、@Beannameを付与し、引数のパラメータ名とあわせている。こうすると自動的に解決してくれる*1。なお、Getting Startedのように、Stepの引数一つ、@Beanを付与されたStepも一つの場合、自明なのでnameは無くても解決してくれる。

以下は起動用のクラス。

App

@SpringBootApplication
public class App {
    public static void main(String[] args) throws Exception {
        System.exit(SpringApplication.exit(SpringApplication.run(App.class, args)));
    }
}

実行

これを実行すると以下のようなログが出力される。

2015-02-14 12:54:37.324  INFO 5636 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=myJob]] launched with the following parameters: [{run.id=1}]
2015-02-14 12:54:37.380  INFO 5636 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [step1]
step 1
2015-02-14 12:54:37.408  INFO 5636 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [step2]
step 2
2015-02-14 12:54:37.425  INFO 5636 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=myJob]] completed with the following parameters: [{run.id=1}] and the following status: [COMPLETED]

なお、チュートリアルが目的であれば、Spring Batchのメタデータ管理テーブルとそれを操作する各種のBeanは自分で用意する必要が無い。もちろん、本番環境で動作させるときには必要。なぜ自分で用意しなくても動く理由についてだが、@EnableBatchProcessingをデフォルトで使う場合にはその辺自動的に設定してくれるから(らしい。良く分かってない)。また、その動作はインメモリなので、起動するたびにメタデータもクリアされる。開発やテストなどで何回もバッチを動かす場合には大変便利である。

チュートリアル的なことその2

SampleBatchApplication

@Configuration
@EnableBatchProcessing
public class SampleBatchApplication {

    @Autowired
    private JobBuilderFactory jobs;

    @Autowired
    private StepBuilderFactory steps;

    @Bean
    public Job job() {
        return jobs
                .get("myJob")
                .incrementer(new RunIdIncrementer())
                .start(step1())
                .next(step2(" hogehoge"))
                .next(steps.get("step3").tasklet((stepContribution, chunkContext) -> {
                    System.out.println("step 3");
                    return RepeatStatus.FINISHED;}).build())
                .build();
    }

    public Step step1() {
        return steps.get("step1").tasklet((stepContribution, chunkContext) -> {
            System.out.println("step 1");
            return RepeatStatus.FINISHED;
        }).build();
    }

    public Step step2(String arg) {
        return steps.get("step2").tasklet((stepContribution, chunkContext) -> {
            System.out.println("step 2" + arg);
            return RepeatStatus.FINISHED;
        }).build();
    }
}

Jobを返すメソッドだが、どうやら正しくインスタンスが返りさえすれば、その記述方法にはある程度の柔軟性があるらしい。一つ前のコードではjobメソッドの引数でStepを与えていたが、上記のコードではStepを返すメソッド(step1,step2)であったり、更にそのメソッドを省略したり(step3)している。ラムダ式が書ける部分はそうしてしまっても動作した*2

なんにせよ、ジョブの定義がコンパイルでチェックできる利点は大きいように思われる。

RESTからキック

バッチの起動を、コマンドラインではなくREST経由で行う。

SampleBatchApplication

@Configuration
@EnableBatchProcessing
public class SampleBatchApplication {

    @Autowired
    private JobBuilderFactory jobs;

    @Autowired
    private StepBuilderFactory steps;

    @Bean(name="job1")
    public Job job1() {
         return jobs
                .get("myJob")
                .incrementer(new RunIdIncrementer())
                .start(step1())
                .next(step2(" hogehoge"))
                .next(steps.get("step 3").tasklet((stepContribution, chunkContext) -> {
                    System.out.println("step 3");
                    return RepeatStatus.FINISHED;}).build())
                .build();
    }
    
    @Bean(name="job2")
    public Job job2() {
        return jobs
                .get("myJob2")
                .incrementer(new RunIdIncrementer())
                .start(steps.get("step 3").tasklet((stepContribution, chunkContext) -> {
                    System.out.println("job2 step");
                    return RepeatStatus.FINISHED;}).build())
                .build();
    }

    public Step step1() {
        return steps.get("step1").tasklet((stepContribution, chunkContext) -> {
            System.out.println("step 1");
            return RepeatStatus.FINISHED;
        }).build();
    }

    public Step step2(String arg) {
        return steps.get("step1").tasklet((stepContribution, chunkContext) -> {
            System.out.println("step 2" + arg);
            return RepeatStatus.FINISHED;
        }).build();
    }
}

ジョブを2個にしている以外、特に変更点はなし。

REST

@SpringBootApplication
@RestController
public class AppWeb {
    @Autowired
    JobLauncher jobLauncher;
    
    @Autowired
    Job job1;

    @Autowired
    Job job2;
    
    @RequestMapping("/job1")
    String requestJob1() throws JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException, JobParametersInvalidException{
        jobLauncher.run(job1, createInitialJobParameterMap());
        return "Job1!";
    }

    @RequestMapping("/job2")
    String requestJob2() throws JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException, JobParametersInvalidException{
        jobLauncher.run(job2, createInitialJobParameterMap());
        return "Job2!";
    }
    
    private JobParameters createInitialJobParameterMap() {
        Map<String, JobParameter> m = new HashMap<>();
        m.put("time", new JobParameter(System.currentTimeMillis()));
        JobParameters p = new JobParameters(m);
        return p;
    }
    
    public static void main(String[] args) {
        SpringApplication.run(AppWeb.class, args);
    }
}

ジョブをキックするRESTのエントリポイントを二つ(/job1/job2)作成し、それぞれ対応するバッチを起動する。ジョブは@Autowiredすれば自動的にDIされる。ジョブの起動はjobLauncherを使用するが、これもDIするだけで良い。jobLauncher#runの第二引数はバッチをキックするたびにJobInstanceを生成して、何回でもバッチを起動できるようにするためで、詳細は後述。

application.yml

src/main/resources/application.ymlを作成して以下の1行を加える。

spring.batch.job.enabled: false

@EnableBatchProcessingのデフォルト設定は、バッチの設定を読み込むとすべて1回起動するようになっている。このデフォルト設定は、ちょっと試したりする分には自前でjobLauncher#runでバッチを起動しなくても良いので便利である。しかし、REST経由など任意のタイミングで起動したい場合には不要なので、上記の設定でオフにしておく。

実行

起動したあと、http://localhost:8080/job1を2回、http://localhost:8080/job2を2回実行したときのログの様子。

2015-02-14 13:22:45.432  INFO 2112 --- [nio-8080-exec-1] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=myJob]] launched with the following parameters: [{time=1423887765246}]
2015-02-14 13:22:45.457  INFO 2112 --- [nio-8080-exec-1] o.s.batch.core.job.SimpleStepHandler     : Executing step: [step1]
step 1
2015-02-14 13:22:45.491  INFO 2112 --- [nio-8080-exec-1] o.s.batch.core.job.SimpleStepHandler     : Executing step: [step2]
step 2 hogehoge
2015-02-14 13:22:45.524  INFO 2112 --- [nio-8080-exec-1] o.s.batch.core.job.SimpleStepHandler     : Executing step: [step3]
step 3
2015-02-14 13:22:45.538  INFO 2112 --- [nio-8080-exec-1] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=myJob]] completed with the following parameters: [{time=1423887765246}] and the following status: [COMPLETED]
2015-02-14 13:22:47.065  INFO 2112 --- [nio-8080-exec-2] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=myJob]] launched with the following parameters: [{time=1423887767051}]
2015-02-14 13:22:47.083  INFO 2112 --- [nio-8080-exec-2] o.s.batch.core.job.SimpleStepHandler     : Executing step: [step1]
step 1
2015-02-14 13:22:47.130  INFO 2112 --- [nio-8080-exec-2] o.s.batch.core.job.SimpleStepHandler     : Executing step: [step2]
step 2 hogehoge
2015-02-14 13:22:47.143  INFO 2112 --- [nio-8080-exec-2] o.s.batch.core.job.SimpleStepHandler     : Executing step: [step3]
step 3
2015-02-14 13:22:47.161  INFO 2112 --- [nio-8080-exec-2] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=myJob]] completed with the following parameters: [{time=1423887767051}] and the following status: [COMPLETED]
2015-02-14 13:22:50.652  INFO 2112 --- [nio-8080-exec-3] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=myJob2]] launched with the following parameters: [{time=1423887770648}]
2015-02-14 13:22:50.657  INFO 2112 --- [nio-8080-exec-3] o.s.batch.core.job.SimpleStepHandler     : Executing step: [step3]
job2 step
2015-02-14 13:22:50.664  INFO 2112 --- [nio-8080-exec-3] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=myJob2]] completed with the following parameters: [{time=1423887770648}] and the following status: [COMPLETED]
2015-02-14 13:22:52.838  INFO 2112 --- [nio-8080-exec-4] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=myJob2]] launched with the following parameters: [{time=1423887772813}]
2015-02-14 13:22:52.859  INFO 2112 --- [nio-8080-exec-4] o.s.batch.core.job.SimpleStepHandler     : Executing step: [step3]
job2 step
2015-02-14 13:22:52.902  INFO 2112 --- [nio-8080-exec-4] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=myJob2]] completed with the following parameters: [{time=1423887772813}] and the following status: [COMPLETED]

ジョブXMLとCommandLineJobRunner

従来通りのジョブXMLorg.springframework.batch.core.launch.support.CommandLineJobRunnerによる起動も当然可能である。これは昔ながらの方法なので、ググればある程度情報は出てくる。

ジョブXML(とapplication-context.xml

src/main/resources/sample.xmlとかにXMLファイルを作る。

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:batch="http://www.springframework.org/schema/batch"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/batch
            http://www.springframework.org/schema/batch/spring-batch-3.0.xsd
            http://www.springframework.org/schema/beans 
            http://www.springframework.org/schema/beans/spring-beans-4.1.xsd">

    <bean id="jobRepository"
        class="org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean">
        <property name="transactionManager" ref="transactionManager" />
    </bean>

    <bean id="transactionManager"
        class="org.springframework.batch.support.transaction.ResourcelessTransactionManager" />

    <bean id="jobLauncher"
        class="org.springframework.batch.core.launch.support.SimpleJobLauncher">
        <property name="jobRepository" ref="jobRepository" />
    </bean>

    <batch:job id="myJob">
        <batch:step id="s11">
            <batchlet
                class="com.example.kagamihoge.springbatchstudy.commandline.MyTasklet" />
        </batch:step>
    </batch:job>
</beans>

CommandLineJobRunnerから起動する場合、そのパラメータにはXMLを渡す口は一つしかない。そのため、一つのXMLにジョブ定義と、いわゆるapplication-context.xmlに記述するbean設定を全部書く(らしい)。

上記XMLにある通り、Spring Batchを動かすにはjobRepositoryなど幾つか必要なbeanを設定してやる必要がある。本来はメタデータを格納するなんらかのデータベース設定が必要だが、ひとまずインメモリで良いなら上記のような設定でかまわない*3

MyTasklet

上記のジョブXMLに記述されている、適当なTaskletは以下の通り。

public class MyTasklet implements Tasklet {

    @Override
    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
        System.out.println("task 1");
        return RepeatStatus.FINISHED;
    }

}

起動

あとはorg.springframework.batch.core.launch.support.CommandLineJobRunnerjavaコマンドで実行する。

Run -> Run ConfigurationでNew launch configurationを選ぶ。Projectを選択して、Main classにはorg.springframework.batch.core.launch.support.CommandLineJobRunnerを入力する。

f:id:kagamihoge:20150214133626p:plain

Argumentsタブを選択する。第一引数がジョブXMLのパス、第二引数が実行するジョブ名。参考:4. Configuring and Running a Job

f:id:kagamihoge:20150214133826p:plain

ハマった点

以下、ハマった点と原因、解決策など。

勝手にバッチが起動する

原因は、エントリ本文中にも書いたが、@BatchAutoConfigurationのデフォルト設定がそうなっているため。

単にBatchAutoConfigurationを止めるだけなら@EnableAutoConfiguration(exclude=BatchAutoConfiguration.class)とかすれば良い。しかし、これだとメタデータ管理用テーブルもインメモリにセットアップされなくなるので、以下のようなエラーになる。

org.hsqldb.HsqlException: user lacks privilege or object not found: BATCH_JOB_INSTANCE

よって、@BatchAutoConfigurationを有効かつデフォルトのバッチ起動設定を止めるには、アプリケーションプロパティにspring.batch.job.enabled: falseと書く。

なお、spring.batch.job.enabled: falseの指定はプロパティ以外にも幾つか方法がある。参考:cron - how to stop spring batch scheduled jobs from running at first time when executing the code? - Stack Overflow

Step already complete or not restartable, so no action to execute

現象としては、RESTからキックを試しているとき、上記のようなエラーが出て同一のジョブを2回以上実行できなかった。

原因は、Jobrestartableのデフォルトはfalseなため。よって、安直に考えればrestartabletrueに設定すればよい。これは一応上手くいく。

SimpleJob job = new SimpleJob();
job.setRestartable(false);

しかし、マニュアル4. Configuring and Running a Jobにも書かれている通り、これはSpring Batch的にはよろしくない方法である。なぜか。

たとえば、毎日実行される請求バッチがあるとする。いま、4/1にこのバッチが正常終了したとする。4/2にもこのバッチは起動する。もし、restartabletrueだとすると、4/2に、4/1に正常終了したバッチを再実行できてしまうことになる。これは二重請求を生む可能性がある。

同一のバッチであったとしても、実行するタイミングなどが異なれば、パラメータ等は異なるのが自然である。また、正常終了したバッチを同じパラメータで再実行することはまず無いのも自然である。

もしrestartabletrueにする場合、4/24/1のバッチが実行されないような仕組みは開発者が責任を持って作なければならない。デフォルトのfalseの場合、実行されないようにするのはSpring Batchが面倒を見てくれる。

ざっくりだがSpring Batchは以上のような前提を元にしている。詳細はマニュアルなどを参照。で、上の例だと、1つの請求バッチJobがあり、4/14/2それぞれにJobInstanceが作成される。

RESTのサンプルコードに戻ってくるが、つまり、RESTのURLをキックするたびにJobInstanceを作成すれば、同じジョブを何度でも実行できることになる。では、JobInstanceが同一と見なされる条件は何か。

JobInstance Jobの論理単位を保持するモデル。Job名とJob起動パラメータを保持しており、これが同一のものは同じJobInstanceとみなされる。(Jobと 1:N)

概説 Springプロダクト(12) - Spring Batchで簡単にバッチを作る (2) Spring Batch 基本構成 | マイナビニュース より抜粋

というわけで、jobLauncher#runのジョブパラメータ引数に、System.currentTimeMillis()を渡すことにした。これによって、おおむね毎回異なるジョブパラメータとなり、異なるJobInstanceと見なされ、同一のジョブを何度でも起動できるようになった。

Map<String, JobParameter> m = new HashMap<>();
m.put("time", new JobParameter(System.currentTimeMillis()));
JobParameters p = new JobParameters(m);

ソースコード

参考URL

*1:らしい。SpringのDIの仕組み良く分かってないけど、こうやったら上手くいった。

*2:ただ、バッチの記述がラムダ式で書けるほど短くなるケースは余り無いとは思うけど

*3:http://www.mkyong.com/spring-batch/spring-batch-example-xml-file-to-database/ を参考にした