CLOVER🍀

That was when it all began.

CDIのEventを使う

もうそろそろ一区切りにしようと思っている、CDI勉強シリーズ。個人的に、次に勉強すべきJava EEのテーマはあるのですが…やるのでしょうか…。

で、今回はCDIのEventを使ってみたいと思います。

CDIで、デザインパターンのObserverパターンをサポートしているということらしいです。

Observerパターン
http://www.techscore.com/tech/DesignPattern/Observer.html/

CDIのEventについてメモ
http://qiita.com/opengl-8080/items/73bddc7c744624c6ef9c

自分も手を動かしてやってみましょう。

確認環境は、Weld SEとします。

準備

まずは、ビルド定義。
build.sbt

name := "cdi-event"

version := "0.0.1-SNAPSHOT"

scalaVersion := "2.11.6"

organization := "org.littlewings"

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

updateOptions := updateOptions.value.withCachedResolution(true)

libraryDependencies ++= Seq(
  "org.jboss.weld.se" % "weld-se" % "2.2.12.Final",
  "org.scalatest" %% "scalatest" % "2.2.5" % "test"
)

CDIを使うために、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">
</beans>

また、テストコードを使って動作確認をしますが、Weldの起動/停止用にトレイトを用意しておきます。
src/test/scala/org/littlewings/javaee7/cdi/WeldSuiteSupport.scala

package org.littlewings.javaee7.cdi

import org.jboss.weld.environment.se.Weld
import org.scalatest.Suite

trait WeldSuiteSupport extends Suite {
  protected def withWeld(f: => Unit): Unit = {
    val weld = new Weld

    try {
      weld.initialize()

      f
    } finally {
      weld.shutdown()
    }
  }
}

テストコードでは、このトレイトをMix-inします。

はじめてのCDI Event

では、CDIのEventを使ったコードを書いていきます。登場するのは、この3つのクラスです。

  • イベントの通知をする人(Subject)
  • イベントの受け渡しに使われるもの
  • イベントの通知を受け取る人(Observer)

それぞれ実装していきます。

まずは、イベントの受け渡しに使われるクラスを定義してみます。今回は、ログインイベントとしてみましょう。
src/main/scala/org/littlewings/javaee7/cdi/LoginEvent.scala

package org.littlewings.javaee7.cdi

class LoginEvent(val firstName: String, val lastName: String)

これ自体は、何の変哲もないクラスです。

イベントを通知するSubject。CDI管理Beanとして作成し、Eventを@Injectしているところがポイントです。
src/main/scala/org/littlewings/javaee7/cdi/LoginSubject.scala

package org.littlewings.javaee7.cdi

import javax.enterprise.context.Dependent
import javax.enterprise.event.Event
import javax.inject.Inject

@Dependent
class LoginSubject {
  @Inject
  private var loginEvent: Event[LoginEvent] = _

  def login(firstName: String, lastName: String): Unit = {
    val event = new LoginEvent(firstName, lastName)
    loginEvent.fire(event)
  }
}

イベントを発生させるには、Event#fireにイベントで受け渡すクラスを渡せばよいみたいです。

そしてObserver。Observerは複数いてもよさそうなので、今回は2つ用意してみました。
src/main/scala/org/littlewings/javaee7/cdi/LoginObserver.scala

package org.littlewings.javaee7.cdi

import javax.enterprise.context.Dependent
import javax.enterprise.event.Observes

@Dependent
class LoginObserver {
  def observe(@Observes event: LoginEvent): Unit =
    println(s"${event.lastName + event.firstName}さんがログインしました!!")
}

@Dependent
class LoginObserver2 {
  def observe(@Observes event: LoginEvent): Unit =
    println(s"${event.lastName + event.firstName}さんがログインしたらしいですよ?")
}

イベントを受け取るメソッドには、@Observesを付与すればよいみたいだと。

それでは、動作確認してみます。
src/test/scala/org/littlewings/javaee7/cdi/LoginEventSpec.scala

package org.littlewings.javaee7.cdi

import javax.enterprise.inject.spi.CDI

import org.scalatest.FunSpec

class LoginEventSpec extends FunSpec with WeldSuiteSupport {
  describe("Login Event Spec") {
    // ここに、テストを書く!
  }
}

テストと言っても、対象のコードがprintlnしかしてないので、ただ動かすだけですが…。

では、イベントを通知する人(Subject)のメソッドを起動します。

    it("fire login event") {
      withWeld {
        val loginSubject = CDI.current.select(classOf[LoginSubject]).get
        loginSubject.login("カツオ", "磯野")
      }
    }

結果、このような出力が行われます。

磯野カツオさんがログインしました!!
磯野カツオさんがログインしたらしいですよ?

ちゃんと動いていますね。用意した、2つのObserverの両方が呼び出されています。

なお、BeanManager#fireEventでも同じことができるらしいです。

    it("fire login event by BeanManager") {
      withWeld {
        val beanManager = CDI.current.getBeanManager
        val event = new LoginEvent("ワカメ", "磯野")
        beanManager.fireEvent(event)
      }
    }

結果。

磯野ワカメさんがログインしました!!
磯野ワカメさんがログインしたらしいですよ?

@Qualifierを併用する

続いて、@Qualifierを合わせて使うことで、通知する対象を絞ったりできるのだとか。こちらも試してみます。

先ほどと同じ定義の、イベントの受け渡しに使うクラス。
src/main/scala/org/littlewings/javaee7/cdi/qualifier/LoginEvent.scala

package org.littlewings.javaee7.cdi.qualifier

class LoginEvent(val firstName: String, val lastName: String)

@Qualifierを用意します。@Adminと@Userという、2つの@Qualifierを用意しました。
src/main/java/org/littlewings/javaee7/cdi/qualifier/Admin.java

package org.littlewings.javaee7.cdi.qualifier;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.inject.Qualifier;

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PACKAGE, ElementType.TYPE})
public @interface Admin {
}

@User
src/main/java/org/littlewings/javaee7/cdi/qualifier/User.java

package org.littlewings.javaee7.cdi.qualifier;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.inject.Qualifier;

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PACKAGE, ElementType.TYPE})
public @interface User {
}
@Injectに@Qualifierを付与する

イベントを通知するSubject。
src/main/scala/org/littlewings/javaee7/cdi/qualifier/LoginSubject.scala

package org.littlewings.javaee7.cdi.qualifier

import javax.enterprise.context.Dependent
import javax.enterprise.event.Event
import javax.inject.Inject

@Dependent
class LoginSubject {
  @Inject
  private var loginEvent: Event[LoginEvent] = _

  def login(firstName: String, lastName: String): Unit = {
    val event = new LoginEvent(firstName, lastName)
    loginEvent.fire(event)
  }

  @User
  @Inject
  private var userLoginEvent: Event[LoginEvent] = _

  def loginAsUser(firstName: String, lastName: String): Unit = {
    val event = new LoginEvent(firstName, lastName)
    userLoginEvent.fire(event)
  }

  @Admin
  @Inject
  private var adminLoginEvent: Event[LoginEvent] = _

  def loginAsAdmin(firstName: String, lastName: String): Unit = {
    val event = new LoginEvent(firstName, lastName)
    adminLoginEvent.fire(event)
  }
}

各Eventを@Injectする際に、先ほど作成した@Qualifierを付与しています(最初のは、何も付与していませんが)。

通知を受け取るObserver。
src/main/scala/org/littlewings/javaee7/cdi/qualifier/LoginObserver.scala

package org.littlewings.javaee7.cdi.qualifier

import javax.enterprise.context.Dependent
import javax.enterprise.event.Observes

@Dependent
class LoginObserver {
  def loggined(@Observes event: LoginEvent): Unit =
    println(s"${event.lastName + event.firstName}さんがログインしました!!")
}

@Dependent
class AdminLoginObserver {
  def observe(@Observes @Admin event: LoginEvent): Unit =
    println(s"${event.lastName + event.firstName}さんが、管理者としてログインしました")
}

@Dependent
class UserLoginObserver {
  def observe(@Observes @User event: LoginEvent): Unit =
    println(s"${event.lastName + event.firstName}さんが、一般ユーザーとしてログインしました")
}

最初のObserverは@Observesのみですが、他のObserverは@Observersと先ほど作成した@Qualifierを使って受け取るイベントを絞り込みます。@Qualifierを付与しないと、同じ型のイベントを全部受け取るっぽいです。

確認してみましょう。
src/test/scala/org/littlewings/javaee7/cdi/qualifier/LoginEventSpec.scala

package org.littlewings.javaee7.cdi.qualifier

import javax.enterprise.inject.spi.CDI

import org.littlewings.javaee7.cdi.WeldSuiteSupport
import org.scalatest.FunSpec

class LoginEventSpec extends FunSpec with WeldSuiteSupport {
  // ここに、テストを書く!

@Qualifierなし。

    it("no qualifier") {
      withWeld {
        val loginSubject = CDI.current.select(classOf[LoginSubject]).get
        loginSubject.login("カツオ", "磯野")
      }
    }

結果。

磯野カツオさんがログインしました!!

@Qualifierがないもののみ、イベントが通知されています。

@Userの@Qualifierを使用。

    it("with @User qualifier") {
      withWeld {
        val loginSubject = CDI.current.select(classOf[LoginSubject]).get
        loginSubject.loginAsUser("マスオ", "フグ田")
      }
    }

結果。

フグ田マスオさんが、一般ユーザーとしてログインしました
フグ田マスオさんがログインしました!!

@Userを付与したObserverと、何も付与していないObserverが動作しています。

@Admin指定。

    it("with @Admin qualifier") {
      withWeld {
        val loginSubject = CDI.current.select(classOf[LoginSubject]).get
        loginSubject.loginAsAdmin("サザエ", "フグ田")
      }
    }

結果。

フグ田サザエさんがログインしました!!
フグ田サザエさんが、管理者としてログインしました

@Userと似た結果に。そりゃあそうですね。

Event#fireを呼び出す時に絞り込む

続いて、指定する@Qualifierを@Injectではなく、Event#fireの時に決めるようにしてみましょう。

こちらが、そのコードになります。
src/main/scala/org/littlewings/javaee7/cdi/qualifier/LoginSubjectBySelect.scala

package org.littlewings.javaee7.cdi.qualifier

import javax.enterprise.context.Dependent
import javax.enterprise.event.Event
import javax.enterprise.util.AnnotationLiteral
import javax.inject.Inject

@Dependent
class LoginSubjectBySelect {
  @Inject
  private var loginEvent: Event[LoginEvent] = _

  def login(firstName: String, lastName: String): Unit = {
    val event = new LoginEvent(firstName, lastName)
    loginEvent.fire(event)
  }

  def loginAsUser(firstName: String, lastName: String): Unit = {
    val event = new LoginEvent(firstName, lastName)
    loginEvent.select(new AnnotationLiteral[User] {}).fire(event)
  }

  def loginAsAdmin(firstName: String, lastName: String): Unit = {
    val event = new LoginEvent(firstName, lastName)
    loginEvent.select(classOf[LoginEvent], new AnnotationLiteral[Admin] {}).fire(event)
  }
}

Event#selectで絞り込めるみたいです。

  def loginAsUser(firstName: String, lastName: String): Unit = {
    val event = new LoginEvent(firstName, lastName)
    loginEvent.select(new AnnotationLiteral[User] {}).fire(event)
  }

また、select時に、Classクラスを渡せるのですが、これは@Inject時に型パラメーターに指定したクラスかそのサブクラスでなくてはいけないようです。今回は、意味はないですが…。

  def loginAsAdmin(firstName: String, lastName: String): Unit = {
    val event = new LoginEvent(firstName, lastName)
    loginEvent.select(classOf[LoginEvent], new AnnotationLiteral[Admin] {}).fire(event)
  }

確認。

    it("no qualifier") {
      withWeld {
        val loginSubject = CDI.current.select(classOf[LoginSubjectBySelect]).get
        loginSubject.login("カツオ", "磯野")
      }
    }

結果。

磯野カツオさんがログインしました!!

まあ、こちらは@Qualifierなしなので…。

@User指定。

    it("with @User qualifier") {
      withWeld {
        val loginSubject = CDI.current.select(classOf[LoginSubjectBySelect]).get
        loginSubject.loginAsUser("マスオ", "フグ田")
      }
    }

結果。

フグ田マスオさんが、一般ユーザーとしてログインしました
フグ田マスオさんがログインしました!!

@Injectの時と、同じ結果ですね。

@Admin指定。

    it("with @Admin qualifier") {
      withWeld {
        val loginSubject = CDI.current.select(classOf[LoginSubjectBySelect]).get
        loginSubject.loginAsAdmin("サザエ", "フグ田")
      }
    }

結果。

フグ田サザエさんがログインしました!!
フグ田サザエさんが、管理者としてログインしました

こちらも同様。

とりあえず使い方はわかりましたので、OKとしましょう。

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