ViewMapListener JSF 未被调用
Posted
技术标签:
【中文标题】ViewMapListener JSF 未被调用【英文标题】:ViewMapListener JSF not being called 【发布时间】: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
对象来添加此功能:
-
跟踪它为哪些
UIViewRoot
s 创建的Contextual
对象;
实现ViewMapListener接口并通过调用UIViewRoot.subscribeToViewEvent(PreDestroyViewMapEvent.class, this)
将自己注册为每个UIViewRoot
的监听器;
在调用ViewMapListener.processEvent(SystemEvent event)
时销毁任何已创建的Contextual
s,并从该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 开发人员提出这个问题。
【参考方案1】:
这与 JSF2.2 规范中正在修复的 JSF 规范问题有关,请参阅 here。此外,我在 Apache DeltaSpike 上创建了一个问题,因此他们可能会尝试修复它,请参阅 here。如果它在 DeltaSpike 中得到修复,那么它最终可能也会被合并到 CODI 和/或 Seam 中。
【讨论】:
【参考方案2】:视图映射存储在 LRU 映射中,因为您永远不知道将回发哪个视图。不幸的是,在从该地图中删除之前没有调用 PreDestroyViewMapEvent。
一种解决方法是从 WeakReference 中引用您的对象。您可以使用 ReferenceQueue 或检查何时调用您的销毁代码。
【讨论】:
仅供参考:rdcrng 的贡献最终在 OmniFaces 1.6 版本中结束,我也进一步完善了它。 OmniFaces 使用的 LRU 映射支持在驱逐时触发侦听器,因此也涵盖了这种情况。另请参阅 showcase.omnifaces.org/cdi/ViewScoped 示例和文档/资源链接。 @Tires 正如 BalusC 所指出的那样,在 Omnifaces 社区的帮助下,这最终成为了 Omnifaces 的一个很好的特性——一个很好的实现,可以在许多应用服务器甚至集群环境中工作。至于您使用弱引用的想法 - 这也是我的第一种方法,但是 DeltaSpike 团队似乎不喜欢它,请参阅mail-archives.apache.org/mod_mbox/incubator-deltaspike-dev/…。以上是关于ViewMapListener JSF 未被调用的主要内容,如果未能解决你的问题,请参考以下文章
在 JSF 中,“saveState()”方法被调用了两次。为啥?
不调用 JSF 2.1 ViewScopedBean @PreDestroy 方法
awakeFromNib 被调用,viewDidLoad 未被调用