CDIをJavaSEで使えるようにするのはそんな難しいことじゃないよね。
問題なのは、JavaEEのスコープをJavaSEで使うこと。
RequestScopedとか、ConversationScopedとか、SessionScopedとか。
他のカスタムされたスコープは、まぁ、必要な人が対応すれば良いよねってことで。
基本は、同じだし。
まずは、コンテナとしての機能を追加しないとダメだから、JUnitのRunnerを拡張してWeldの機能を有効にするところから。
ホントは、別の方法とかあるのかもしれないけど、JUnitに詳しくなくて……動けば良いよね!
こんな感じ。
public class JUnit4RunnerWithWeld extends BlockJUnit4ClassRunner {
/** Weld */
private static final Weld Weld;
/** Weld Container. */
private static final WeldContainer Container;
static {
Weld = new Weld();
Container = Weld.initialize();
}
/** 対象となるテストクラス。 */
private final Class<?> TargetTestClass;
public JUnit4RunnerWithWeld(final Class<Object> TargetTestClass) throws InitializationError {
super(TargetTestClass);
this.TargetTestClass = TargetTestClass;
}
@Override
protected Object createTest() throws Exception {
Object test = JUnit4RunnerWithWeld.Container.instance().select(this.TargetTestClass).get();
return test;
}
}
ポイントは2つ。
- Weldを作って初期化する。
- BlockJUnit4ClassRunnerを継承してcreateTestで対象のクラスをコンテナとして作る。
うん、簡単だよね!Weld大好き!……かも。いや、別にそんな好きなわけじゃないけど。
ちなみに、Runner自体は、テストクラスごとに出来るから、Weldはstaticに作ってるよ。
これで、ユニットテストクラスに@RunWithでCDIを有効にできるようになったのだ。
@RunWith(JUnit4RunnerWithWeld.class)
public class SomethingTest {
あ、実際に有効にするには、beans.xml忘れないでね。
beans.xmlをどういう風にしたら良いかは別の記事でまとめるよ。
次に、JavaEEのスコープにコンテキストとストレージを設定しないとね。
まぁ、これは、GlassFish3.xのころにViewScopedとか作ってたし楽勝ってことで。
こんなユーティリティを用意する。長いよ。
public class WeldUtilities {
public static String ExtensionProductName = "Empressia Test Library";
public static String SupportCDIProductName = "Weld";
/** JavaEE向けのスコープをJavaSEでサポートする。 */
public static void supportEEScopes(BeanManager bm)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
Map<String, Object> requestMap = new HashMap<>();
WeldUtilities.activateContextWithStorage(bm, RequestScoped.class, requestMap);
Map<String, Object> sessionMap = new HashMap<>();
WeldUtilities.activateContextWithStorage(bm, SessionScoped.class, sessionMap);
WeldUtilities.activateContextWithStorage(bm, ConversationScoped.class, new MutableBoundRequest(requestMap, sessionMap));
}
/** JavaEE向けのスコープをJavaSEでサポートをリセットする。 */
public static void resetEEScopeContexts(BeanManager bm)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
AbstractBoundContext<?>[] contexts = {
WeldUtilities.findFirstContext(bm, RequestScoped.class),
WeldUtilities.findFirstContext(bm, SessionScoped.class),
WeldUtilities.findFirstContext(bm, ConversationScoped.class)
};
for(AbstractBoundContext<?> context : contexts) {
context.deactivate();
context.cleanup();
}
WeldUtilities.supportEEScopes(bm);
}
public static <S> void activateContextWithStorage(BeanManager bm, Class<? extends Annotation> scopeType, S storage)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
AbstractBoundContext<S> boundContext = WeldUtilities.findFirstContext(bm, scopeType);
boundContext.associate(storage);
boundContext.activate();
}
/** BeanManagerから指定スコープの最初のContextを抽出する。アクティブかどうかは考慮されない。 */
public static <S> AbstractBoundContext<S> findFirstContext(BeanManager bm, Class<? extends Annotation> scopeType)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
Map<Class<? extends Annotation>, List<Context>> contextsMap = WeldUtilities.extractContextsMap(bm);
List<Context> contexts = contextsMap.get(scopeType);
// ContextはScopeの種類に対して必ず1個用意してある前提で。
if(contexts.isEmpty()) {
String message = "なにかメッセージ";
throw new IllegalStateException(message);
}
Context context = contexts.get(0);
if(context instanceof AbstractBoundContext) {
} else {
// java.lang.ClassCastException: org.jboss.weld.context.PassivatingContextWrapper$AlterableContextWrapper cannot be cast to org.jboss.weld.context.AbstractBoundContext
context = PassivatingContextWrapper.unwrap(context);
}
@SuppressWarnings("unchecked")
AbstractBoundContext<S> boundContext = (AbstractBoundContext<S>)context;
return boundContext;
}
/** BeanManagerからScopeTypeごとの?Contextを抽出する。 */
public static Map<Class<? extends Annotation>, List<Context>> extractContextsMap(BeanManager bm)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
BeanManagerImpl beanManagerImpl;
if(bm instanceof BeanManagerProxy) {
BeanManagerProxy beanManagerProxy = (BeanManagerProxy)bm;
beanManagerImpl = beanManagerProxy.delegate();
} else if(bm instanceof BeanManagerImpl) {
beanManagerImpl = (BeanManagerImpl)bm;
} else {
String message = "なにかメッセージ";
throw new IllegalStateException(message);
}
// getContextだとactiveなものしか取れないから、強引に設定したい今回の用途では使えない。
Method m = BeanManagerImpl.class.getDeclaredMethod("getContexts");
m.setAccessible(true);
@SuppressWarnings("unchecked")
Map<Class<? extends Annotation>, List<Context>> contextsMap = (Map<Class<? extends Annotation>, List<Context>>)m.invoke(beanManagerImpl);
return contextsMap;
}
}
この辺は、解説入れるとめんどくさいから、WeldのソースとかGlassFishのソースとか読むと良いかもってことで。
で、これを呼び出すExtensionを用意する。
package jp.empressia.test;
// (略)
public class WeldTestExtension implements Extension {
public void afterDeployment(@Observes AfterDeploymentValidation e, BeanManager bm)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
logger(this).info("Empressia Test Library Weld Test Extension startup.");
WeldUtilities.supportEEScopes(bm);
}
}
このExtensionは、META-INF/servicesに『javax.enterprise.inject.spi.Extension』ってファイルを作って、その中に、
jp.empressia.test.WeldTestExtension
とか書いておくよ。こうしておくことで、Weldの初期化がされたときに、@Observesが呼ばれてJavaEEのスコープがすぐに有効になるのだ。
あと、テストごとに、スコープはリセットした方が良いだろうから、Runnerのメソッドをオーバーライドしておこうね。
public class JUnit4RunnerWithWeld extends BlockJUnit4ClassRunner {
// (略)
/** テストを1個ずつ実行します。順序は保証されないから、コンテキストは毎回リセットする。 */
@Override
protected void runChild(FrameworkMethod method, RunNotifier notifier) {
BeanManager bm = JUnit4RunnerWithWeld.Container.getBeanManager();
try {
WeldUtilities.resetEEScopeContexts(bm);
} catch(NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
String message = "なにかメッセージ";
throw new IllegalStateException(message, ex);
}
super.runChild(method, notifier);
}
}
これで完成(゚ー゚)(。_。)(゚-゚)(。_。)ウンウン
この辺のクラスをパッケージにしたのを置いておいたよ。
Mavenのリポジトリを追加して依存関係を書けば使えるよ。こんな感じ?
<repositories>
……
<repository>
<id>Empressia</id>
<url>http://www.empressia.jp/maven/</url>
</repository>
</repositories>
<dependencies>
……
<dependency>
<groupId>jp.empressia</groupId>
<artifactId>jp.empressia.test</artifactId>
<version>1.0.1-1</version>
<scope>test</scope>
</dependency>
</dependencies>
古い実装とかライブラリが混じっていると、JNDIにBeanManagerを登録しておかないとちゃんと動かないかもしれないけど。
このパッケージは、その辺も簡単にだけど対応しておいたよ。
[url=http://www.gj80vkhg361213vvtin1f94axd774i92s.org/]ugepfzxqwq[/url]
gepfzxqwq http://www.gj80vkhg361213vvtin1f94axd774i92s.org/
<a href="http://www.gj80vkhg361213vvtin1f94axd774i92s.org/">agepfzxqwq</a>