CLOVER🍀

That was when it all began.

CDIのインターセプターのかかり方を確認する

CDIを使うと、Client Proxy(擬似スコープの場合は拡張されたクラス?)がインジェクションされて(もしくはBeanManagerなどから取得)、それを使うことはわかりましたが、次に気になることとしてインターセプターのかかり方があります。

なにかというと、Client Proxyが使われるくらいだから、きっとインジェクションされたインスタンス越しに使わないとインターセプターってかからないんだろうなぁと思いまして。今回、それを確認してみます。

自分は仕事柄、Seasar2をメインでDI×AOPコンテナとして使っていることが多いのですが、このあたりは押さえておきたいなぁと。

それでは、いってみましょう。Weld SEを使います。

準備

ビルド定義。
build.sbt

name := "cdi-interceptor-apply-point"

version := "1.0"

scalaVersion := "2.11.6"

organization := "org.littlewings"

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

updateOptions := updateOptions.value.withCachedResolution(true)

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

インターセプターの作成

今回は、文字列を返すメソッドに対して「★」を最初と最後に付与するインターセプターを作ってみます。

アノテーションの作成。
src/main/java/org/littlewings/javaee7/interceptor/AddStar.java

package org.littlewings.javaee7.interceptor;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.interceptor.InterceptorBinding;

@InterceptorBinding
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AddStar {
}

アノテーションなので、Javaで書いています…。

インターセプター。
src/main/scala/org/littlewings/javaee7/interceptor/AddStarInterceptor.scala

package org.littlewings.javaee7.interceptor

import javax.enterprise.context.Dependent
import javax.interceptor.{InvocationContext, AroundInvoke, Interceptor}

@Interceptor
@AddStar
@Dependent
@SerialVersionUID(1L)
class AddStarInterceptor extends Serializable {
  @AroundInvoke
  @throws(classOf[Exception])
  def invoke(ic: InvocationContext): Any =
    ic.proceed() match {
      case s: String => "★" + s + "★"
      case other => other
    }
}

インターセプターを適用するために、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">
    <interceptors>
        <class>org.littlewings.javaee7.interceptor.AddStarInterceptor</class>
    </interceptors>
</beans>

インターセプターを適用する対象のクラス

続いて、インターセプターを適用するクラスを作成します。単純ですが、String#join的なもので。

Normal Scopeなクラス。
src/main/scala/org/littlewings/javaee7/interceptor/NormalScopedMessageService.scala

package org.littlewings.javaee7.interceptor

import javax.enterprise.context.ApplicationScoped

@ApplicationScoped
@SerialVersionUID(1L)
class NormalScopedMessageService extends Serializable {
  @AddStar
  def join(separator: String, tokens: String*): String =
    tokens.mkString(separator)

  @AddStar
  def joinChain(separator: String, tokens: String*): String = join(separator, tokens: _*)
}

ここではメソッドを2つ用意し、片方は内部で別のアノテーションを付与したメソッドを呼び出しています。

擬似スコープなクラス。
src/main/scala/org/littlewings/javaee7/interceptor/PseudoScopedMessageService.scala

package org.littlewings.javaee7.interceptor

import javax.enterprise.context.Dependent

@Dependent
@SerialVersionUID(1L)
class PseudoScopedMessageService extends Serializable {
  @AddStar
  def join(separator: String, tokens: String*): String =
    tokens.mkString(separator)

  @AddStar
  def joinChain(separator: String, tokens: String*): String = join(separator, tokens: _*)
}

やっていることは、Normal Scopeなものと同じです。

テストコードで確認

あとは、テストコードで確認してみます。

テストコードの骨格実装。
src/test/scala/org/littlewings/javaee7/interceptor/CdiInterceptorSpec.scala

package org.littlewings.javaee7.interceptor

import javax.enterprise.inject.spi.CDI

import org.jboss.weld.environment.se.Weld
import org.scalatest.FunSpec
import org.scalatest.Matchers._

class CdiInterceptorSpec extends FunSpec {
  describe("CDI Interceptor Spec") {
    // ここにテストを書く!
  }

  private def withWeld(f: => Unit): Unit = {
    val weld = new Weld

    try {
      weld.initialize()

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

Weldの起動/停止を行う簡易メソッド付き。

では、順に見ていきます。

Nomal Scope。

    it("Normal Scoped") {
      withWeld {
        val messageService = CDI.current.select(classOf[NormalScopedMessageService]).get

        messageService.join(", ", "Hello", "World") should be("★Hello, World★")
      }
    }

インターセプターが効いています。

擬似スコープ。

    it("Pseudo Scoped") {
      withWeld {
        val messageService = CDI.current.select(classOf[PseudoScopedMessageService]).get

        messageService.join(", ", "Hello", "World") should be("★Hello, World★")
      }
    }

こちらもOKです。

続いて、Normal Scopeでインターセプター適用対象のメソッドをインスタンス内で続けて呼ぶパターン。

    it("Normal Scoped, Method Call Chain") {
      withWeld {
        val messageService = CDI.current.select(classOf[NormalScopedMessageService]).get

        messageService.joinChain(", ", "Hello", "World") should be("★Hello, World★")
      }
    }

この結果を見ると、1回しか適用されていないようです。ということは、やっぱりClient Proxy越しに実行しないと効かないということですね。

擬似スコープの場合も、同じ。

    it("Pseudo Scoped, Method Call Chain") {
      withWeld {
        val messageService = CDI.current.select(classOf[PseudoScopedMessageService]).get

        messageService.joinChain(", ", "Hello", "World") should be("★Hello, World★")
      }

    }

バリエーションの追加

あとは、ちょっと意地悪気味?ですが、アクセス修飾子も作用するかどうか見てみます。

こういうクラスを作成。突然Javaになりましたが、キニシナイ。
src/main/java/org/littlewings/javaee7/interceptor/AccessModifierMessageService.java

package org.littlewings.javaee7.interceptor;

import java.io.Serializable;
import javax.enterprise.context.Dependent;

@Dependent
public class AccessModifierMessageService implements Serializable {
    private static final long serialVersionUID = 1L;

    @AddStar
    public String join(String separator, String... tokens) {
        return String.join(separator, tokens);
    }

    @AddStar
    String joinPackagePrivate(String separator, String... tokens) {
        return String.join(separator, tokens);
    }
}

やっていることは先ほどまでのクラスと同じですが、片方のメソッドがパッケージプライベートです。

これを、このようなクラスから呼び出してみます。
src/main/scala/org/littlewings/javaee7/interceptor/MixedMessageService.scala

package org.littlewings.javaee7.interceptor

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

@Dependent
@SerialVersionUID(1L)
class MixedMessageService extends Serializable {
  @Inject
  private var delegate: AccessModifierMessageService = _

  @AddStar
  def joinChain(separator: String, tokens: String*): String =
    delegate.join(separator, tokens: _*)

  @AddStar
  def joinChainToPackagePrivate(separator: String, tokens: String*): String =
    delegate.joinPackagePrivate(separator, tokens: _*)
}

それぞれ、先ほどJavaで作成したクラスを呼び出すような実装になっています。

テスト。

    it("Object Chain") {
      withWeld {
        val messageService = CDI.current.select(classOf[MixedMessageService]).get

        messageService.joinChain(", ", "Hello", "World") should be("★★Hello, World★★")
        messageService.joinChainToPackagePrivate(", ", "Hello", "World") should be("★★Hello, World★★")
      }
    }

同じクラス内のインスタンスメソッド呼び出しに効かないのは先ほどの通りですが、インジェクションされたインスタンス越しであればpublicでなくても効きそう?

まあ、普通はしないと思いますが…。

というわけで

インターセプターが効くのは、インジェクションされた、もしくはBeanManagerなどから取得したインスタンス越しに実行したその時だけ、ということでいいのかな。

また、別のバリエーションとして、インスタンス内で呼び出すメソッドに対して別のアノテーションを付与しても、やっぱり効きませんでした。
※同じ種類のインターセプターを重複してかけてたのが、良くなかったかどうかの確認です(同じインターセプターだから2回効かない、とかだったら紛らわしいなぁと)

とりあえず覚えておきましょう。

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