如何检测和删除(在会话期间)无法被垃圾收集的未使用的 @ViewScoped beans

How detect and remove (during a session) unused @ViewScoped beans that can't be garbage collected

编辑:codebulb.ch 这篇文章很好地解释和确认了这个问题提出的问题,包括 JSF @ViewScoped、CDI @ViewSCoped 和 Omnifaces 之间的一些比较@ViewScoped,并明确声明 JSF @ViewScoped 是 'leaky by design':May 24, 2015 Java EE 7 Bean scopes compared part 2 of 2


编辑:2017-12-05 用于此问题的测试用例仍然非常有用,但是原始 post(和图像)中有关垃圾收集的结论是基于JVisualVM,后来我发现它们无效。 改用 NetBeans Profiler! 我现在在 OmniFaces ViewScoped 上获得了完全一致的结果,测试应用程序从 NetBeans Profiler 中强制 GC 而不是附加到 GlassFish/Payara 的 JVisualVM,其中我得到的引用仍然被 ContainerBase$ContainerBackgroundProcessorcom.sun.web.server.WebContainerListener 类型的字段 sessionListeners 持有(即使在调用 @PreDestroy 之后),它们不会 GC.


众所周知,在 JSF2.2 中,对于使用 @ViewScoped bean 的页面,使用以下任何技术离开它(或重新加载它)将导致 @ViewScoped bean 的实例 "dangling" 在会话中,这样它就不会被垃圾收集,导致堆内存不断增长(只要由 GET 引发):

相比之下,使用 h:commandButton 传递 JSF 导航系统会导致释放 @ViewScoped bean,以便它可以被垃圾收集。

JSF 2.1 ViewScopedBean @PreDestroy method is not called and demonstrated for JSF2.2 and Mojarra 2.2.9 by my small NetBeans example project at , which project illustrates the various navigation cases and is available for download here(BalusC)解释了这一点。 (编辑:2015-05-28:完整代码现在也可以在下面找到。)

[编辑:2016-11-13 现在还有一个改进的测试网络应用程序,其中包含完整的说明和与 OmniFaces @ViewScoped 的比较以及 [=85] 上的结果 table =] 这里:https://github.com/webelcomau/JSFviewScopedNav]

我在这里重复 index.html 的图像,它总结了堆内存的导航案例和结果:

问:如何检测由 GET 导航引起的这种 "hanging/dangling" @ViewScoped beans 并删除它们,或者以其他方式呈现垃圾收集table?

请注意,我不是在问如何在会话结束时清理它们,我已经看到了各种解决方案,我正在寻找在会话期间清理它们的方法,所以由于无意的 GET 导航,堆内存在会话期间不会过度增长。


好的,所以我拼凑了一些东西。

原理

现在不相关的视图范围 bean 坐在那里,浪费每个人的时间,space 因为在 GET 导航情况下,使用您突出显示的任何控件,不涉及服务器。如果服务器不参与,它就无法知道 viewscoped beans 现在是多余的(直到会话终止)。所以这里需要的是一种方法来告诉服务器端您正在导航的视图需要终止其视图范围的 beans

约束

导航发生后应立即通知服务器端

  1. beforeunloadunload<h:body/> 中本来是理想的,但对于以下问题

    • Browsers don't uniformly respect either of them

    • 使用其中任何一个的解决方案很可能需要一个在 JSF 框架之外的 AJAX 解决方案。 JSF 的 ajax-ready script 必须在表单的上下文中执行。您不能在表单中包含 <h:body/>。我更喜欢将其全部保存在 JSF

  2. 您不能在控件的 onclick 中发送 ajax 请求,并在同一控件中导航。无论如何,并非没有脏弹出窗口。所以在 h:buttonh:link 中导航 onclick 是不可能的

肮脏的妥协

触发 ajax 请求 onclick,并让 PhaseListener 执行实际导航 视图清理

食谱

  1. 1 PhaseListenerViewHandler 也适用于此;我选择前者,因为它更容易设置)

  2. 1 个 JSF js 包装器 API

  3. 中度帮助羞耻

让我们看看:

  1. PhaseListener

    public ViewScopedCleaner implements PhaseListener{
    
        public void afterPhase(PhaseEvent evt){
             FacesContext ctxt = event.getFacesContext();
             NavigationHandler navHandler = ctxt.getApplication().getNavigationHanler();
             boolean isAjax =  ctx.getPartialViewContext().isAjaxRequest(); //determine that it's an ajax request
             Object target = ctxt.getExternalContext().getRequestParameterMap().get("target"); //get the destination URL
    
                    if(target !=null && !target.toString().equals("")&&isAjax ){
                         ctxt.getViewRoot().getViewMap().clear(); //clear the map
                         navHandler.handleNavigation(ctxt,null,target);//navigate
                     }
    
        }
    
        public PhaseId getPhaseId(){
            return PhaseId.APPLY_REQUEST_VALUES;
        }
    
    }
    
  2. JS 包装器

     function cleanViewScope(){
      jsf.ajax.request(this, null, {execute: 'someButton', target: this.href});
       return false;
      }
    
  3. 放在一起

      <script>
         function cleanViewScope(){
             jsf.ajax.request(this, null, {execute: 'someButton', target: this.href}); return false;
          }
      </script>  
    
     <f:phaseListener type="com.you.test.ViewScopedCleaner" />
     <h:link onclick="cleanViewScope();" value="h:link: GET: done" outcome="done?faces-redirect=true"/>
    

待办事项

  1. 扩展h:link,可能添加一个属性来配置清除行为

  2. 传递目标 url 的方式令人怀疑;可能会打开一个洞

基本上,您希望在 window 卸载期间销毁 JSF 视图状态和所有视图范围的 bean。该解决方案已在 OmniFaces @ViewScoped annotation 中实施,并在其文档中充实如下:

There may be cases when it's desirable to immediately destroy a view scoped bean as well when the browser unload event is invoked. I.e. when the user navigates away by GET, or closes the browser tab/window. None of the both JSF 2.2 view scope annotations support this. Since OmniFaces 2.2, this CDI view scope annotation will guarantee that the @PreDestroy annotated method is also invoked on browser unload. This trick is done by a synchronous XHR request via an automatically included helper script omnifaces:unload.js. There's however a small caveat: on slow network and/or poor server hardware, there may be a noticeable lag between the enduser action of unloading the page and the desired result. If this is undesireable, then better stick to JSF 2.2's own view scope annotations and accept the postponed destroy.

Since OmniFaces 2.3, the unload has been further improved to also physically remove the associated JSF view state from JSF implementation's internal LRU map in case of server side state saving, hereby further decreasing the risk at ViewExpiredException on the other views which were created/opened earlier. As side effect of this change, the @PreDestroy annotated method of any standard JSF view scoped beans referenced in the same view as the OmniFaces CDI view scoped bean will also guaranteed be invoked on browser unload.

您可以在这里找到相关的源代码:

卸载脚本将 运行 during window's beforeunload event, unless it's caused by any JSF based (ajax) form submit. As to commandlink and/or ajax submits, this is implementation specific. Currently Mojarra、MyFaces 和 PrimeFaces 被识别。

卸载脚本将 trigger navigator.sendBeacon 在现代浏览器上并回退到同步 XHR(异步会失败,因为页面可能比请求实际到达服务器更快地卸载)。

var url = form.action;
var query = "omnifaces.event=unload&id=" + id + "&" + VIEW_STATE_PARAM + "=" + encodeURIComponent(form[VIEW_STATE_PARAM].value);
var contentType = "application/x-www-form-urlencoded";

if (navigator.sendBeacon) {
    // Synchronous XHR is deprecated during unload event, modern browsers offer Beacon API for this which will basically fire-and-forget the request.
    navigator.sendBeacon(url, new Blob([query], {type: contentType}));
}
else {
    var xhr = new XMLHttpRequest();
    xhr.open("POST", url, false);
    xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
    xhr.setRequestHeader("Content-Type", contentType);
    xhr.send(query);
}

卸载视图处理程序将explicitly销毁所有@ViewScoped beans,包括标准的JSF beans(请注意,卸载脚本仅在视图引用至少一个OmniFaces @ViewScoped豆)。

context.getApplication().publishEvent(context, PreDestroyViewMapEvent.class, UIViewRoot.class, createdView);

但这不会破坏 HTTP session 中的物理 JSF 视图状态,因此下面的 use case 将失败:

  1. 将物理视图数设置为 3(在 Mojarra 中,使用 com.sun.faces.numberOfLogicalViews 上下文参数,在 MyFaces 中使用 org.apache.myfaces.NUMBER_OF_VIEWS_IN_SESSION 上下文参数)。
  2. 创建一个引用标准 JSF @ViewScoped bean 的页面。
  3. 在选项卡中打开此页面并始终保持打开状态。
  4. 在另一个选项卡中打开同一页面,然后立即关闭此选项卡。
  5. 在另一个选项卡中打开同一页面,然后立即关闭此选项卡。
  6. 在另一个选项卡中打开同一页面,然后立即关闭此选项卡。
  7. 在第一个选项卡中提交表单。

这将失败并返回 ViewExpiredException,因为先前关闭的选项卡的 JSF 视图状态在 PreDestroyViewMapEvent 期间并未被物理破坏。他们仍然留在 session。 OmniFaces @ViewScoped 实际上会摧毁它们。然而,销毁 JSF 视图状态是特定于实现的。这至少解释了 Hacks class 中应该实现该目标的相当骇人听闻的代码。

可以在 ViewScopedIT#destroyViewState() on ViewScopedIT.xhtml which is currently 运行 中找到针对 WildFly 10.0.0、TomEE 7.0.1 和 Payara 4.1.1.163 的集成测试。


简而言之:只需将 javax.faces.view.ViewScoped 替换为 org.omnifaces.cdi.ViewScoped。其余透明。

import javax.inject.Named;
import org.omnifaces.cdi.ViewScoped;

@Named
@ViewScoped
public class Bean implements Serializable {}

我至少努力 propose public API 方法来物理破坏 JSF 视图状态。也许它会出现在 JSF 2.3 中,然后我应该能够消除 OmniFaces Hacks class 中的样板文件。一旦这个东西在 OmniFaces 中得到完善,它可能最终会出现在 JSF 中,但不会在 2.4 之前。