CLOVER🍀

That was when it all began.

はじめてのjBatch

このあたりのエントリを見て、ちょっとやってみようかなぁと思いまして。

JSR352-Batch Applicationを試してみた(Batchlet編)
http://siosio.hatenablog.com/entry/2015/06/06/011830

JSR352-Batch Applicationを試してみた(BatchletでDBアクセス-JPA編)
http://siosio.hatenablog.com/entry/2015/06/07/151425

その他、ちょっと目を通したのはこのあたり。

Jbatch実践入門 #jdt2015
http://www.slideshare.net/agetsuma/jbatch-jdt2015

The Java EE 7 TutorialのjBatchの章をテキトーに訳した
http://kagamihoge.hatenablog.com/entry/2014/04/10/205918

jBatchを触ってみるのは初めてなのですが、テキトーに始めたらいろいろハマりましたので、そのあたりのメモも含めて…。

テーマ

今回は、とりあえずこんな感じでやってみます。

  • Batchletで実装
  • CDI+JPAくらいは一緒に使ってみる
  • ジョブの起動は、@Scheduleで
  • バッチ実行時にプロパティとか渡してみたい

このあたりを踏まえて書いていきます。

準備

実行環境は、WildFly 8.2.0.Finalとします。言語とビルドは、Scala+sbtで。

というわけで、ビルド定義。
build.sbt

name := "jbatch-getting-started"

version := "0.0.1-SNAPSHOT"

scalaVersion := "2.11.6"

organization := "org.littlewings"

scalacOptions := Seq("-Xlint", "-deprecation", "-unchecked", "-feature")

updateOptions := updateOptions.value.withCachedResolution(true)

jetty()

webInfClasses in webapp := true

artifactName := { (version: ScalaVersion, module: ModuleID, artifact: Artifact) =>
  //artifact.name + "." + artifact.extension
  "javaee7-web." + artifact.extension
}

val jettyVersion = "9.2.11.v20150529"

libraryDependencies ++= Seq(
  "org.eclipse.jetty" % "jetty-webapp" % jettyVersion % "container",
  "org.eclipse.jetty" % "jetty-plus"   % jettyVersion % "container",
  "javax" % "javaee-api" % "7.0" % "provided",
  "org.jboss.logging" % "jboss-logging" % "3.1.4.GA" % "provided"
)

「javaee-web-api」ではなくて、「javaee-api」を使うのも初めてですね。ログ出力のために、JBoss Loggingも入れています。

xsbt-web-pluginも使用します。
project/plugins.sbt

logLevel := Level.Warn

addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "1.1.0")

Batchletを実装する

まずはBatchletを実装してみます。
src/main/scala/org/littlewings/javaee7/batch/MyBatchlet.scala

package org.littlewings.javaee7.batch

import javax.batch.api.{BatchProperty, AbstractBatchlet}
import javax.batch.runtime.context.JobContext
import javax.enterprise.context.Dependent
import javax.inject.{Inject, Named}

import org.jboss.logging.Logger
import org.littlewings.javaee7.service.LanguageService

@Named
@Dependent
class MyBatchlet extends AbstractBatchlet {
  @Inject
  private var jobContext: JobContext = _

  @transient
  private val logger: Logger = Logger.getLogger(getClass)

  @Inject
  @BatchProperty
  private var message: String = _

  @Inject
  @BatchProperty
  private var id: String = _

  @Inject
  private var languageService: LanguageService = _

  @throws(classOf[Exception])
  override def process(): String = {
    logger.infof("***** start process MyBatchlet job. *****")
    logger.infof("job name = %s", jobContext.getJobName)

    val properties = jobContext.getProperties

    logger.infof("properties jobProperty = %s", properties.getProperty("jobProperty"))
    logger.infof("batch property message = %s", message)

    val language = languageService.findById(id.toLong)

    logger.infof("found entity, %s", language)
    logger.infof("***** end process MyBatchlet job. *****")

    "done."
  }
}

バッチの定義ファイルからプロパティを取得する用に、ここでは宣言。

  @Inject
  @BatchProperty
  private var message: String = _

  @Inject
  @BatchProperty
  private var id: String = _

その他、ジョブ全体の定義からもプロパティを取得するようにしています。

    logger.infof("properties jobProperty = %s", properties.getProperty("jobProperty"))

あとは、CDI管理BeanからJPAでEntityを取得してログ出力。

    val language = languageService.findById(id.toLong)

CDI管理BeanとEntity

先ほどのBatchletで使っているCDI管理Beanと、Entityです。
src/main/scala/org/littlewings/javaee7/service/LanguageService.scala

package org.littlewings.javaee7.service

import javax.enterprise.context.ApplicationScoped
import javax.persistence.{EntityManager, PersistenceContext}
import javax.transaction.Transactional

import org.littlewings.javaee7.entity.Language

import scala.collection.JavaConverters._

@ApplicationScoped
@Transactional
class LanguageService {
  @PersistenceContext
  private var entityManager: EntityManager = _

  def findById(id: Long): Language =
    entityManager.find(classOf[Language], id)

  def findAll: Seq[Language] = {
    val query =
      entityManager.createQuery("SELECT l FROM Language l ORDER BY l.id", classOf[Language])
    query.getResultList.asScala
  }
}

このあたりは、まあ単純です。
src/main/scala/org/littlewings/javaee7/entity/Language.scala

package org.littlewings.javaee7.entity

import javax.persistence.{Column, Entity, Id, Table}

import scala.beans.BeanProperty

@Entity
@Table(name = "language")
@SerialVersionUID(1L)
class Language extends Serializable {
  @Id
  @BeanProperty
  var id: Long = _

  @Column
  @BeanProperty
  var name: String = _

  override def toString: String =
    s"Language id[${id}], name[${name}]"
}

persistence.xmlも載せておきましょう。
src/main/resources/META-INF/persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
                                 http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
             version="2.1">
    <persistence-unit name="javaee7.web.pu" transaction-type="JTA">
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <jta-data-source>java:jboss/datasources/MysqlDs</jta-data-source>
        <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect"/>
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
        </properties>
    </persistence-unit>
</persistence>

データは、このようなものが入っているとします。

mysql> SELECT * FROM language;
+----+---------+
| id | name    |
+----+---------+
|  1 | Java    |
|  2 | Scala   |
|  3 | Groovy  |
|  4 | Clojure |
|  5 | Perl    |
+----+---------+
5 rows in set (0.00 sec)

ジョブの起動

ジョブの起動は、今回はTimerを使ってみました。
src/main/scala/org/littlewings/javaee7/service/JobService.scala

package org.littlewings.javaee7.service

import java.util.Properties
import javax.batch.runtime.BatchRuntime
import javax.ejb.{Schedule, Singleton}

import org.jboss.logging.Logger

import scala.util.Random

@Singleton
class JobService {
  @transient
  val logger: Logger = Logger.getLogger(getClass)

  @Schedule(second = "*/5", minute = "*", hour = "*", persistent = false)
  def executeJob(): Unit = {
    logger.infof("start job service.")

    val properties = new Properties
    properties.setProperty("id", (new Random().nextInt(5) + 1).toString)

    val jobOperator = BatchRuntime.getJobOperator
    val executionId = jobOperator.start("myJob", properties)

    logger.infof("end job service, executionId[%d]", executionId)
  }
}

今回は、起動スケジュールは5秒に1回…。

  @Schedule(second = "*/5", minute = "*", hour = "*", persistent = false)

Batchletが「id」として参照していたプロパティですが、今回は起動元が設定する用にしました。

    val properties = new Properties
    properties.setProperty("id", (new Random().nextInt(5) + 1).toString)

ジョブの定義

ジョブの定義は、ここまでのコードを踏まえるとこのようになりました。
src/main/resources/META-INF/batch-jobs/myJob.xml

<?xml version="1.0" encoding="UTF-8"?>
<job id="myJob" xmlns="http://xmlns.jcp.org/xml/ns/javaee" version="1.0">
    <properties>
        <property name="jobProperty" value="Hello MyJob!!"/>
    </properties>

    <step id="step">
        <batchlet ref="myBatchlet">
            <properties>
                <property name="message" value="Hello MyBatchlet!!"/>
                <property name="id" value="#{jobParameters['id']}"/>
            </properties>
        </batchlet>
    </step>
</job>

Batchletで、JobContext#getPropertiesから取得しているプロパティは

    logger.infof("properties jobProperty = %s", properties.getProperty("jobProperty"))

こちらの定義に。

    <properties>
        <property name="jobProperty" value="Hello MyJob!!"/>
    </properties>

@BatchPropertyでインジェクションしているプロパティは

  @Inject
  @BatchProperty
  private var message: String = _

  @Inject
  @BatchProperty
  private var id: String = _

こちらの定義です。

            <properties>
                <property name="message" value="Hello MyBatchlet!!"/>
                <property name="id" value="#{jobParameters['id']}"/>
            </properties>

なので、idは動的に決まる内容になっています。

実行

それでは、WildFlyにデプロイして動作させてみます。

16:35:25,007 INFO  [org.littlewings.javaee7.batch.MyBatchlet] (Batch Thread - 5) ***** start process MyBatchlet job. *****
16:35:25,008 INFO  [org.littlewings.javaee7.batch.MyBatchlet] (Batch Thread - 5) job name = myJob
16:35:25,008 INFO  [org.littlewings.javaee7.batch.MyBatchlet] (Batch Thread - 5) properties jobProperty = Hello MyJob!!
16:35:25,008 INFO  [org.littlewings.javaee7.batch.MyBatchlet] (Batch Thread - 5) batch property message = Hello MyBatchlet!!
16:35:25,012 INFO  [org.littlewings.javaee7.batch.MyBatchlet] (Batch Thread - 5) found entity, Language id[1], name[Java]
16:35:25,013 INFO  [org.littlewings.javaee7.batch.MyBatchlet] (Batch Thread - 5) ***** end process MyBatchlet job. *****

とか

:35:45,009 INFO  [org.littlewings.javaee7.batch.MyBatchlet] (Batch Thread - 9) ***** start process MyBatchlet job. *****
16:35:45,009 INFO  [org.littlewings.javaee7.batch.MyBatchlet] (Batch Thread - 9) job name = myJob
16:35:45,010 INFO  [org.littlewings.javaee7.batch.MyBatchlet] (Batch Thread - 9) properties jobProperty = Hello MyJob!!
16:35:45,010 INFO  [org.littlewings.javaee7.batch.MyBatchlet] (Batch Thread - 9) batch property message = Hello MyBatchlet!!
16:35:45,018 INFO  [org.littlewings.javaee7.batch.MyBatchlet] (Batch Thread - 9) found entity, Language id[2], name[Scala]
16:35:45,018 INFO  [org.littlewings.javaee7.batch.MyBatchlet] (Batch Thread - 9) ***** end process MyBatchlet job. *****

こんな感じで動きます。

ハマったところ

けっこう適当に始めたので、いろいろ踏みました(笑)。ちゃんと調べていないだけとも…。

CDIでのインジェクションに失敗する

最初、Batchletに@Namedを付与せずに

@Dependent
class MyBatchlet extends AbstractBatchlet {

ジョブの定義にはBatchletのFQCNを書いていたのですが

    <step id="step">
        <batchlet ref="org.littlewings.javaee7.batch.MyBatchlet">

CDI管理Beanのインジェクションが全然動かなくて、かなり悩みました。

で、あれ〜と思っていたのですが、ここを読んで原因がわかる…。

・作成したバッチアーティファクトにNamedアノテーションを付与してCDI管理Beanを定義します。
・バッチアーティファクトに、public・中身が空・引数なしのコンストラクタを作成します。
・ジョブ定義ファイルで、完全修飾クラス名(FQCN)の代わりに、バッチアーティファクトをCDIネームで指定します。
・javax.enterprise.context.Dependentアノテーションをバッチアーティファクトに付与するか空のbeans.xmlを含めるかして、モジュールをCDIBeanアーカイブにします。

Note:CDIはバッチアーティファクトでバッチランタイムからコンテキストオブジェクトを取得するために必要です。

以上の手順に従わない場合は下記のようなエラーが発生します。

・バッチランタイムがバッチアーティファクトを特定できない。
・バッチアーティファクトがDIされたオブジェクトにアクセスするときNullPointerExceptionをスローする。

The Java EE 7 TutorialのjBatchの章をテキトーに訳した - kagamihogeの日記

こ・れ・だ。

というわけで、@Namedを付けました、と。自分がJava EE使っている時に@Namedって使う機会がなかったので、避けてたらいい感じに地雷になりました…。

JobContext#getPropertyからプロパティ値が取れない

バッチの定義ファイルにプロパティを書けと、たとえ動的であっても。

なので、今回は「id」が動的な定義になりますが、以下のように設定を記述しています。こうすると、プロパティの
取得(今回は「id」)が取得できるようになります。

            <properties>
                <property name="message" value="Hello MyBatchlet!!"/>
                <property name="id" value="#{jobParameters['id']}"/>
            </properties>
sbtでビルドすると、WEB-INF/classes/META-INF配下にファイルがない

完全に忘れていましたが、今のsbt+xsbt-web-pluginでWARを作成すると、プロジェクトの.classファイルなどは全部JARにアーカイブされた上でWEB-INF/libに置かれるので、デフォルトではWEB-INF/classesの配下にほぼ物がありません。

これだと困るので、以下の設定を追加。

webInfClasses in webapp := true

これで、JARにアーカイブするのではなく、普通にWEB-INF/classes配下に置かれるようになりました。もちろん、META-INF配下のファイルも含めて。

まとめ

とまあ、余計なことしてどハマりしましたが、とりあえずBatchletは動かすことができました。

今後jBatchを継続してみていくかというと、ちょっと微妙ですが…。ジョブスケジュールの設定とか、WARの中にアーカイブするってどうなんでしょう?

個人的には、やるならSE環境で動かすのなら少し追ってみてもいいかなーという気がします。

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