2020/4/7 追記 本エントリは古い内容のため下記を参照してください。
Spring Batchをためす。
やること
- Getting Started · Creating a Batch Service のチュートリアルを読んでHello Worldレベルのことをやる。
環境
- Java
- Spring
- spring-boot-starter-parent 1.2.1.RELEASE
- spring-batch-core-3.0.2.RELEASE.jar
ソースコードと説明など
チュートリアル的なことその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
メソッドで行っている。構成はただ単にStep
s1 -> Step
s2の2ステップだけ。各Step
の中身はデバッグ出力するだけのTasklet
が一つだけある。
job
メソッドはJob
を返しさえすれば、引数はなんでもよい。つまり、もう一つのサンプルにも出てくるが、シグネチャに渡すStep
はゼロでもいいし何個でも良い。ここではStep
が複数あるので、@Bean
にname
を付与し、引数のパラメータ名とあわせている。こうすると自動的に解決してくれる*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
従来通りのジョブXMLとorg.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.CommandLineJobRunner
をjava
コマンドで実行する。
Run -> Run ConfigurationでNew launch configurationを選ぶ。Projectを選択して、Main classにはorg.springframework.batch.core.launch.support.CommandLineJobRunner
を入力する。
Argumentsタブを選択する。第一引数がジョブXMLのパス、第二引数が実行するジョブ名。参考:4. Configuring and Running a Job
ハマった点
以下、ハマった点と原因、解決策など。
勝手にバッチが起動する
原因は、エントリ本文中にも書いたが、@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回以上実行できなかった。
原因は、Job
のrestartable
のデフォルトはfalse
なため。よって、安直に考えればrestartable
をtrue
に設定すればよい。これは一応上手くいく。
SimpleJob job = new SimpleJob(); job.setRestartable(false);
しかし、マニュアル4. Configuring and Running a Jobにも書かれている通り、これはSpring Batch的にはよろしくない方法である。なぜか。
たとえば、毎日実行される請求バッチがあるとする。いま、4/1
にこのバッチが正常終了したとする。4/2
にもこのバッチは起動する。もし、restartable
がtrue
だとすると、4/2
に、4/1
に正常終了したバッチを再実行できてしまうことになる。これは二重請求を生む可能性がある。
同一のバッチであったとしても、実行するタイミングなどが異なれば、パラメータ等は異なるのが自然である。また、正常終了したバッチを同じパラメータで再実行することはまず無いのも自然である。
もしrestartable
をtrue
にする場合、4/2
に4/1
のバッチが実行されないような仕組みは開発者が責任を持って作なければならない。デフォルトのfalse
の場合、実行されないようにするのはSpring Batchが面倒を見てくれる。
ざっくりだがSpring Batchは以上のような前提を元にしている。詳細はマニュアルなどを参照。で、上の例だと、1つの請求バッチJob
があり、4/1
と4/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
- Getting Started · Creating a Batch Service
- Spring Batch - Reference Documentation
- Spring Batch Example – XML File To MongoDB Database
- cron - how to stop spring batch scheduled jobs from running at first time when executing the code? - Stack Overflow
- 概説 Springプロダクト(12) - Spring Batchで簡単にバッチを作る (2) Spring Batch 基本構成 | マイナビニュース
*1:らしい。SpringのDIの仕組み良く分かってないけど、こうやったら上手くいった。
*2:ただ、バッチの記述がラムダ式で書けるほど短くなるケースは余り無いとは思うけど
*3:http://www.mkyong.com/spring-batch/spring-batch-example-xml-file-to-database/ を参考にした