ViewMapListener JSF не вызывается

Я пытаюсь перенести аннотацию JSF @ViewScoped в CDI. Причина скорее образовательная, чем основанная на потребностях. Я выбрал эту конкретную область в основном из-за отсутствия лучшего конкретного примера пользовательской области, которую можно было бы реализовать в CDI.

Тем не менее, моей отправной точкой было Перенос аннотации @ViewScoped JSF в CDI. Но эта реализация не принимает во внимание, казалось бы, очень важную ответственность Контекст (т.е. уничтожение), упомянутый в API:

Объект контекста отвечает за создание и уничтожение контекстных экземпляров путем вызова операций Contextual. В частности, объект контекста отвечает за уничтожение любого экземпляра контекста, который он создает, путем передачи экземпляра в Contextual.destroy(Object, CreationalContext). Уничтоженный экземпляр не должен впоследствии возвращаться функцией get(). Объект контекста должен передать тот же самый экземпляр CreationalContext в Contextual.destroy(), который он передал в Contextual.create() при создании экземпляра.

Я решил добавить эту функциональность, имея свой объект Context:

  1. отслеживать, какие Contextual объекты он создает для каких UIViewRoots;
  2. реализовать интерфейс ViewMapListener и зарегистрироваться в качестве слушателя для каждого UIViewRoot по телефону UIViewRoot.subscribeToViewEvent(PreDestroyViewMapEvent.class, this);
  3. уничтожить все созданные Contextual при вызове ViewMapListener.processEvent(SystemEvent event) и отменить регистрацию в этом 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());
    }

}

Я добавил файл javax.enterprise.inject.spi.Extension под META-INF/services, содержащий com.example.CustomContextsExtension, чтобы правильно зарегистрировать вышеуказанное в 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.

Любые подсказки будут очень признательны.

EDIT: дальнейшее расследование.

PreDestroyViewMapEvent не похоже на пока PostConstructViewMapEvent запускается, как и ожидалось - каждый время создания новой карты представления, особенно во время UIViewRoot.getViewMap(true). В документации указано, что PreDestroyViewMapEvent следует запускать каждый раз, когда clear() вызывается на карте просмотра. Остается задаться вопросом: нужно ли вообще вызывать clear()? Если да, то когда?

Единственное место в документации, где мне удалось найти такое требование, — это FacesContext.setViewRoot():

Если текущий UIViewRoot не равен нулю, а вызов equals() для корня аргумента, передача текущего UIViewRoot возвращает false, необходимо вызвать метод очистки для карты, возвращенной из UIViewRoot#getViewMap.

Происходит ли это когда-либо в обычном жизненном цикле JSF, то есть без программного вызова UIViewRoot.setViewMap()? Кажется, я не могу найти никаких указаний.


person rdcrng    schedule 14.11.2012    source источник
comment
Вы видели версию в MyFaces CODI? Это может помочь указать вам правильное направление.   -  person LightGuard    schedule 15.11.2012
comment
Теперь, когда вы упомянули об этом - я сделал. Я скачал их исходники и проверил их. Их реализация для контекста представления очень похожа на то, что я сделал выше. Основное сходство заключается в том, что они регистрируются для событий JSF таким образом FacesContext.getCurrentInstance().getApplication().subscribeToEvent(PreDestroyViewMapEvent.class, this); и реализуют SystemEventListener. То, как я это делаю, должно быть точно таким же, поскольку API заявляет, что вызовы UIViewRoot.subscribeToViewEvent(PreDestroyViewMapEvent.class, this); должны вызывать другой метод. @LightGuard   -  person rdcrng    schedule 15.11.2012
comment
Кроме того, они реализуют SystemEventListener, а я реализую ViewMapListener, который является просто подынтерфейсом, так что здесь нет проблем. Так что, кажется, в моей реализации нет ничего страшного. Проблема в том, что моя среда JSF не запускает никаких системных событий, регистрирую ли я их программно или через faces-config.xml. Любая идея, почему это может происходить? Спасибо за предложение MyFaces CODI @LightGuard   -  person rdcrng    schedule 15.11.2012
comment
Это странно, потому что я знаю, что CODI работает в вашей среде. Возможно, ошибка в Mojarra 2.1.7.   -  person LightGuard    schedule 15.11.2012
comment
@LightGuard Запуск этого на MyFaces дал те же результаты, поэтому я сомневаюсь, что это ошибка в реализации JSF. Я изучил это дальше - см. редактирование в исходном вопросе выше. Это наводит меня на мысль, что даже реализация CODI ошибочна; то есть и мой пример, и реализации CODI гарантируют, что между экземпляром представления и экземпляром типа bean существует связь 1-к-1, но не могут должным образом уничтожить экземпляры. Интересно, стоит ли сообщать об этом разработчикам CODI.   -  person rdcrng    schedule 16.11.2012
comment
Кто-нибудь из CODI это читает? Обсуждаемый класс — org.apache.myfaces.extensions.cdi.jsf2.impl.scope.view.ViewScopedContext.   -  person rdcrng    schedule 16.11.2012
comment
Сейчас мы работаем над этим в DeltaSpike. Публикация в списке пользователей или разработчиков была бы отличным местом, чтобы убедиться, что на этот раз мы все сделали правильно.   -  person LightGuard    schedule 16.11.2012


Ответы (2)



Карта представления хранится в карте LRU, потому что вы никогда не знаете, какое представление будет отправлено обратно. К сожалению, PreDestroyViewMapEvent не вызывается перед удалением с этой карты.

Обходной путь — сослаться на ваш объект из WeakReference. Вы можете использовать ReferenceQueue или проверить ссылку, когда вызывать код уничтожения.

person Tires    schedule 25.11.2013
comment
К вашему сведению: вклад rdcrng в конечном итоге оказался в OmniFaces, начиная с версии 1.6, которую я также усовершенствовал. Карта LRU, используемая OmniFaces, поддерживает запуск прослушивателя при выселении, поэтому этот случай также рассматривается. См. также showcase.omnifaces.org/cdi/ViewScoped, например, и ссылки на документы/источники. - person BalusC; 25.11.2013
comment
@Tires Действительно, как отмечает BalusC, с помощью сообщества Omnifaces это стало приятной функцией Omnifaces - отличной реализацией, которая работает на многих серверах приложений и даже в кластерных средах. Что касается вашей идеи использования слабых ссылок - это тоже был мой первый подход, однако команде DeltaSpike, похоже, это не понравилось, см. mail-archives.apache.org/mod_mbox/incubator-deltaspike-dev /. - person rdcrng; 27.11.2013