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 对象来添加此功能:

    跟踪它为哪些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.CustomContextsExtensionjavax.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.1Mojarra 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 方法

多次调用 JSF Backing Bean 构造函数

awakeFromNib 被调用,viewDidLoad 未被调用

JSF 1.1-不刷新页面调用backing bean的动作方法(通过ajax/javascript)

willRotateToInterfaceOrientation 未被调用