CLOVER🍀

That was when it all began.

CDIのBeanManagerを使う

今まで@Injectとか@Qualifierとかを使ったインジェクションは少し試してきましたが、手動でBeanを取得するようなやり方はやってこなかったので、今回試してみることにしました。

Seasar2でいうとS2Containerを直接使う、みたいな感じですね。

CDIだと、BeanManagerというものを使うみたいです。

サンプルを作るにあたり、このあたりを参考にさせていただきました。

BeanManagerでCDIコンテナにアクセスする
http://tanoseam.wordpress.com/2011/11/03/beanmanager/
[Java] BeanManager: Obtain Contextual Reference to Beans (TOTD #215)
http://orablogs-jp.blogspot.jp/2013/07/beanmanager-obtain-contextual-reference.html
BeanManagerからManagedBeanを取得する
http://inya-bauer.blogspot.jp/2011/01/beanmanagermanagedbean.html

準備

とりあえず、依存関係などの準備をしましょう。

今回は、Aqruillianを使わずに素直にWARをアプリケーションサーバにデプロイすることにします。
build.sbt

name := "bean-manager-lookup"

version := "0.0.1-SNAPSHOT"

scalaVersion := "2.10.3"

organization := "org.littlewings"

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

seq(webSettings: _*)

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

libraryDependencies ++= Seq(
  "org.eclipse.jetty" % "jetty-webapp" % "9.1.0.v20131115" % "container",
  "org.eclipse.jetty" % "jetty-plus"   % "9.1.0.v20131115" % "container",
  "javax" % "javaee-web-api" % "6.0" % "provided"
)

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

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

Java EE 6でやっているので、CDIを有効化するために、以下のファイルを中身は空で配置。

src/main/webapp/WEB-INF/beans.xml

では、コードを書いていってみましょう。

Bean

今回、Beanとして以下の2つを用意しました。
src/main/scala/org/littlewings/javaee6/service/CalcService.scala

package org.littlewings.javaee6.service

class CalcService {
  def add(left: Int, right: Int): Int =
    left + right

  def multiply(left: Int, right: Int): Int =
    left * right
}

EJBではありません。

src/main/scala/org/littlewings/javaee6/service/DateService.scala

package org.littlewings.javaee6.service

import java.util.Date

import javax.enterprise.context.ApplicationScoped

@ApplicationScoped
class DateService {
  private[this] val thisTime: Date = new Date

  def time: Date = thisTime
}

こちらは、@ApplicationScopedアノテーションを付与しました。

これらを、CDIのBeanManagerから取得します。

呼び出し元

WARとしてデプロイするので、リクエストを受け付ける役割はJAX-RSでやることに。Beanの取得も、ここでやります。

まずは有効化。
src/main/scala/org/littlewings/javaee6/jaxrs/JaxrsApplication.scala

package org.littlewings.javaee6.jaxrs

import javax.ws.rs.ApplicationPath
import javax.ws.rs.core.Application

@ApplicationPath("/rest")
class JaxrsApplication extends Application

パスは「/rest」で。

BeanManagerを取得するには、JNDIルックアップするか、BeanManager自体を@InjectアノテーションでDIすればよいみたいです。

まずは、JNDIルックアップの方から。
src/main/java/org/littlewings/javaee6/jaxrs/JndiLookupResource.java

package org.littlewings.javaee6.jaxrs;

import java.util.Set;

import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;

import org.littlewings.javaee6.service.CalcService;
import org.littlewings.javaee6.service.DateService;

@Path("jndi")
public class JndiLookupResource {
    @GET
    @Path("calc")
    @Produces(MediaType.TEXT_PLAIN)
    public String calc(@QueryParam("p1") int p1, @QueryParam("p2") int p2) throws NamingException {
        InitialContext ic = new InitialContext();
        BeanManager bm = (BeanManager) ic.lookup("java:comp/BeanManager");

        Set<Bean<?>> beans = bm.getBeans(CalcService.class);
        Bean<?> bean = bm.resolve(beans);
        CalcService calcService =
            (CalcService) bm.getReference(bean, CalcService.class, bm.createCreationalContext(bean));

        return Integer.toString(calcService.add(p1, p2)) + System.lineSeparator();
    }

    @GET
    @Path("date")
    @Produces(MediaType.TEXT_PLAIN)
    public String date() throws NamingException {
        InitialContext ic = new InitialContext();
        BeanManager bm = (BeanManager) ic.lookup("java:comp/BeanManager");

        Set<Bean<?>> beans = bm.getBeans(DateService.class);
        Bean<?> bean = bm.resolve(beans);
        DateService dateService =
            (DateService) bm.getReference(bean, DateService.class, bm.createCreationalContext(bean));

        return dateService.time().toString() + System.lineSeparator();
    }
}

なぜか、ソースコードJavaになりました。まあ、いろいろあったんですよ…。

BeanManagerを取得したら、BeanManager#getBeansでBean(このBeanは、javax.enterprise.inject.spi.Beanインターフェース)のSetを取得し、BeanManager#resolveで特定、BeanManager#getReferenceでBean(同じくjavax.enterprise...)のインスタンスを取得できるようです。

BeanManager#getBeansには、Qualifierを指定して絞り込めるみたい?また、文字列でBean名を指定できるバージョンもあります。

続いて、@Injectアノテーションで注入するバージョン。
src/main/java/org/littlewings/javaee6/jaxrs/InjectResource.java

package org.littlewings.javaee6.jaxrs;

import java.util.Set;

import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;

import org.littlewings.javaee6.service.CalcService;
import org.littlewings.javaee6.service.DateService;

@Path("inject")
public class InjectResource {
    @Inject
    private BeanManager bm;

    @GET
    @Path("calc")
    @Produces(MediaType.TEXT_PLAIN)
    public String calc(@QueryParam("p1") int p1, @QueryParam("p2") int p2) {
        Set<Bean<?>> beans = bm.getBeans(CalcService.class);
        Bean<?> bean = bm.resolve(beans);
        CalcService calcService =
            (CalcService) bm.getReference(bean, CalcService.class, bm.createCreationalContext(bean));

        return Integer.toString(calcService.add(p1, p2)) + System.lineSeparator();
    }

    @GET
    @Path("date")
    @Produces(MediaType.TEXT_PLAIN)
    public String date() {
        Set<Bean<?>> beans = bm.getBeans(DateService.class);
        Bean<?> bean = bm.resolve(beans);
        DateService dateService =
            (DateService) bm.getReference(bean, DateService.class, bm.createCreationalContext(bean));

        return dateService.time().toString() + System.lineSeparator();
    }
}

これを、JBoss AS 7.1.1にデプロイ。コンテキスト名は、「javaee6-web」とします。

まずは、CalcServiceクラスを使用。

# JNDIルックアップ版
$ curl "http://localhost:8080/javaee6-web/rest/jndi/calc?p1=3&p2=6"
9
# @Inject版
$ curl "http://localhost:8080/javaee6-web/rest/inject/calc?p1=3&p2=6"
9

続いて、DateServiceクラスを使用。こちらは、@ApplicationScopedでしたね。

# JNDIルックアップ版
$ curl "http://localhost:8080/javaee6-web/rest/jndi/date"
Sat Feb 08 16:09:30 JST 2014
# @Inject版
$ curl "http://localhost:8080/javaee6-web/rest/inject/date"
Sat Feb 08 16:09:30 JST 2014

ちゃんと、同じインスタンスを見てくれてそうですね。

とりあえず、BeanManagerを使ってBeanを取得することができました。

今回書いたサンプルは、こちらにアップしています。
https://github.com/kazuhira-r/javaee6-scala-examples/tree/master/bean-manager-lookup

余談

今回、BeanManagerを使った部分は、最初はScalaではなくJavaで書いていました。が、BeanManager#getBeansの部分から先がうまくいかず、結局断念。

一応、こんなことを書いていました。
src/main/scala/org/littlewings/javaee6/jaxrs/JndiLookupAsScalaResource.scala

package org.littlewings.javaee6.jaxrs

import javax.enterprise.inject.spi.{Bean, BeanManager}
import javax.naming.InitialContext
import javax.ws.rs.{GET, Path, Produces, QueryParam}
import javax.ws.rs.core.MediaType

import org.littlewings.javaee6.service.{CalcService, DateService}

@Path("jndi-as-scala")
class JndiLookupResourceAsScala {
  @GET
  @Path("calc")
  @Produces(Array(MediaType.TEXT_PLAIN))
  def calc(@QueryParam("p1") p1: Int, @QueryParam("p2") p2: Int): String = {
    val initialContext = new InitialContext
    val bm: BeanManager =
      initialContext.lookup("java:comp/BeanManager").asInstanceOf[BeanManager]

    // このコードは、コンパイルできない  
    val beans: java.util.Set[Bean[_]] = bm.getBeans(classOf[CalcService])
    val bean: Bean[_] = bm.resolve(beans)
    val calcService =
      bm
        .getReference(bean, classOf[CalcService], bm.createCreationalContext(bean))
        .asInstanceOf[CalcService]

    calcService.add(p1, p2).toString
  }
}

これをコンパイルすると、こんなことになります。

> compile
[info] Compiling 1 Scala source to /xxxxx/target/scala-2.10/classes...
[error] /xxxxx/src/main/scala/org/littlewings/javaee6/jaxrs/JndiLookupAsScalaResource.scala:22: no type parameters for method resolve: (x$1: java.util.Set[javax.enterprise.inject.spi.Bean[_ <: X]])javax.enterprise.inject.spi.Bean[_ <: X] exist so that it can be applied to arguments (java.util.Set[javax.enterprise.inject.spi.Bean[_]])
[error]  --- because ---
[error] argument expression's type is not compatible with formal parameter type;
[error]  found   : java.util.Set[javax.enterprise.inject.spi.Bean[_]]
[error]  required: java.util.Set[javax.enterprise.inject.spi.Bean[_ <: ?X]]
[error]     val bean: Bean[_] = bm.resolve(beans)
[error]                            ^
[error] /xxxx/src/main/scala/org/littlewings/javaee6/jaxrs/JndiLookupAsScalaResource.scala:22: type mismatch;
[error]  found   : java.util.Set[javax.enterprise.inject.spi.Bean[_]]
[error]  required: java.util.Set[javax.enterprise.inject.spi.Bean[_ <: X]]
[error] Note: javax.enterprise.inject.spi.Bean[_] >: javax.enterprise.inject.spi.Bean[_ <: X], but Java-defined trait Set is invariant in type E.
[error] You may wish to investigate a wildcard type such as `_ >: javax.enterprise.inject.spi.Bean[_ <: X]`. (SLS 3.2.10)
[error]     val bean: Bean[_] = bm.resolve(beans)
[error]                                    ^
[error] two errors found
[error] (compile:compile) Compilation failed

うまく型を扱えないみたい?

このあたりが類似(SLS 3.2.10)なんでしょうなぁと思いつつ、今回は放棄しましたが…(面倒になって、ちゃんと読んでない)。

More Scala typing issues
http://stackoverflow.com/questions/6141701/more-scala-typing-issues
Problem with bounded type parameterised case class and default args in Scala
http://stackoverflow.com/questions/6135663/problem-with-bounded-type-parameterised-case-class-and-default-args-in-scala
Scala, can't implement generic java method
http://stackoverflow.com/questions/11723211/scala-cant-implement-generic-java-method