これは、なにをしたくて書いたもの?
Spring Boot(とSpring Framework)に、Quartzとの連携機能があるので、試してみようということで。
Spring Boot、Spring FrameworkとQuartz
Spring BootおよびSpring Frameworkの、Quartzとの連携については以下に記載があります。
- Spring Boot / IO / Quartz Scheduler
- Spring Framework / Integration / Task Execution and Scheduling / Using the Quartz Scheduler
またSpring Bootのプロパティとして、spring.quartz.〜
で設定ができます。
Common Application Properties / Core Properties
もう少し見てみましょう。
Spring Bootでは以下をBean定義することで、QuartzのScheduler
に関連付けます。
JobDetail
Calendar
Trigger
JobStoreは、デフォルトではインメモリです。spring.quartz.job-store-type
プロパティをjdbc
とすることで、JobStoreをデータベースに
することができます。
JobStoreをデータベースにした場合は、spring.quartz.jdbc.initialize-schema
プロパティでQuartzが使うテーブルの初期化方法を
指定できます。
データソースは、デフォルトではSpring Bootのアプリケーション向けのものを使います。それ以外のデータソースを使うには、
別のデータソースを@Bean
と@QuartzDataSource
を使って定義することになります。
JobDetail
やTrigger
は直接Springのコンポーネントとして定義してもよいですが、JobDetailFactoryBean
やSimpleTriggerFactoryBean
と
CronTriggerFactoryBean
で定義することもできます。
また、Scheduler
をSchedulerFactoryBean
で定義することもできます。
先にSpring BootでのQuartzに関するプロパティを紹介しましたが、SchedulerFactoryBean
で定義で設定する内容はQuartzの設定と
バッティングするので、SchedulerFactoryBean
とquartz.properties
でそれぞれ設定しない方が良いとされています。
たとえば、Quartzが使用するデータソースをSpring Frameworkのものとする場合は、org.quartz.jobStore.class
を指定してはいけません。
Please note that many SchedulerFactoryBean settings interact with common Quartz settings in the properties file; it is therefore not recommended to specify values at both levels. For example, do not set an "org.quartz.jobStore.class" property if you mean to rely on a Spring-provided DataSource, or specify an org.springframework.scheduling.quartz.LocalDataSourceJobStore variant which is a full-fledged replacement for the standard org.quartz.impl.jdbcjobstore.JobStoreTX.
ところで、Spring Frameworkのデータソースを使う場合は、LocalDataSourceJobStore
が使われるようです。これ、JobStoreCMT
の
サブクラスみたいですね。
public class LocalDataSourceJobStore extends JobStoreCMT {
Job
は、QuartzJobBean
を継承したクラスを作成して定義します。
public class ExampleJob extends QuartzJobBean { private int timeout; /** * Setter called after the ExampleJob is instantiated * with the value from the JobDetailFactoryBean (5) */ public void setTimeout(int timeout) { this.timeout = timeout; } protected void executeInternal(JobExecutionContext ctx) throws JobExecutionException { // do the actual work } }
Job
としての処理は、executeInternal
メソッドをオーバーライドして実装します。また、JobData
で設定した値をプロパティとして
設定することもできます。
ドキュメントを見るのはこれくらいにして、実際に使ってみましょう。
お題
今回のお題は、以下とします。
環境
今回の環境は、こちら。
$ java --version openjdk 17.0.5 2022-10-18 OpenJDK Runtime Environment (build 17.0.5+8-Ubuntu-2ubuntu120.04) OpenJDK 64-Bit Server VM (build 17.0.5+8-Ubuntu-2ubuntu120.04, mixed mode, sharing) $ mvn --version Apache Maven 3.8.6 (84538c9988a25aec085021c365c560670ad80f63) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 17.0.5, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.4.0-131-generic", arch: "amd64", family: "unix"
MySQLは172.17.0.2で動作しているものとし、データベースはpractice
、ユーザー/パスワードはkazuhira
/passwordで
作成済みとします。
$ mysql --version mysql Ver 8.0.31 for Linux on x86_64 (MySQL Community Server - GPL) mysql> select version(); +-----------+ | version() | +-----------+ | 8.0.31 | +-----------+ 1 row in set (0.01 sec)
準備
Quartzのテーブルは、事前に作成しておくことにします。
$ curl -sL https://raw.githubusercontent.com/quartz-scheduler/quartz/v2.3.2/quartz-core/src/main/resources/org/quartz/impl/jdbcjobstore/tables_mysql_innodb.sql | mysql -ukazuhira -p practice
Spring Bootアプリケーションを作成する
では、Spring Bootアプリケーションを作成していきます。依存関係には、quartz
、jdbc
、mysql
を指定。
$ curl -s https://start.spring.io/starter.tgz \ -d bootVersion=2.7.5 \ -d javaVersion=17 \ -d type=maven-project \ -d name=quartz-example \ -d groupId=org.littlewings \ -d artifactId=quartz-examples \ -d version=0.0.1-SNAPSHOT \ -d packageName=org.littlewings.spring.quartz \ -d dependencies=quartz,jdbc,mysql \ -d baseDir=quartz-example | tar zxvf -
jdbc
とmysql
があるのは、JobStoreをデータベースにするからですね。
プロジェクト内に移動。
$ cd quartz-example
生成されたソースコードは、削除しておきます。
$ rm src/main/java/org/littlewings/spring/quartz/QuartzExampleApplication.java src/test/java/org/littlewings/spring/quartz/QuartzExampleApplicationTests.java
<properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
Job
の定義。プロパティとして、word
をJobDataとして受け取るように想定して作成。
src/main/java/org/littlewings/spring/quartz/PrintMessageJob.java
package org.littlewings.spring.quartz; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.springframework.scheduling.quartz.QuartzJobBean; public class PrintMessageJob extends QuartzJobBean { LoggingService loggingService; private String word; public PrintMessageJob(LoggingService loggingService) { this.loggingService = loggingService; } @Override protected void executeInternal(JobExecutionContext context) throws JobExecutionException { loggingService.log(word); } public String getWord() { return word; } public void setWord(String word) { this.word = word; } }
設定されたプロパティをログ出力するだけのJob
です。
loggingService.log(word);
ログ出力の部分は、DIできることを示すためにSpringのコンポーネントにすることにしました。
src/main/java/org/littlewings/spring/quartz/LoggingService.java
package org.littlewings.spring.quartz; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @Service public class LoggingService { Logger logger = LoggerFactory.getLogger(LoggingService.class); public void log(String word) { logger.info("Hello, {}!!!", word); } }
JobDetail
やTrigger
の定義。今回は、JobDetailFactoryBean
とSimpleTriggerFactoryBean
を使って定義しました。
src/main/java/org/littlewings/spring/quartz/JobConfig.java
package org.littlewings.spring.quartz; import java.sql.Date; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.Map; import org.quartz.JobDetail; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.quartz.JobDetailFactoryBean; import org.springframework.scheduling.quartz.SimpleTriggerFactoryBean; @Configuration public class JobConfig { @Bean public JobDetailFactoryBean jobDetailFactoryBean() { JobDetailFactoryBean jobDetailFactoryBean = new JobDetailFactoryBean(); jobDetailFactoryBean.setName("print-message-job"); jobDetailFactoryBean.setJobClass(PrintMessageJob.class); jobDetailFactoryBean.setJobDataAsMap(Map.of("word", "Quartz")); jobDetailFactoryBean.setGroup("job-group1"); jobDetailFactoryBean.setDurability(true); // Triggerを紐付けないJobとして登録 return jobDetailFactoryBean; } @Bean public SimpleTriggerFactoryBean simpleTriggerFactoryBean(JobDetail jobDetail) { SimpleTriggerFactoryBean simpleTriggerFactoryBean = new SimpleTriggerFactoryBean(); simpleTriggerFactoryBean.setName("trigger1"); simpleTriggerFactoryBean.setJobDetail(jobDetail); simpleTriggerFactoryBean.setGroup("trigger-group1"); simpleTriggerFactoryBean.setStartTime( Date.from(LocalDateTime.now().plusSeconds(15L).atZone(ZoneId.of("Asia/Tokyo")).toInstant()) ); simpleTriggerFactoryBean.setRepeatInterval(5 * 1000L); return simpleTriggerFactoryBean; } }
実行スケジュールは、15秒後に開始して、5秒おきに実行するようにTrigger
設定しました。
simpleTriggerFactoryBean.setStartTime( Date.from(LocalDateTime.now().plusSeconds(15L).atZone(ZoneId.of("Asia/Tokyo")).toInstant()) ); simpleTriggerFactoryBean.setRepeatInterval(5 * 1000L);
このソースコードでは、以下の設定を行う必要があります。
jobDetailFactoryBean.setDurability(true); // Triggerを紐付けないJobとして登録
Durabilityとは永続性で、これがfalse
の時はTrigger
に紐づいていないJob
はScheduler
から削除されます。
Durability - if a job is non-durable, it is automatically deleted from the scheduler once there are no longer any active triggers associated with it. In other words, non-durable jobs have a life span bounded by the existence of its triggers.
Lesson 3: More About Jobs and Job Details
これをtrue
にしていない場合は、以下のように起動時に例外になります。
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'quartzScheduler' defined in class path resource [org/springframework/boot/autoconfigure/quartz/QuartzAutoConfiguration.class]: Invocation of init method failed; nested exception is org.quartz.SchedulerException: Jobs added with no trigger must be durable. Caused by: org.quartz.SchedulerException: Jobs added with no trigger must be durable.
メッセージに書かれているように、Trigger
に紐付かないJob
はDurabilityをtrue
にしておく必要があります。
JobDetail
とTrigger
のScheduler
への登録タイミングが異なるからでしょうね。
main
クラス。
src/main/java/org/littlewings/spring/quartz/App.java
package org.littlewings.spring.quartz; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class App { public static void main(String... args) { SpringApplication.run(App.class, args); } }
Spring Bootの設定。
src/main/resources/application.properties
spring.datasource.url=jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&connectionCollation=utf8mb4_0900_bin spring.datasource.username=kazuhira spring.datasource.password=password spring.quartz.job-store-type=jdbc # Jobの上書き設定を許容する spring.quartz.overwrite-existing-jobs=true spring.quartz.properties.org.quartz.jobStore.isClustered=true spring.quartz.properties.org.quartz.jobStore.useProperties=true spring.quartz.wait-for-jobs-to-complete-on-shutdown=true spring.quartz.jdbc.initialize-schema=never
spring.datasource.〜
はデータソースの設定なので良いとして。
こちらで、JobStoreをデータベースにします。これで使われるJobStoreは、Spring Frameworkの提供するLocalDataSourceJobStore
です。
spring.quartz.job-store-type=jdbc
Quartzのテーブル定義は先に作成したので、以下はnever
を設定。
spring.quartz.jdbc.initialize-schema=never
アプリケーションの終了時に、実行中のJobがある場合は待機するように設定。
spring.quartz.wait-for-jobs-to-complete-on-shutdown=true
spring.quartz.properties.〜
は、Quartzのプロパティをそのまま指定するようになりますね。
spring.quartz.properties.org.quartz.jobStore.isClustered=true spring.quartz.properties.org.quartz.jobStore.useProperties=true
あと、今回はスケジューリングをしょっちゅう変更していたので、以下のように既存のJobを上書きできるようにしました。
# Jobの上書き設定を許容する spring.quartz.overwrite-existing-jobs=true
こちらに書かれている内容ですね。
By default, jobs created by configuration will not overwrite already registered jobs that have been read from a persistent job store. To enable overwriting existing job definitions set the spring.quartz.overwrite-existing-jobs property.
通常は、デフォルトのfalse
で良いと思います。
確認する
では、動作確認してみましょう。
パッケージング。
$ mvn package
起動。
$ java -jar target/quartz-examples-0.0.1-SNAPSHOT.jar
起動ログ。
2022-11-14 00:49:19.086 INFO 39395 --- [ main] o.s.s.quartz.LocalDataSourceJobStore : JobStoreCMT initialized. 2022-11-14 00:49:19.087 INFO 39395 --- [ main] org.quartz.core.QuartzScheduler : Scheduler meta-data: Quartz Scheduler (v2.3.2) 'quartzScheduler' with instanceId 'NON_CLUSTERED' Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally. NOT STARTED. Currently in standby mode. Number of jobs executed: 0 Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 10 threads. Using job-store 'org.springframework.scheduling.quartz.LocalDataSourceJobStore' - which supports persistence. and is clustered. 2022-11-14 00:49:19.087 INFO 39395 --- [ main] org.quartz.impl.StdSchedulerFactory : Quartz scheduler 'quartzScheduler' initialized from an externally provided properties instance. 2022-11-14 00:49:19.087 INFO 39395 --- [ main] org.quartz.impl.StdSchedulerFactory : Quartz scheduler version: 2.3.2 2022-11-14 00:49:19.087 INFO 39395 --- [ main] org.quartz.core.QuartzScheduler : JobFactory set to: org.springframework.scheduling.quartz.SpringBeanJobFactory@340da44c 2022-11-14 00:49:19.305 INFO 39395 --- [ main] o.s.s.quartz.SchedulerFactoryBean : Starting Quartz Scheduler now 2022-11-14 00:49:19.313 INFO 39395 --- [ main] o.s.s.quartz.LocalDataSourceJobStore : ClusterManager: detected 1 failed or restarted instances. 2022-11-14 00:49:19.313 INFO 39395 --- [ main] o.s.s.quartz.LocalDataSourceJobStore : ClusterManager: Scanning for instance "NON_CLUSTERED"'s failed in-progress jobs. 2022-11-14 00:49:19.339 INFO 39395 --- [ main] org.quartz.core.QuartzScheduler : Scheduler quartzScheduler_$_NON_CLUSTERED started. 2022-11-14 00:49:19.360 INFO 39395 --- [ main] org.littlewings.spring.quartz.App : Started App in 2.305 seconds (JVM running for 2.711)
クラスタリングが有効になっていますね。
そして、起動してからほぼ15秒後にJobが開始し、5秒おきに実行されます。
2022-11-14 00:49:19.360 INFO 39395 --- [ main] org.littlewings.spring.quartz.App : Started App in 2.305 seconds (JVM running for 2.711) 2022-11-14 00:49:33.560 INFO 39395 --- [eduler_Worker-1] o.l.spring.quartz.LoggingService : Hello, Quartz!!! 2022-11-14 00:49:38.523 INFO 39395 --- [eduler_Worker-2] o.l.spring.quartz.LoggingService : Hello, Quartz!!! 2022-11-14 00:49:43.521 INFO 39395 --- [eduler_Worker-3] o.l.spring.quartz.LoggingService : Hello, Quartz!!! 2022-11-14 00:49:48.529 INFO 39395 --- [eduler_Worker-4] o.l.spring.quartz.LoggingService : Hello, Quartz!!!
JobDataとして渡した値も設定できていることが確認できますね。
クラスタリングが有効になっているので、このあと同じアプリケーションを起動すると、クラスターに参加してそれぞれのノードでJobが
実行されるのを確認できます。
$ java -jar target/quartz-examples-0.0.1-SNAPSHOT.jar
OKですね。
あと、今回はJobDetail
やTrigger
をBeanとして定義しましたが、Scheduler
自体はDI可能なので、こちらを使って動的にJob登録を行うなど
するのもよいでしょう。
src/main/java/org/littlewings/spring/quartz/JobService.java
package org.littlewings.spring.quartz; import org.quartz.Scheduler; import org.springframework.stereotype.Service; @Service public class JobService { Scheduler scheduler; public JobService(Scheduler scheduler) { this.scheduler = scheduler; } public void registerJobs() { // ... } }
補足
QuartzJobBean
クラスを継承して作成したクラスはSpringのコンポーネントがDI可能なのですが、そうするとインスタンスはどのように
扱われているのか気になりますね。
結論を見ると、Jobの実行都度インスタンスが作成されるようです。
Object job = (this.applicationContext != null ? this.applicationContext.getAutowireCapableBeanFactory().createBean( bundle.getJobDetail().getJobClass(), AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false) : super.createJobInstance(bundle));
まとめ
Spring BootおよびSpring Frameworkの、Quartzとの連携機能を使ってみました。
最初、設定方法がちょっとわからなかったのですが、いろいろ調べたり動かしたりしてわかるようになりました。
というか、慣れるとかなりわかりやすい気がしますね。