CLOVER🍀

That was when it all began.

Apache DeltaSpikeのScheduler Moduleを試す

Apache DeltaSpikeには、Scheduler Moduleというものがあり、こちらを使って定期的に動かすジョブを作成することができます。

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

Project Setup

アプリケーションの実行は、WildFlyへデプロイして確認するものとします。とりあえず、WildFlyを起動しておきましょう。

$ bin/standalone.sh

Jobを作成する

Apache DeltaSpikeのSchedule ModuleでJobを作成する方法は、以下の2つがあります。

  • org.quartz.Jobインターフェースを実装したクラスを作成する
  • java.lang.Runnableインターフェースを実装したクラスを作成する

いずれにしろ、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によって実現されています。

https://github.com/apache/deltaspike/blob/deltaspike-1.7.1/deltaspike/modules/scheduler/impl/src/main/java/org/apache/deltaspike/scheduler/impl/SchedulerExtension.java#L44

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 {

こうしておくと、コンテナ起動時にスケジューラーに登録されなくなります。

https://github.com/apache/deltaspike/blob/deltaspike-1.7.1/deltaspike/modules/scheduler/impl/src/main/java/org/apache/deltaspike/scheduler/impl/SchedulerExtension.java#L103

ただ、こうすると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}")

Configurable CRON expressions

こちらを使うと、JobのスケジューリングをProjectStageごとに管理することもできるようですね。

また、Quartzではなく、独自のスケジューラーを実装することも可能ではあるようです。

Custom Scheduler

まとめ

Apache DeltaSpikeのSchedule Moduleを試してみました。

内部実装はQuartzですが、CDI管理BeanをインジェクションできるJobを作成できるなど、ちょっと便利そうだなぁと思いました。

今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/javaee7-scala-examples/tree/master/cdi-deltaspike-scheduler