Apache DeltaSpikeには、Scheduler Moduleというものがあり、こちらを使って定期的に動かすジョブを作成することができます。
ジョブのスケジュールはCRON形式で指定でき、実装としてはQuartzを使っているようです。
Quartz Enterprise Job Scheduler
こちらを利用すると、EJBのTimerServiceを使わなくてもCDIでジョブ起動ができる、と。
※EJB TimerServiceのPersistenceみたいなのはないみたいですが
では、試してみましょう。
準備
まずは、ビルド定義から。
build.sbt
name := "cdi-deltaspike-scheduler" organization := "org.littlewings" scalaVersion := "2.11.8" scalacOptions ++= Seq("-Xlint", "-deprecation", "-unchecked", "-feature") updateOptions := updateOptions.value.withCachedResolution(true) enablePlugins(JettyPlugin) webappWebInfClasses := true artifactName := { (scalaVersion: ScalaVersion, module: ModuleID, artifact: Artifact) => // artifact.name + "." + artifact.extension "ROOT." + artifact.extension } fork in Test := true libraryDependencies ++= Seq( "javax" % "javaee-web-api" % "7.0" % Provided, "org.apache.deltaspike.core" % "deltaspike-core-api" % "1.7.1" % Compile, "org.apache.deltaspike.core" % "deltaspike-core-impl" % "1.7.1" % Runtime, "org.apache.deltaspike.modules" % "deltaspike-scheduler-module-api" % "1.7.1" % Compile, "org.apache.deltaspike.modules" % "deltaspike-scheduler-module-impl" % "1.7.1" % Runtime, "org.apache.deltaspike.cdictrl" % "deltaspike-cdictrl-api" % "1.7.1" % Compile, "org.apache.deltaspike.cdictrl" % "deltaspike-cdictrl-weld" % "1.7.1" % Runtime, "org.quartz-scheduler" % "quartz" % "2.2.1" % Compile )
Webプロジェクトとして作成します。
project/plugins.sbt
logLevel := Level.Warn addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "2.1.0")
依存関係としては、Apache DeltaSpikeのCoreモジュール。
"org.apache.deltaspike.core" % "deltaspike-core-api" % "1.7.1" % Compile, "org.apache.deltaspike.core" % "deltaspike-core-impl" % "1.7.1" % Runtime,
Schedulerモジュール。
"org.apache.deltaspike.modules" % "deltaspike-scheduler-module-api" % "1.7.1" % Compile, "org.apache.deltaspike.modules" % "deltaspike-scheduler-module-impl" % "1.7.1" % Runtime,
そして、Container-Controlモジュールが必要です。
"org.apache.deltaspike.cdictrl" % "deltaspike-cdictrl-api" % "1.7.1" % Compile, "org.apache.deltaspike.cdictrl" % "deltaspike-cdictrl-weld" % "1.7.1" % Runtime,
Quartzについては、デフォルトのスケジューラーとしてApache DeltaSpikeのSchedule Moduleが採用しているのですが、Quartz自体への依存関係は明示的に記述する必要があります。
"org.quartz-scheduler" % "quartz" % "2.2.1" % Compile
アプリケーションの実行は、WildFlyへデプロイして確認するものとします。とりあえず、WildFlyを起動しておきましょう。
$ bin/standalone.sh
Jobを作成する
Apache DeltaSpikeのSchedule ModuleでJobを作成する方法は、以下の2つがあります。
いずれにしろ、Quartzの上で動作します。
@Scheduled with org.quartz.Job or java.lang.Runnable
Jobの起動タイミングについては、CRON形式で記述します。
Configurable CRON expressions
それでは、まずはQuartzのJobインターフェースを実装する方法で実装してみましょう。
src/main/scala/org/littlewings/javaee7/cdi/QuartzBasedJob.scala
package org.littlewings.javaee7.cdi import javax.enterprise.context.ApplicationScoped import javax.inject.Inject import org.apache.deltaspike.scheduler.api.Scheduled import org.quartz.{Job, JobExecutionContext} import org.slf4j.{Logger, LoggerFactory} @Scheduled(cronExpression = "0 0/1 * * * ?") @ApplicationScoped class QuartzBasedJob extends Job { @Inject var applicationScopedMessageService: ApplicationScopedMessageService = _ @Inject var sessionScopedMessageService: SessionScopedMessageService = _ @Inject var requestScopedMessageService: RequestScopedMessageService = _ @Inject var pseudoScopedMessageService: PseudoScopedMessageService = _ val logger: Logger = LoggerFactory.getLogger(getClass) override def execute(context: JobExecutionContext): Unit = { logger.info("[{}] startup job", getClass.getSimpleName) applicationScopedMessageService.loggingMessage() sessionScopedMessageService.loggingMessage() requestScopedMessageService.loggingMessage() pseudoScopedMessageService.loggingMessage() logger.info("[{}] end job", getClass.getSimpleName) } }
@ScheduleアノテーションおよびcronExpressionで、起動タイミングを指定します。また、CDI管理Beanとして宣言しておきます。クラス自体は、QuartzのJobインターフェースを実装して作成します。
@Scheduled(cronExpression = "0 0/1 * * * ?") @ApplicationScoped class QuartzBasedJob extends Job {
今回は、1分毎に起動するJobを作成しました。
あとは、Job#executeメソッドを実装すればOKです。
override def execute(context: JobExecutionContext): Unit = { logger.info("[{}] startup job", getClass.getSimpleName) applicationScopedMessageService.loggingMessage() sessionScopedMessageService.loggingMessage() requestScopedMessageService.loggingMessage() pseudoScopedMessageService.loggingMessage() logger.info("[{}] end job", getClass.getSimpleName) }
と書くと、QuartzのJobを作成するのと何が違う?という話になりますが、このJobへは@InjectでCDI管理Beanをインジェクションすることができます。
@Inject var applicationScopedMessageService: ApplicationScopedMessageService = _ @Inject var sessionScopedMessageService: SessionScopedMessageService = _ @Inject var requestScopedMessageService: RequestScopedMessageService = _ @Inject var pseudoScopedMessageService: PseudoScopedMessageService = _
ここで利用しているCDI管理Beanは、名称から自明かもしれませんが、各スコープに応じたCDI管理Beanです。
src/main/scala/org/littlewings/javaee7/cdi/MessageService.scala
package org.littlewings.javaee7.cdi import java.time.LocalDateTime import javax.enterprise.context.{ApplicationScoped, Dependent, RequestScoped, SessionScoped} import org.slf4j.{Logger, LoggerFactory} trait MessageServiceSupport { val logger: Logger = LoggerFactory.getLogger(getClass) def loggingMessage(): Unit = { logger.info(s"Hello ${getClass.getSimpleName}@${hashCode}, now = ${LocalDateTime.now}") } } @ApplicationScoped class ApplicationScopedMessageService extends MessageServiceSupport @SessionScoped @SerialVersionUID(1L) class SessionScopedMessageService extends MessageServiceSupport with Serializable @RequestScoped class RequestScopedMessageService extends MessageServiceSupport @Dependent class PseudoScopedMessageService extends MessageServiceSupport
何気に、SessionScopedやRequestScopedなCDI管理Beanも混じっています。これが動くのでしょうか…?
とりあえず、パッケージングして
> package
デプロイ。
$ cp target/scala-2.11/ROOT.war /path/to/wildfly-10.0.0.Final/standalone/deployments
すると、1分おきにジョブが実行されます。
01:05:00,041 INFO [org.littlewings.javaee7.cdi.QuartzBasedJob] (DefaultQuartzScheduler_Worker-1) [QuartzBasedJob] startup job 01:05:00,130 INFO [org.littlewings.javaee7.cdi.ApplicationScopedMessageService] (DefaultQuartzScheduler_Worker-1) Hello ApplicationScopedMessageService@1340878689, now = 2016-08-12T01:05:00.127 01:05:00,131 INFO [org.littlewings.javaee7.cdi.SessionScopedMessageService] (DefaultQuartzScheduler_Worker-1) Hello SessionScopedMessageService@1552885928, now = 2016-08-12T01:05:00.131 01:05:00,131 INFO [org.littlewings.javaee7.cdi.RequestScopedMessageService] (DefaultQuartzScheduler_Worker-1) Hello RequestScopedMessageService@1988410949, now = 2016-08-12T01:05:00.131 01:05:00,131 INFO [org.littlewings.javaee7.cdi.PseudoScopedMessageService] (DefaultQuartzScheduler_Worker-1) Hello PseudoScopedMessageService@157109538, now = 2016-08-12T01:05:00.131 01:05:00,131 INFO [org.littlewings.javaee7.cdi.QuartzBasedJob] (DefaultQuartzScheduler_Worker-1) [QuartzBasedJob] end job 01:06:00,001 INFO [org.littlewings.javaee7.cdi.QuartzBasedJob] (DefaultQuartzScheduler_Worker-3) [QuartzBasedJob] startup job 01:06:00,004 INFO [org.littlewings.javaee7.cdi.ApplicationScopedMessageService] (DefaultQuartzScheduler_Worker-3) Hello ApplicationScopedMessageService@1340878689, now = 2016-08-12T01:06:00.004 01:06:00,005 INFO [org.littlewings.javaee7.cdi.SessionScopedMessageService] (DefaultQuartzScheduler_Worker-3) Hello SessionScopedMessageService@1186961072, now = 2016-08-12T01:06:00.005 01:06:00,005 INFO [org.littlewings.javaee7.cdi.RequestScopedMessageService] (DefaultQuartzScheduler_Worker-3) Hello RequestScopedMessageService@1215852567, now = 2016-08-12T01:06:00.005 01:06:00,005 INFO [org.littlewings.javaee7.cdi.PseudoScopedMessageService] (DefaultQuartzScheduler_Worker-3) Hello PseudoScopedMessageService@157109538, now = 2016-08-12T01:06:00.005 01:06:00,006 INFO [org.littlewings.javaee7.cdi.QuartzBasedJob] (DefaultQuartzScheduler_Worker-3) [QuartzBasedJob] end job
一緒にハッシュコードも出力していますが、よくよく見るとSessionScopedやRequestScopedなCDI管理Beanは、都度インスタンスが作成されてインジェクションされているようですね。
In such scheduled-tasks CDI based dependency-injection is enabled. Furthermore, the request- and session-scope get started (and stopped) per job-execution. Therefore, the container-control module (of DeltaSpike) is required. That can be controlled via @Scheduled#startScopes (possible values: all scopes supported by the container-control module as well as {} for 'no scopes').
https://deltaspike.apache.org/documentation/scheduler.html#@Scheduledwithorg.quartz.Joborjava.lang.Runnable
とりあえず、最低限の動作は確認できました。
では、続いてRunnableインターフェースを実装したクラスで、Jobを作成してみましょう。
src/main/scala/org/littlewings/javaee7/cdi/RunnableBasedJob.scala
package org.littlewings.javaee7.cdi import javax.enterprise.context.ApplicationScoped import javax.inject.Inject import org.apache.deltaspike.scheduler.api.Scheduled import org.slf4j.{Logger, LoggerFactory} @Scheduled(cronExpression = "0 0/1 * * * ?") @ApplicationScoped class RunnableBasedJob extends Runnable { @Inject var applicationScopedMessageService: ApplicationScopedMessageService = _ @Inject var sessionScopedMessageService: SessionScopedMessageService = _ @Inject var requestScopedMessageService: RequestScopedMessageService = _ @Inject var pseudoScopedMessageService: PseudoScopedMessageService = _ val logger: Logger = LoggerFactory.getLogger(getClass) override def run(): Unit = { logger.info("[{}] startup job", getClass.getSimpleName) applicationScopedMessageService.loggingMessage() sessionScopedMessageService.loggingMessage() requestScopedMessageService.loggingMessage() pseudoScopedMessageService.loggingMessage() logger.info("[{}] end job", getClass.getSimpleName) } }
QuartzのJobインターフェースを実装した場合と、やっていることはほとんど同じです。実装しているメソッドが、Runnable#runなくらいですね。
デプロイして動かした時のログは、このように。
01:08:00,445 INFO [org.littlewings.javaee7.cdi.RunnableBasedJob] (DefaultQuartzScheduler_Worker-1) [RunnableBasedJob] startup job 01:08:00,552 INFO [org.littlewings.javaee7.cdi.ApplicationScopedMessageService] (DefaultQuartzScheduler_Worker-1) Hello ApplicationScopedMessageService@1897461758, now = 2016-08-12T01:08:00.550 01:08:00,552 INFO [org.littlewings.javaee7.cdi.SessionScopedMessageService] (DefaultQuartzScheduler_Worker-1) Hello SessionScopedMessageService@1805686172, now = 2016-08-12T01:08:00.552 01:08:00,553 INFO [org.littlewings.javaee7.cdi.RequestScopedMessageService] (DefaultQuartzScheduler_Worker-1) Hello RequestScopedMessageService@1407251698, now = 2016-08-12T01:08:00.553 01:08:00,553 INFO [org.littlewings.javaee7.cdi.PseudoScopedMessageService] (DefaultQuartzScheduler_Worker-1) Hello PseudoScopedMessageService@1527663713, now = 2016-08-12T01:08:00.553 01:08:00,553 INFO [org.littlewings.javaee7.cdi.RunnableBasedJob] (DefaultQuartzScheduler_Worker-1) [RunnableBasedJob] end job 01:09:00,002 INFO [org.littlewings.javaee7.cdi.RunnableBasedJob] (DefaultQuartzScheduler_Worker-3) [RunnableBasedJob] startup job 01:09:00,003 INFO [org.littlewings.javaee7.cdi.ApplicationScopedMessageService] (DefaultQuartzScheduler_Worker-3) Hello ApplicationScopedMessageService@1897461758, now = 2016-08-12T01:09:00.003 01:09:00,004 INFO [org.littlewings.javaee7.cdi.SessionScopedMessageService] (DefaultQuartzScheduler_Worker-3) Hello SessionScopedMessageService@1975586858, now = 2016-08-12T01:09:00.004 01:09:00,005 INFO [org.littlewings.javaee7.cdi.RequestScopedMessageService] (DefaultQuartzScheduler_Worker-3) Hello RequestScopedMessageService@2064048354, now = 2016-08-12T01:09:00.005 01:09:00,006 INFO [org.littlewings.javaee7.cdi.PseudoScopedMessageService] (DefaultQuartzScheduler_Worker-3) Hello PseudoScopedMessageService@1527663713, now = 2016-08-12T01:09:00.005 01:09:00,006 INFO [org.littlewings.javaee7.cdi.RunnableBasedJob] (DefaultQuartzScheduler_Worker-3) [RunnableBasedJob] end job
JobとRunnableは同時に使えない
と、あたかもさらっと動かしたように書きましたが、実は上記までを忠実に実行すると、このアプリケーションは動作しません。
どういうことかというと、上記までのコードを両方書くと、「@ScheduledなCDI管理BeanがJobとRunnableの2つの実装方法で両方とも存在している」という状態になります。
Apache DeltaSpikeのScheduler Moduleは、この状態を許容していません。JobおよびRunnableを実装して作成したCDI管理BeanとしてのJobが存在すると、デプロイ時に以下のようなエラーを見ることになります。
java.lang.IllegalStateException: Please only annotate classes with @org.apache.deltaspike.scheduler.api.Scheduled of type org.quartz.Job or of type java.lang.Runnable, but not both!
なお、@ScheduledなCDI管理Beanの登録は、Apache DeltaSpikeが実装しているCDI Extensionsによって実現されています。
JobおよびRunnableの使ったJobがそれぞれ定義されている場合は、このクラスが検出してエラーとします。
ドキュメントにも、一応それっぽいことが書いてあります。
Behind the scenes DeltaSpike registers an adapter for Quartz which just delegates to the run-method once the adapter gets called by Quartz. Technically you end up with almost the same, just with a reduced API for implementing (all) your scheduled jobs. Therefore the main difference is that your code is independent of Quartz-classes. However, you need to select org.quartz.Job or java.lang.Runnable for all your scheduled-tasks, bot not both!
https://deltaspike.apache.org/documentation/scheduler.html#@Scheduledwithorg.quartz.Joborjava.lang.Runnable
なお、JobもしくはRunnableのどちらかを使用することで統一していれば、@ScheduledなCDI管理Beanは、複数登録可能です。
このように。
@Scheduled(cronExpression = "0 0/1 * * * ?") @ApplicationScoped class QuartzBasedJob extends Job { // 省略 } @Scheduled(cronExpression = "10 0/1 * * * ?") @ApplicationScoped class QuartzBasedJob2 extends Job { // 省略 }
もしくは、こう。
@Scheduled(cronExpression = "0 0/1 * * * ?") @ApplicationScoped class RunnableBasedJob extends Runnable { // 省略 } @Scheduled(cronExpression = "10 0/1 * * * ?") @ApplicationScoped class RunnableBasedJob2 extends Runnable { // 省略 }
手動でJob登録する
これまでは、コンテナの起動時にJobを登録して、即時にスケジューリングしていました。
これを手動登録することもできます。
@Scheduled with org.quartz.Job or java.lang.Runnable
Manual Scheduler Control
ここで書かれている方法は、QuartzのJobのみになるようです。Runnableでやりたければ、ManagedExecutorServiceを使うことになりそうな?
Execute java.lang.Runnable with ManagedExecutorService
で、話を戻して手動でQuartzのJobを実行する方法ですが、@AlternativesなCDI管理Bean、QuartzSchedulerProducerをbeans.xmlに指定します。
src/main/resources/META-INF/beans.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd" bean-discovery-mode="annotated"> <alternatives> <class>org.apache.deltaspike.scheduler.impl.QuartzSchedulerProducer</class> </alternatives> </beans>
そして、これまでと同じようにJobを作成します。@Injectもふつうに使えます。
src/main/scala/org/littlewings/javaee7/cdi/ManualQuartzBasedJob.scala
package org.littlewings.javaee7.cdi import javax.enterprise.context.ApplicationScoped import javax.inject.Inject import org.apache.deltaspike.scheduler.api.Scheduled import org.quartz.{Job, JobExecutionContext} import org.slf4j.{Logger, LoggerFactory} @Scheduled(cronExpression = "30 0/1 * * * ?", onStartup = false) @ApplicationScoped class ManualQuartzBasedJob extends Job { @Inject var applicationScopedMessageService: ApplicationScopedMessageService = _ @Inject var sessionScopedMessageService: SessionScopedMessageService = _ @Inject var requestScopedMessageService: RequestScopedMessageService = _ @Inject var pseudoScopedMessageService: PseudoScopedMessageService = _ val logger: Logger = LoggerFactory.getLogger(getClass) override def execute(context: JobExecutionContext): Unit = { logger.info("[{}] startup job", getClass.getSimpleName) applicationScopedMessageService.loggingMessage() sessionScopedMessageService.loggingMessage() requestScopedMessageService.loggingMessage() pseudoScopedMessageService.loggingMessage() logger.info("[{}] end job", getClass.getSimpleName) } }
先ほどまでのJobとほとんど同じですが、違いは@ScheudledのonStarupをfalseにしていることです。
@Scheduled(cronExpression = "30 0/1 * * * ?", onStartup = false) @ApplicationScoped class ManualQuartzBasedJob extends Job {
こうしておくと、コンテナ起動時にスケジューラーに登録されなくなります。
ただ、こうするとJobを登録する処理が必要ですね。こちらはJAX-RSで作成しました。
JAX-RSの有効化。
src/main/scala/org/littlewings/javaee7/rest/JaxrsApplication.scala
package org.littlewings.javaee7.rest import javax.ws.rs.ApplicationPath import javax.ws.rs.core.Application @ApplicationPath("rest") class JaxrsApplication extends Application
Jobをスケジューラーに登録する、JAX-RSリソースクラス。
src/main/scala/org/littlewings/javaee7/rest/ManualJobResource.scala
package org.littlewings.javaee7.rest import javax.enterprise.context.RequestScoped import javax.inject.Inject import javax.ws.rs.core.MediaType import javax.ws.rs.{GET, Path, Produces} import org.apache.deltaspike.scheduler.spi.Scheduler import org.littlewings.javaee7.cdi.ManualQuartzBasedJob import org.quartz.Job @Path("job") @RequestScoped class ManualJobResource { @Inject var scheculer: Scheduler[Job] = _ @GET @Path("start-job") @Produces(Array(MediaType.TEXT_PLAIN)) def startJob: String = { scheculer.registerNewJob(classOf[ManualQuartzBasedJob]) "Register Job!!" } }
ここでのポイントは、Apache DeltaSpikeのSchedulerを@Injectして
@Inject
var scheculer: Scheduler[Job] = _
作成したJobのClassクラスをScheduler#registerNewJobすることですね。
scheculer.registerNewJob(classOf[ManualQuartzBasedJob])
これで、Job登録完了です。
デプロイして確認してみます。デプロイ後、以下のコマンドを実行するとJobが登録され
$ curl http://localhost:8080/rest/job/start-job Register Job!!
実行されるようになります。
01:32:30,012 INFO [org.littlewings.javaee7.cdi.ManualQuartzBasedJob] (DefaultQuartzScheduler_Worker-5) [ManualQuartzBasedJob] startup job 01:32:30,013 INFO [org.littlewings.javaee7.cdi.ApplicationScopedMessageService] (DefaultQuartzScheduler_Worker-5) Hello ApplicationScopedMessageService@1293488989, now = 2016-08-12T01:32:30.013 01:32:30,014 INFO [org.littlewings.javaee7.cdi.SessionScopedMessageService] (DefaultQuartzScheduler_Worker-5) Hello SessionScopedMessageService@1057457274, now = 2016-08-12T01:32:30.014 01:32:30,015 INFO [org.littlewings.javaee7.cdi.RequestScopedMessageService] (DefaultQuartzScheduler_Worker-5) Hello RequestScopedMessageService@2047151614, now = 2016-08-12T01:32:30.015 01:32:30,016 INFO [org.littlewings.javaee7.cdi.PseudoScopedMessageService] (DefaultQuartzScheduler_Worker-5) Hello PseudoScopedMessageService@818428427, now = 2016-08-12T01:32:30.015 01:32:30,016 INFO [org.littlewings.javaee7.cdi.ManualQuartzBasedJob] (DefaultQuartzScheduler_Worker-5) [ManualQuartzBasedJob] end job
その他、気になること
今回は特に試しませんでしたが、CRONのスケジュールを指定している部分には設定ファイルの内容を参照することもできるようです。
@Scheduled(cronExpression = "{myCronExpression}")
こちらを使うと、JobのスケジューリングをProjectStageごとに管理することもできるようですね。
また、Quartzではなく、独自のスケジューラーを実装することも可能ではあるようです。
まとめ
Apache DeltaSpikeのSchedule Moduleを試してみました。
内部実装はQuartzですが、CDI管理BeanをインジェクションできるJobを作成できるなど、ちょっと便利そうだなぁと思いました。
今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/javaee7-scala-examples/tree/master/cdi-deltaspike-scheduler