Как обнаружить и удалить (во время сеанса) неиспользованные @ViewScoped beans, которые не могут быть собраны в мусор

EDIT: проблема, поднятая этим вопросом, очень хорошо объяснена и подтверждена в этой статье с помощью codebulb.ch, включая некоторое сравнение между JSF @ViewScoped, CDI @ViewScoped и Omnifaces @ViewScoped, а также четкое утверждение что JSF @ViewScoped является "негерметичным по дизайну": 24 мая 2015 г. Java EE 7 Bean области по сравнению с частью 2 из 2


EDIT: 2017-12-05. Тест, используемый для этого вопроса, по-прежнему чрезвычайно полезен, однако выводы, касающиеся коллекции мусора в исходном посте (и изображениях), были основаны на JVisualVM, и с тех пор я обнаружил, что они недействительны, Вместо этого используйте NetBeans Profiler!. Теперь я получаю полностью согласованные результаты для OmniFaces ViewScoped с тестовым приложением для принудительного использования GC из NetBeans Profiler вместо JVisualVM, подключенного к GlassFish/Payara, где я все еще получаю ссылки (даже после того, как @PreDestroy называется) по полю sessionListeners типа com.sun.web.server.WebContainerListener внутри ContainerBase$ContainerBackgroundProcessor, и они не будут GC.


Известно, что в JSF2.2 для страницы, использующей @ViewScoped bean, переход от нее (или перезагрузка) с помощью любого из следующих методов приведет к экземплярам @ViewScoped Bean "болтаться" в сеансе, чтобы не собирать мусор, что привело к бесконечно растущей памяти кучи (до тех пор, пока она вызвана GET):

  • Использование h: link для создания новой страницы.

  • Использование h: outputLink (или тега HTML A) для получения новой страницы.

  • Перезагрузка страницы в браузере с помощью команды или кнопки RELOAD.

  • Перезагрузка страницы с помощью клавиатуры ENTER на URL-адрес браузера (также GET).

В отличие от этого, проходя через навигационную систему JSF, используя команду h: commandButton, выдается выпуск @ViewScoped Bean, так что он может быть собран в мусор.

Это объясняется (по BalusC) в JSF 2.1. Метод ViewScopedBean @PreDestroy не называется и демонстрируется для JSF2.2 и Mojarra 2.2.9 моим небольшим примером NetBeans проект qaru.site/info/9739/..., который иллюстрирует различные варианты навигации и доступен для загрузки здесь. (EDIT: 2015-05-28: полный код теперь также доступен здесь ниже.)

[EDIT: 2016-11-13 В настоящее время также улучшено тестовое веб-приложение с полными инструкциями и сравнением с OmniFaces @ViewScoped и таблицей результатов в GitHub здесь: https://github.com/webelcomau/JSFviewScopedNav]

Здесь я повторяю изображение index.html, в котором суммируются случаи навигации и результаты для памяти кучи:

enter image description here

В: Как я могу обнаружить "зависание/зависание" @ViewScoped beans, вызванное навигацией GET, и удалить их или иным образом сделать их сборными для мусора?

Обратите внимание, что я не спрашиваю, как очистить их, когда закончится сеанс, я уже видел различные решения для этого, я ищу способы их очистки во время сеанса, так что память кучи не увеличивается чрезмерно во время сеанса из-за непреднамеренных навигаций GET.


Ответы

Ответ 1

В принципе, вы хотите, чтобы состояние просмотра JSF и все видимые области видимости beans были уничтожены во время выгрузки окна. Решение было реализовано в OmniFaces @ViewScoped аннотация, которая описана в документации, как показано ниже:

Могут быть случаи, когда желательно немедленно уничтожить область с видимым видом bean, а также при вызове браузера unload. То есть когда пользователь переходит от GET или закрывает вкладку/окно браузера. Ни одна из аннотаций аннотации представления JSF 2.2 не поддерживает это. Начиная с OmniFaces 2.2, аннотация этого вида представления CDI гарантирует, что аннотированный метод @PreDestroy также вызывается при разгрузке браузера. Этот трюк выполняется синхронным запросом XHR через автоматически включаемый помощник script omnifaces:unload.js. Однако существует небольшая оговорка: на медленной сети и/или на плохом серверном оборудовании может быть заметное отставание между действием enduser разгрузки страницы и желаемым результатом. Если это нежелательно, тогда лучше придерживаться аннотаций к собственному виду JSF 2.2 и принять отложенную версию.

Начиная с OmniFaces 2.3, выгрузка была дополнительно улучшена, чтобы также физически удалить связанное состояние представления JSF из внутренней карты LRU реализации JSF в случае сохранения состояния на стороне сервера, тем самым дополнительно уменьшая риск при ViewExpiredException на других представлениях, которые были созданы/открыты ранее. В качестве побочного эффекта этого изменения, аннотированный метод @PreDestroy любого стандартного представления JSF с привязкой beans, на который ссылается в том же представлении, что и просмотр содержимого CDI в формате OmniFaces, ограниченный bean, также будет активирован при разгрузке браузера.

Здесь вы можете найти соответствующий исходный код:

Выгрузка script будет запускаться во время окна beforeunload событие, , если оно не вызвано каким-либо JSF (ajax). Что касается коммандлинка и/или ajax, это специфично для реализации. В настоящее время распознаются Mojarra, MyFaces и PrimeFaces.

Разгрузка script будет 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);
}

Обработчик вида выгрузки будет явно уничтожить все @ViewScoped beans, включая стандартные JSF (обратите внимание, что разгрузка script инициализируется только тогда, когда представление ссылается хотя бы на один OmniFaces @ViewScoped bean).

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

Однако это не разрушает физическое состояние представления JSF в сеансе HTTP, и, таким образом, use case не будет выполняться:

  • Задайте количество физических представлений до 3 (в Mojarra используйте параметр контекста com.sun.faces.numberOfLogicalViews и в MyFaces используйте параметр org.apache.myfaces.NUMBER_OF_VIEWS_IN_SESSION context).
  • Создайте страницу, которая ссылается на стандартный JSF @ViewScoped bean.
  • Откройте эту страницу на вкладке и сохраните ее все время.
  • Откройте ту же страницу на другой вкладке и сразу закройте эту вкладку.
  • Откройте ту же страницу на другой вкладке и сразу закройте эту вкладку.
  • Откройте ту же страницу на другой вкладке и сразу закройте эту вкладку.
  • Отправьте форму на первой вкладке.

Это приведет к ошибке с ViewExpiredException, потому что состояния представления JSF ранее закрытых вкладок физически не уничтожаются во время PreDestroyViewMapEvent. Они все еще остаются на сессии. OmniFaces @ViewScoped фактически уничтожит их. Уничтожение состояния представления JSF, однако, специфично для реализации. Это объясняет, по крайней мере, довольно хакерский код в классе Hacks, который должен достичь этого.

Тест интеграции для этого конкретного случая можно найти в ViewScopedIT#destroyViewState() на ViewScopedIT.xhtml, который в настоящее время работает с 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 {}

Я по крайней мере сделал попытку предложить общедоступный API-метод для физического разрушения состояния представления JSF. Возможно, это произойдет в JSF 2.3, а затем я смогу устранить шаблон в классе OmniFaces Hacks. Как только предмет полируется в OmniFaces, он, возможно, в конечном итоге попадет в JSF, но не раньше 2.4.

Ответ 2

Хорошо, поэтому я собрал что-то вместе.

Принцип

Теперь неактуальные viewcoped beans сидят там, тратя время на все время и пространство, потому что в случае с навигацией GET с использованием любого из выделенных элементов управления сервер не задействован. Если сервер не задействован, он не может знать, что просмотренные beans теперь избыточны (то есть до тех пор, пока сессия не умер). Итак, что нужно здесь, это способ рассказать серверной стороне о том, что представление, с которого вы переходите, должно прервать его область видимости beans

Ограничения

Серверная сторона должна быть уведомлена, как только произойдет навигация.

  • beforeunload или unload в <h:body/> было бы идеальным, но для следующих задач

    • Браузеры неравномерно относятся к ним обоим

    • Решение с использованием любого из них, скорее всего, потребует решения AJAX, выходящего за рамки JSF. JSF ajax-ready script должен выполняться в контексте формы. У вас не может быть <h:body/> внутри формы. Я предпочитаю держать все это внутри JSF

  • Вы не можете отправить запрос ajax в onclick элемента управления, а также перемещаться в том же элементе управления. В любом случае, без грязного всплывающего окна. Таким образом, перемещение onclick в h:button или h:link выходит из него

Грязный компромисс

Запустите запрос ajax onclick и выполните PhaseListener фактическую навигацию и очистку viewcope

Рецепт

  • 1 PhaseListener (a ViewHandler также будет работать здесь, я пойду с первым, потому что его проще настроить)

  • 1 оболочка вокруг API JSF js

  • Среднее опоздание

Посмотрим:

  • 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 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;
        }
    
    }
    
  • Оболочка JS

     function cleanViewScope(){
      jsf.ajax.request(this, null, {execute: 'someButton', target: this.href});
       return false;
      }
    
  • Совмещение

      <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"/>
    

Сделать

  • Расширьте h:link, возможно добавьте атрибут для настройки поведения очистки

  • Способ передачи целевого URL-адреса является подозрительным; может открыть отверстие