我正在尝试将 JSF@ViewScoped
注释移植到 CDI。原因是更多的教育而不是基于需要。我选择这个特定的范围主要是因为缺少一个可能希望在 CDI 中实现的自定义范围的更好的具体示例。
也就是说,我的出发点是将JSF 注释移植@ViewScoped
到 CDI 。但是,这个实现并没有考虑到API中提到的Context的一个看似非常重要的责任(即销毁) :
上下文对象负责通过调用 Contextual 的操作来创建和销毁上下文实例。特别是,上下文对象负责通过将实例传递给 Contextual.destroy(Object, CreationalContext) 来销毁它创建的任何上下文实例。get() 不得随后返回已销毁的实例。上下文对象必须将相同的 CreationalContext 实例传递给 Contextual.destroy(),就像它在创建实例时传递给 Contextual.create() 一样。
我决定通过拥有我的Context
对象来添加此功能:
- 跟踪
Contextual
它为哪个 s 创建了哪些对象UIViewRoot
; - 实现ViewMapListener
UIViewRoot
接口并通过调用将自己注册为每个监听器UIViewRoot.subscribeToViewEvent(PreDestroyViewMapEvent.class, this)
; - 在调用 时销毁任何已创建
Contextual
的 sViewMapListener.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
contains 下添加了文件,以便在 CDIMETA-INF/services
中com.example.CustomContextsExtension
正确注册上述内容。
我现在可以制作像这样的 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 {
...
}
The beans are created properly and properly injected into JSF pages (i.e. the same instance is returned per view, new ones are created only when the view is created, the same instances are injected over multiple requests to the same view). How do I know? Imagine the above code littered with debugging code which I purposefully stripped out for clarity and since this is already a huge post.
The problem is that my ViewContext.isListenerForSource(Object source)
and ViewContext.processEvent(SystemEvent event)
are never called. I was expecting that at least upon session expiration those events would be called, since the view map is stored in the session map (correct?). I set the session timeout to 1 minute, waited, saw the timeout happen, but my listener was still not called.
I also tried adding the following to my faces-config.xml
(mostly out of the lack of ideas):
<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>
Finally, my environment is JBoss AS 7.1.1
with Mojarra 2.1.7
.
Any clues would be greatly appreciated.
EDIT: Further Investigation.
PreDestroyViewMapEvent
doesn't seem to be fired at all while PostConstructViewMapEvent
is fired as expected - every time a new view map is created, specifically during UIViewRoot.getViewMap(true)
. The documentation states that PreDestroyViewMapEvent
should be fired every time clear()
is called on the view map. That leaves to wonder - is clear()
required to be called at all? If so, when?
The only place in the documentation that I was able to find such a requirement is in FacesContext.setViewRoot()
:
If the current UIViewRoot is non-null, and calling equals() on the argument root, passing the current UIViewRoot returns false, the clear method must be called on the Map returned from UIViewRoot#getViewMap.
Does this ever happen in the normal JSF lifecycle, i.e. without programmatically calling UIViewRoot.setViewMap()
? I can't seem to find any indication.