Как обнаружить и удалить (во время сеанса) неиспользуемые бины @ViewScoped, которые не могут быть собраны мусором
РЕДАКТИРОВАТЬ: Проблема, поднятая этим вопросом, очень хорошо объяснена и подтверждена в этой статье codebulb.ch, включая некоторое сравнение между JSF @ViewScoped
CDI @ViewSCoped
и Omnifaces @ViewScoped
и четкое утверждение, что JSF @ViewScoped
"утечка по замыслу": 24 мая 2015 г. Области применения Java EE 7 Bean сравнили часть 2 из 2
РЕДАКТИРОВАТЬ: 2017-12-05 Тестовый пример, используемый для этого вопроса, по-прежнему чрезвычайно полезен, однако выводы, касающиеся сбора мусора в исходном посте (и изображениях), были основаны на JVisualVM, и с тех пор я обнаружил, что они недействительны. Вместо этого используйте средство профилирования NetBeans! Теперь я получаю полностью согласующиеся результаты для OmniFaces ViewScoped с тестовым приложением по принудительной установке GC из средства профилирования NetBeans вместо JVisualVM, подключенного к GlassFish/Payara, где я получаю ссылки, которые все еще хранятся (даже после вызова @PreDestroy) по полю sessionListeners
типа com.sun.web.server.WebContainerListener
в ContainerBase$ContainerBackgroundProcessor
и они не будут GC.
Известно, что в JSF2.2 для страницы, использующей bean-компонент @ViewScoped, переход от него (или перезагрузка) с использованием любого из следующих методов приведет к тому, что bean-компонент @ViewScoped будет "зависать" в сеансе, поэтому что это не будет сборщик мусора, что приведет к бесконечно растущей куче памяти (если это спровоцировано GET):
Используя ссылку h: для получения новой страницы.
Использование h:outputLink (или тега HTML A) для получения новой страницы.
Перезагрузите страницу в браузере, используя команду или кнопку RELOAD.
Перезагрузка страницы с помощью клавиатуры ENTER на URL браузера (также GET).
Напротив, прохождение через систему навигации JSF с использованием, скажем, h:commandButton приводит к освобождению компонента @ViewScoped, так что он может быть собран мусором.
Это объясняется (BalusC) в JSF 2.1. Метод ViewScopedBean @PreDestroy не вызывается и не демонстрируется для JSF2.2 и Mojarra 2.2.9 с помощью моего небольшого примера проекта NetBeans по адресу /questions/7593281/jsf-21-viewscopedbean-predestroy-metod-ne-vyizyivaetsya/7593283#7593283, который Проект иллюстрирует различные варианты навигации и доступен для скачивания здесь. (РЕДАКТИРОВАТЬ: 2015-05-28: полный код теперь также доступен здесь ниже.)
[EDIT: 2016-11-13 В настоящее время существует также улучшенное тестовое веб-приложение с подробными инструкциями и сравнением с OmniFaces. @ViewScoped
и таблица результатов на GitHub здесь: https://github.com/webelcomau/JSFviewScopedNav]
Я повторяю здесь изображение index.html, которое суммирует случаи навигации и результаты для кучи памяти:
Q: Как я могу обнаружить такие "зависания / зависания" бинов @ViewScoped, вызванные GET-навигацией, и удалить их, или как-то иначе сделать их собираемыми мусором?
Обратите внимание, что я не спрашиваю, как их очистить по окончании сеанса, я уже видел различные решения для этого, я ищу способы очистить их во время сеанса, чтобы объем кучи не увеличивался чрезмерно во время сеанса из-за непреднамеренной навигации GET.
2 ответа
По сути, вы хотите, чтобы состояние представления JSF и все компоненты области видимости были разрушены во время выгрузки окна. Решение было внедрено в OmniFaces @ViewScoped
аннотация, которая изложена в его документации, как показано ниже:
Могут быть случаи, когда желательно сразу же уничтожить bean-объект области видимости, когда браузер
unload
событие вызывается. Т.е. когда пользователь уходит с помощью GET или закрывает вкладку / окно браузера. Ни одна из аннотаций области видимости JSF 2.2 не поддерживает это. Начиная с OmniFaces 2.2 эта аннотация области просмотра CDI гарантирует, что@PreDestroy
аннотированный метод также вызывается при выгрузке браузера. Этот трюк выполняется синхронным запросом XHR через автоматически включенный вспомогательный скриптomnifaces:unload.js
, Однако есть небольшая оговорка: на медленной сети и / или на слабом серверном оборудовании может быть заметная задержка между действием конечного пользователя по выгрузке страницы и желаемым результатом. Если это нежелательно, лучше придерживаться аннотаций области видимости JSF 2.2 и принять отложенное уничтожение.Начиная с OmniFaces 2.3, выгрузка была дополнительно улучшена, чтобы также физически удалить связанное состояние представления JSF из внутренней карты LRU реализации JSF в случае сохранения состояния на стороне сервера, тем самым дополнительно уменьшая риск на
ViewExpiredException
на других видах, которые были созданы / открыты ранее. Как побочный эффект этого изменения,@PreDestroy
аннотированный метод любого стандартного bean-объекта области видимости JSF, на который ссылаются в том же виде, что и bean-объект области видимости CDI OmniFaces, также будет гарантированно вызываться при выгрузке браузера.
Вы можете найти соответствующий исходный код здесь:
- Инициализатор сценария выгрузки:
ViewScopeManager#registerUnloadScript()
- Сам скрипт выгрузки:
unload.unminified.js
- Обработчик представления unload:
OmniViewHandler#unloadView()
- Разрушитель состояния просмотра JSF:
Hacks#removeViewState()
Скрипт выгрузки будет запущен во время работы окна beforeunload
событие, если оно не вызвано какой-либо отправкой формы на основе JSF (ajax). Что касается командных ссылок и / или ajax, то это зависит от реализации. В настоящее время Mojarra, MyFaces и PrimeFaces признаны.
Сценарий выгрузки сработает 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);
}
Обработчик вида unload явно уничтожит все @ViewScoped
bean-компоненты, в том числе стандартные JSF (обратите внимание, что сценарий unload инициализируется только тогда, когда представление ссылается как минимум на один OmniFaces @ViewScoped
боб).
context.getApplication().publishEvent(context, PreDestroyViewMapEvent.class, UIViewRoot.class, createdView);
Это, однако, не разрушает физическое состояние просмотра JSF в сеансе HTTP, и, таким образом, приведенный ниже вариант использования будет неудачным:
- Установите количество физических видов на 3 (в Мохарре используйте
com.sun.faces.numberOfLogicalViews
контекстный параметр и в использовании MyFacesorg.apache.myfaces.NUMBER_OF_VIEWS_IN_SESSION
контекстный параметр). - Создайте страницу, которая ссылается на стандартную JSF
@ViewScoped
боб. - Откройте эту страницу во вкладке и держите ее открытой все время.
- Откройте ту же страницу в другой вкладке, а затем немедленно закройте эту вкладку.
- Откройте ту же страницу в другой вкладке, а затем немедленно закройте эту вкладку.
- Откройте ту же страницу в другой вкладке, а затем немедленно закройте эту вкладку.
- Отправьте форму на первой вкладке.
Это не удастся с 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.
Ладно, я что-то сковал.
Принцип
Теперь нерелевантные bean-объекты с видимой областью видимости находятся там, тратя впустую время и пространство каждого, потому что в случае навигации GET, с использованием любого из выделенных вами элементов управления, сервер не задействован. Если сервер не задействован, он не может знать, что bean-объекты видимости теперь избыточны (то есть до тех пор, пока сеанс не прекратится). Так что здесь нужен способ сообщить стороне сервера, что представление, из которого вы перемещаетесь, должно завершать свои bean-объекты в области видимости.
Ограничения
Серверная сторона должна быть уведомлена, как только происходит навигация
beforeunload
или жеunload
в<h:body/>
было бы идеально, но для следующих проблемРешение с использованием любого из них, скорее всего, потребует решения AJAX, которое выходит за рамки JSF. JSF -готовый сценарий должен быть выполнен в контексте формы. Вы не можете иметь
<h:body/>
внутри формы. Я предпочитаю держать все это внутри JSF
Вы не можете отправить запрос AJAX в
onclick
элемента управления, а также перемещаться в том же элементе управления. Во всяком случае, не без грязного всплывающего окна. Итак, навигацияonclick
вh:button
или жеh:link
вне этого
Грязный компромисс
Запустить запрос ajax onclick
и есть PhaseListener
выполнить фактическую навигацию и очистку видимости
Рецепт
1
PhaseListener
(аViewHandler
также будет работать здесь; Я иду с первым, потому что это проще в настройке)1 обертка вокруг JSF js API
Средняя порция стыда
Посмотрим:
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; } }
Обертка 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 является подозрительным; может открыть дыру