読者です 読者をやめる 読者になる 読者になる

Weld SE で RequestScoped 云々を使ってみる

CDI を使っているクラスを Unit Test をするために、Wedl SE を使ってみました。
Weld: Download

Weld SE では、RequestScoped や SessionScoped のスコープが使えません。
Chapter 18. Application servers and environments supported by Weld

今回テスト対象のクラスは RequestScoped を使っていたので、このままでは動きません。
オブジェクトの生存期間は考慮せず、とりあえず Injection したいだけだったので下記サイトを参考に適当に実装してみました。
Simulating CDI's Session and Request Scope in a J2SE app | Dan Haywood
Weld-SE のバージョンは 2.1.2 Final を使ってます。
The Central Repository Search Engine

サンプルコードは相変わらず Xtend です。

Junit の Rule を使って Weld の生成/破棄を管理しています。

package sample.junit

import javax.enterprise.inject.spi.Extension
import org.jboss.weld.environment.se.Weld
import org.jboss.weld.environment.se.WeldContainer
import org.junit.rules.ExternalResource
import javax.enterprise.event.Observes
import javax.enterprise.inject.spi.AfterDeploymentValidation
import javax.enterprise.inject.spi.BeanManager
import javax.enterprise.context.SessionScoped
import javax.enterprise.context.RequestScoped
import java.lang.annotation.Annotation
import org.jboss.weld.manager.BeanManagerHelper
import org.jboss.weld.context.AbstractBoundContextHelper

class CDIInjector extends ExternalResource {
  var Weld weld
  var WeldContainer container
  override protected before() throws Throwable {
    weld = new Weld
    weld.addExtension(new WeldServletScopesSupportExtension)
    container = weld.initialize
  }
  
  override protected after() {
    weld.shutdown
  }
  
  def <T> getInstance(Class<T> clazz) {
    container.instance.select(clazz).get
  }
}
package class WeldServletScopesSupportExtension implements Extension {
  def afterDeployment(@Observes AfterDeploymentValidation event,
      BeanManager beanManager) {
    setContextActive(beanManager, SessionScoped);
    setContextActive(beanManager, RequestScoped);
  }
  private def setContextActive(BeanManager beanManager,
      Class<? extends Annotation> cls) {
    val context = BeanManagerHelper.getContext(beanManager, cls)
    AbstractBoundContextHelper.activate(context)
  }
}

Weld に addExtension でカスタム拡張を追加しています。
カスタム拡張が何をしているかというと、SessionScoped と RequestScoped を扱う Context を Active(デフォルトでは非Activeのため動かない)にしています。
public ではない API を使ってるので、Context を引っ張ってくるクラス/Active 化するクラスは パッケージを合わせています。

Context を取ってくるところはこちら。

package org.jboss.weld.manager

import javax.enterprise.inject.spi.BeanManager
import org.jboss.weld.bean.builtin.BeanManagerProxy
import java.lang.annotation.Annotation
import org.jboss.weld.context.AbstractBoundContext
import javax.enterprise.context.spi.Context
import org.jboss.weld.util.ForwardingContext

class BeanManagerHelper {
  def static getContext(BeanManager beanManager, Class<? extends Annotation> cls) {
    val proxy = beanManager as BeanManagerProxy
    val impl = proxy.delegate
    unwrapContext(impl.getContexts().get(cls).get(0))
  }
  private static def AbstractBoundContext<?> unwrapContext(Context context) {
    switch (context) {
      AbstractBoundContext<?> case true: return context
      ForwardingContext case true: unwrapContext(ForwardingContext.unwrap(context))
      default: throw new Exception('''«context.class»''')
    }
  }
}

BeanManagerProxy は決め打ち。
BeanManagerImple.getContexts が protected でした。

Context を Active 化するコードはこちら。

package org.jboss.weld.context

import java.util.HashMap
import org.jboss.weld.context.beanstore.MapBeanStore
import org.jboss.weld.context.beanstore.SimpleNamingScheme
import org.jboss.weld.context.bound.BoundRequestContext
import org.jboss.weld.context.bound.BoundSessionContext

class AbstractBoundContextHelper {
  def static void activate(AbstractBoundContext<?> context) {
    context.beanStore = new MapBeanStore(getNamingScheme(context), new HashMap)
    context.activate
  }
  def static getNamingScheme(AbstractBoundContext<?> context) {
    new SimpleNamingScheme(switch (context) {
      BoundRequestContext case true: BoundRequestContext.name
      BoundSessionContext case true: BoundSessionContext.name
    })
  }
}

生存期間を管理しないので、Store には HashMap を使ってます。
こちらは AbstractBoundContext.setBeanStore が protected です。

テストするクラスはこちら。JAX-RS 使ってますがどうでもいいです。

package sample.weldsetest

import javax.enterprise.context.RequestScoped
import javax.inject.Inject
import javax.ws.rs.GET
import javax.ws.rs.Path
import javax.ws.rs.QueryParam
import javax.enterprise.context.ApplicationScoped

@RequestScoped
@Path("hello")
class Hello {
  @Inject Dao dao

  @GET
  def String sayHello(@QueryParam("id") String id) '''Hello, «dao.getName(id)»'''
}

interface Dao {
  def String getName(String id)
}
@ApplicationScoped
class SimpleDaoImpl implements Dao {
  override getName(String id) '''Name:«id»'''
}

jUnit のテストがこちら

package sample.weldsetest

import org.junit.Test
import sample.junit.CDIInjector
import static extension xtendjunit.AssertExtensions.*
import org.junit.Rule

class HelloTest {
  @Rule public extension CDIInjector injector = new CDIInjector
  
  @Test  def void sayHello() {
    val target = getInstance(Hello)
    target.sayHello("aa").is("Hello, Name:aa")
  }
}

ビルドは gradle 使ってます。

apply plugin: 'java'
apply plugin: 'war'

repositories {
  mavenCentral()
}

dependencies {
  compile ('org.eclipse.xtend:org.eclipse.xtend.lib:2.5.3') {
    exclude module: 'guava'
  }
  compile 'com.google.guava:guava:16.0.1'
  testCompile 'org.jboss.weld.se:weld-se:2.1.2.Final'
  testCompile 'junit:junit:4.11'
  testCompile files('c:/work/github/xtendjunit/build/libs/xtendjunit-0.0.1.jar')
  providedCompile 'javax:javaee-web-api:7.0'
}

sourceSets {
  main {
    java { srcDir 'src/main/xtend-gen' }
  }
  test {
    java { srcDir 'src/test/xtend-gen' }
  }
}

sourceCompatibility = targetCompatibility = '1.7'
def defaultEncoding = 'UTF-8'
tasks.withType(AbstractCompile) each { it.options.encoding = defaultEncoding }

webAppDirName = "WebContent"

EclipsejUnit を動かす時は、クラスパスの設定に注意が必要でした。
Gradle の依存関係を GlassFish より上にしておかないと、weld のバージョンが 2.0.0 になり動かなくなりました。