【发布时间】:2012-11-02 16:14:44
【问题描述】:
我正在尝试将 JSF @ViewScoped 注释移植到 CDI。原因是更多的教育而不是基于需要。我之所以选择这个特定的范围,主要是因为缺少一个可能希望在 CDI 中实现的自定义范围的更好的具体示例。
也就是说,我的出发点是Porting the @ViewScoped JSF annotation to CDI。但是,这个实现并没有考虑到API中提到的Context(即销毁)一个看似非常重要的职责:
上下文对象负责通过调用Contextual的操作来创建和销毁上下文实例。特别是,上下文对象负责通过将实例传递给 Contextual.destroy(Object, CreationalContext) 来销毁它创建的任何上下文实例。 get() 不得随后返回已销毁的实例。上下文对象必须将创建实例时传递给 Contextual.create() 的 CreationalContext 实例传递给 Contextual.destroy()。
我决定通过我的Context 对象来添加此功能:
- 跟踪它为哪些
UIViewRoots 创建的Contextual对象; - 实现ViewMapListener接口并通过调用
UIViewRoot.subscribeToViewEvent(PreDestroyViewMapEvent.class, this)将自己注册为每个UIViewRoot的监听器; - 在调用
ViewMapListener.processEvent(SystemEvent event)时销毁任何已创建的Contextuals,并从该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());
}
}
我在META-INF/services 下添加了包含com.example.CustomContextsExtension 的javax.enterprise.inject.spi.Extension 文件,以便在CDI 中正确注册上述内容。
我现在可以制作类似的 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 页面中(即每个视图返回相同的实例,仅在创建视图时创建新的实例,相同的实例通过多个请求注入到同一个视图)。我怎么知道?想象一下上面的代码中到处都是调试代码,为了清楚起见,我特意删除了这些代码,因为这已经是一篇很大的帖子了。
问题是我的ViewContext.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) 期间。文档指出,每次在视图地图上调用 clear() 时都应该触发 PreDestroyViewMapEvent。这不禁让人怀疑——clear() 是否需要被调用?如果有,什么时候?
我能在文档中找到这样一个要求的唯一地方是FacesContext.setViewRoot():
如果当前 UIViewRoot 为非 null,并且在 参数root,传递当前UIViewRoot返回false,清除 必须在从 UIViewRoot#getViewMap 返回的 Map 上调用方法。
这在正常的 JSF 生命周期中是否曾经发生过,即没有以编程方式调用 UIViewRoot.setViewMap()?我似乎找不到任何迹象。
【问题讨论】:
-
你看过 MyFaces CODI 中的版本吗?这可能有助于为您指明正确的方向。
-
既然你提到了 - 我做到了。我下载了他们的资源并检查了他们。他们对视图上下文的实现与我上面的非常相似。关键的相似之处在于它们以
FacesContext.getCurrentInstance().getApplication().subscribeToEvent(PreDestroyViewMapEvent.class, this);的方式注册JSF 事件并实现SystemEventListener。我这样做的方式应该完全相同,因为 API 声明UIViewRoot.subscribeToViewEvent(PreDestroyViewMapEvent.class, this);的调用应该调用另一个方法。 @LightGuard -
此外,他们实现了
SystemEventListener,而我实现了ViewMapListener,这只是一个子接口,所以那里没有问题。所以我的实现似乎没有什么大问题。问题是我的 JSF 环境不会触发任何系统事件,无论我是通过编程方式还是通过faces-config.xml注册它们。知道为什么会发生这种情况吗?感谢您推荐 MyFaces CODI @LightGuard -
这很奇怪,因为我知道 CODI 可以在您的环境中使用。 Mojarra 2.1.7 中可能存在的错误。
-
@LightGuard 在 MyFaces 上运行它会产生相同的结果,所以我怀疑这是 JSF 实现中的错误。我对此进行了进一步调查-请参阅上面原始问题中的编辑。这让我相信即使是 CODI 的实施也存在缺陷。即我的示例和 CODI 实现都确保视图实例和 bean 类型的实例之间存在一对一的关系,但未能正确销毁实例。我想知道是否值得向 CODI 开发人员提出这个问题。
标签: jsf-2 java-ee-6 cdi myfaces codi