@ViewScoped
JSFアノテーションを CDIに移植しようとしています。その理由は、必要性に基づくというよりも、より教育的なものです。この特定のスコープを選択した主な理由は、CDI で実装したいカスタム スコープの具体的な例が不足しているためです。
そうは言っても、私の出発点はJSF アノテーションを CDIに移植すること@ViewScoped
でした。しかし、この実装は、 APIで言及されているContext (つまり、破棄)の一見非常に重要な責任を考慮していません。
コンテキスト オブジェクトは、Contextual のオペレーションを呼び出して、コンテキスト インスタンスを作成および破棄します。特に、コンテキスト オブジェクトは、インスタンスを Contextual.destroy(Object, CreationalContext) に渡すことによって、作成したコンテキスト インスタンスを破棄する責任があります。破棄されたインスタンスは、後で get() によって返されてはなりません。コンテキスト オブジェクトは、インスタンスを作成したときに Contextual.create() に渡したものと同じ CreationalContext のインスタンスを Contextual.destroy() に渡す必要があります。
Context
オブジェクトを作成して、この機能を追加することにしました。
- どのs
Contextual
に対してどのオブジェクトを作成するかを追跡します。UIViewRoot
- ViewMapListenerインターフェイスを実装し、
UIViewRoot
を呼び出して、それぞれのリスナーとして自身を登録しますUIViewRoot.subscribeToViewEvent(PreDestroyViewMapEvent.class, this)
。 Contextual
が呼び出されたときに作成された をすべて破棄し、ViewMapListener.processEvent(SystemEvent event)
それ自体を登録解除しUIViewRoot
ます。
これが私のContext
実装です:
package com.example;
import java.lang.annotation.Annotation;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import javax.enterprise.context.spi.Context;
import javax.enterprise.context.spi.Contextual;
import javax.enterprise.context.spi.CreationalContext;
import javax.enterprise.inject.spi.Bean;
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.PreDestroyViewMapEvent;
import javax.faces.event.SystemEvent;
import javax.faces.event.ViewMapListener;
public class ViewContext implements Context, ViewMapListener {
private Map<UIViewRoot, Set<Disposable>> state;
public ViewContext() {
this.state = new HashMap<UIViewRoot, Set<Disposable>>();
}
// mimics a multimap put()
private void put(UIViewRoot key, Disposable value) {
if (this.state.containsKey(key)) {
this.state.get(key).add(value);
} else {
HashSet<Disposable> valueSet = new HashSet<Disposable>(1);
valueSet.add(value);
this.state.put(key, valueSet);
}
}
@Override
public Class<? extends Annotation> getScope() {
return ViewScoped.class;
}
@Override
public <T> T get(final Contextual<T> contextual,
final CreationalContext<T> creationalContext) {
if (contextual instanceof Bean) {
Bean bean = (Bean) contextual;
String name = bean.getName();
FacesContext ctx = FacesContext.getCurrentInstance();
UIViewRoot viewRoot = ctx.getViewRoot();
Map<String, Object> viewMap = viewRoot.getViewMap();
if (viewMap.containsKey(name)) {
return (T) viewMap.get(name);
} else {
final T instance = contextual.create(creationalContext);
viewMap.put(name, instance);
// register for events
viewRoot.subscribeToViewEvent(
PreDestroyViewMapEvent.class, this);
// allows us to properly couple the right contaxtual, instance, and creational context
this.put(viewRoot, new Disposable() {
@Override
public void dispose() {
contextual.destroy(instance, creationalContext);
}
});
return instance;
}
} else {
return null;
}
}
@Override
public <T> T get(Contextual<T> contextual) {
if (contextual instanceof Bean) {
Bean bean = (Bean) contextual;
String name = bean.getName();
FacesContext ctx = FacesContext.getCurrentInstance();
UIViewRoot viewRoot = ctx.getViewRoot();
Map<String, Object> viewMap = viewRoot.getViewMap();
if (viewMap.containsKey(name)) {
return (T) viewMap.get(name);
} else {
return null;
}
} else {
return null;
}
}
// this scope is only active when a FacesContext with a UIViewRoot exists
@Override
public boolean isActive() {
FacesContext ctx = FacesContext.getCurrentInstance();
if (ctx == null) {
return false;
} else {
UIViewRoot viewRoot = ctx.getViewRoot();
return viewRoot != null;
}
}
// dispose all of the beans associated with the UIViewRoot that fired this event
@Override
public void processEvent(SystemEvent event)
throws AbortProcessingException {
if (event instanceof PreDestroyViewMapEvent) {
UIViewRoot viewRoot = (UIViewRoot) event.getSource();
if (this.state.containsKey(viewRoot)) {
Set<Disposable> valueSet = this.state.remove(viewRoot);
for (Disposable disposable : valueSet) {
disposable.dispose();
}
viewRoot.unsubscribeFromViewEvent(
PreDestroyViewMapEvent.class, this);
}
}
}
@Override
public boolean isListenerForSource(Object source) {
return source instanceof UIViewRoot;
}
}
Disposable
インターフェースは次のとおりです。
package com.example;
public interface Disposable {
public void dispose();
}
スコープの注釈は次のとおりです。
package com.example;
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.enterprise.context.NormalScope;
@Inherited
@NormalScope
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD,
ElementType.FIELD, ElementType.PARAMETER})
public @interface ViewScoped {
}
CDI 拡張宣言は次のとおりです。
package com.example;
import javax.enterprise.event.Observes;
import javax.enterprise.inject.spi.AfterBeanDiscovery;
import javax.enterprise.inject.spi.Extension;
public class CustomContextsExtension implements Extension {
public void afterBeanDiscovery(@Observes AfterBeanDiscovery event) {
event.addContext(new ViewContext());
}
}
上記をCDIに正しく登録するために、javax.enterprise.inject.spi.Extension
ファイルをMETA-INF/services
含むの下に追加しました。com.example.CustomContextsExtension
次のような Bean を作成できるようになりました (カスタム@ViewScoped
実装の使用に注意してください)。
package com.example;
import com.concensus.athena.framework.cdi.extension.ViewScoped;
import java.io.Serializable;
import javax.inject.Named;
@Named
@ViewScoped
public class User implements Serializable {
...
}
Bean は適切に作成され、JSF ページに適切に注入されます (つまり、ビューごとに同じインスタンスが返され、ビューが作成されたときにのみ新しいインスタンスが作成され、同じインスタンスが同じビューへの複数の要求に対して注入されます)。どうすればわかりますか?上記のコードにデバッグ用のコードが散らばっていると想像してください。
問題は、 myViewContext.isListenerForSource(Object source)
とViewContext.processEvent(SystemEvent event)
が呼び出されないことです。ビュー マップはセッション マップに格納されているため、少なくともセッションの有効期限が切れたときにこれらのイベントが呼び出されることを期待していました (正しいですか?)。セッションのタイムアウトを 1 分に設定して待機し、タイムアウトが発生したことを確認しましたが、リスナーはまだ呼び出されていません。
また、次のものを追加しようとしましたfaces-config.xml
(ほとんどの場合、アイデアが不足しているため)。
<system-event-listener>
<system-event-listener-class>com.example.ViewContext</system-event-listener-class>
<system-event-class>javax.faces.event.PreDestroyViewMapEvent</system-event-class>
<source-class>javax.faces.component.UIViewRoot</source-class>
</system-event-listener>
最後に、私の環境はJBoss AS 7.1.1
ですMojarra 2.1.7
。
手がかりは大歓迎です。
編集: さらなる調査。
PreDestroyViewMapEvent
が期待どおりに起動されている間は、まったく起動されていないようですPostConstructViewMapEvent
- 新しいビュー マップが作成されるたびに、特にUIViewRoot.getViewMap(true)
. ドキュメントには、ビューマップで呼び出されるPreDestroyViewMapEvent
たびに起動する必要があると記載されています。clear()
それは疑問に思います-そもそもclear()
呼び出される必要がありますか? もしそうなら、いつ?
このような要件を見つけることができたドキュメント内の唯一の場所は次のFacesContext.setViewRoot()
とおりです。
現在の UIViewRoot が null でなく、引数ルートで equals() を呼び出し、現在の UIViewRoot を渡して false を返す場合、UIViewRoot#getViewMap から返されたマップで clear メソッドを呼び出す必要があります。
これは通常の JSF ライフサイクルで、つまりプログラムで呼び出すことなく発生することはありますUIViewRoot.setViewMap()
か? 何の兆候も見当たらない。