Я пытаюсь перенести аннотацию JSF @ViewScoped
в CDI. Причина скорее образовательная, чем основанная на потребностях. Я выбрал эту конкретную область в основном из-за отсутствия лучшего конкретного примера пользовательской области, которую можно было бы реализовать в CDI.
Тем не менее, моей отправной точкой было Перенос аннотации @ViewScoped
JSF в CDI. Но эта реализация не принимает во внимание, казалось бы, очень важную ответственность Контекст (т.е. уничтожение), упомянутый в API:
Объект контекста отвечает за создание и уничтожение контекстных экземпляров путем вызова операций Contextual. В частности, объект контекста отвечает за уничтожение любого экземпляра контекста, который он создает, путем передачи экземпляра в Contextual.destroy(Object, CreationalContext). Уничтоженный экземпляр не должен впоследствии возвращаться функцией get(). Объект контекста должен передать тот же самый экземпляр CreationalContext в Contextual.destroy(), который он передал в Contextual.create() при создании экземпляра.
Я решил добавить эту функциональность, имея свой объект Context
:
- отслеживать, какие
Contextual
объекты он создает для какихUIViewRoot
s; - реализовать интерфейс ViewMapListener и зарегистрироваться в качестве слушателя для каждого
UIViewRoot
по телефонуUIViewRoot.subscribeToViewEvent(PreDestroyViewMapEvent.class, this)
; - уничтожить все созданные
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()
? Кажется, я не могу найти никаких указаний.
FacesContext.getCurrentInstance().getApplication().subscribeToEvent(PreDestroyViewMapEvent.class, this);
и реализуютSystemEventListener
. То, как я это делаю, должно быть точно таким же, поскольку API заявляет, что вызовыUIViewRoot.subscribeToViewEvent(PreDestroyViewMapEvent.class, this);
должны вызывать другой метод. @LightGuard - person rdcrng   schedule 15.11.2012SystemEventListener
, а я реализуюViewMapListener
, который является просто подынтерфейсом, так что здесь нет проблем. Так что, кажется, в моей реализации нет ничего страшного. Проблема в том, что моя среда JSF не запускает никаких системных событий, регистрирую ли я их программно или черезfaces-config.xml
. Любая идея, почему это может происходить? Спасибо за предложение MyFaces CODI @LightGuard - person rdcrng   schedule 15.11.2012org.apache.myfaces.extensions.cdi.jsf2.impl.scope.view.ViewScopedContext
. - person rdcrng   schedule 16.11.2012