2015-06-07
はじめての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を触ってみるのは初めてなのですが、テキトーに始めたらいろいろハマりましたので、そのあたりのメモも含めて…。
テーマ
今回は、とりあえずこんな感じでやってみます。
このあたりを踏まえて書いていきます。
準備
実行環境は、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 version="2.1" 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://www.oracle.com/webfolder/technetwork/jsc/xml/ns/persistence/persistence_2_1.xsd"> <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を定義します。
The Java EE 7 TutorialのjBatchの章をテキトーに訳した - kagamihogeの日記
・バッチアーティファクトに、public・中身が空・引数なしのコンストラクタを作成します。
・ジョブ定義ファイルで、完全修飾クラス名(FQCN)の代わりに、バッチアーティファクトをCDIネームで指定します。
・javax.enterprise.context.Dependentアノテーションをバッチアーティファクトに付与するか空のbeans.xmlを含めるかして、モジュールをCDIBeanアーカイブにします。
Note:CDIはバッチアーティファクトでバッチランタイムからコンテキストオブジェクトを取得するために必要です。
以上の手順に従わない場合は下記のようなエラーが発生します。
・バッチランタイムがバッチアーティファクトを特定できない。
・バッチアーティファクトがDIされたオブジェクトにアクセスするときNullPointerExceptionをスローする。
こ・れ・だ。
というわけで、@Namedを付けました、と。自分がJava EE使っている時に@Namedって使う機会がなかったので、避けてたらいい感じに地雷になりました…。
JobContext#getPropertyからプロパティ値が取れない
バッチの定義ファイルにプロパティを書けと、たとえ動的であっても。
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
- 80 https://www.google.co.jp/
- 13 http://t.co/mPnouWBBvD
- 13 http://www.google.co.jp/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&ved=0CB0QFjAA&url=http://d.hatena.ne.jp/Kazuhira/20121012/1350058511&ei=vPlzVZatCsLDmAWRw4K4Dg&usg=AFQjCNEwTHFKT7l5XbPRFS4FBk2LVuxFqA
- 6 http://www.google.co.jp/url?sa=t&rct=j&q=&esrc=s&source=web&cd=2&ved=0CCYQFjAB&url=http://d.hatena.ne.jp/Kazuhira/20120408/1333890311&ei=lwR0VdLlN8zX8gXgm4DYBA&usg=AFQjCNEtRCYuiXUWtkVjYnzqd_xn3_EEsg&bvm=bv.95039771,d.dGc
- 5 http://t.co/ubebja2Hwl
- 4 http://pipes.yahoo.com/pipes/pipe.info?_id=02db597254ec68550537866a2fca2ce6
- 4 http://pipes.yahoo.com/pipes/pipe.info?_id=8dda7c5265619c2fb368495a3d11b784
- 4 http://pipes.yahoo.com/pipes/pipe.info?_id=cd1c8b53a467958b474019a814a6496f
- 3 http://pipes.yahoo.com/pipes/pipe.info?_id=VPw6npu13RGKo15vBRNMsA
- 3 http://practical-scheme.net/wiliki/rssmix.cgi