Posted on: 2014-01-11, Last modified: 2015-07-31, View: 4347
如果项目是JSF和Spring的整合实现,会遇到一个小问题,要想让JSF的后台Bean与Spring的业务Bean放在一个容器管理的话,那么都要统一使用CDI的标签来声明Bean。但是Spring的CDI实现中没有ViewScope的定义,不得不说这是个很有用的东西,那么就要自己来实现它,下面是一个可行的方式:
1、定义ViewScope实现类
public class ViewScope implements Scope{
@Override
public Object get(String s, ObjectFactory<?> objectFactory) {
Map<String, Object> viewMap = FacesContext.getCurrentInstance().getViewRoot().getViewMap();
if (viewMap.containsKey(s)) {
return viewMap.get(s);
} else {
Object object = objectFactory.getObject();
viewMap.put(s, object);
return object;
}
}
@Override
public Object remove(String s) {
return FacesContext.getCurrentInstance().getViewRoot().getViewMap().remove(s);
}
@Override
public void registerDestructionCallback(String s, Runnable runnable) {
}
@Override
public Object resolveContextualObject(String s) {
return null;
}
@Override
public String getConversationId() {
return null;
}
}
所有自定义的Scope都要实现Scope接口,这个ViewScope也就是把新生成的Bean放在ViewMap里,在新的View里会重新生成Bean。
2、注册自定义ViewScope到容器
注册自定义ViewScope到容器有两种方式,一种是在带哪里动态注册,一种是在配置文件里注册,这里采用后一种方式,更符合项目的需求,在applicationContext.xml文件里添加下面的配置:
<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer"> <property name="scopes"> <map> <entry key="view"> <bean class="com.sonft.blog.utils.ViewScope"/> </entry> </map> </property> </bean>
3、使用
定义完成后如同正常的Spring CDI Bean使用即可:
@Named
@Scope("view")
public class ViewBean implements Serializable {
}
存在的问题:
但是最终在项目的使用中,这个实现存在一个问题,在tomcat服务器中部署后出现了内存泄漏,特别是访问量随着时间增多以后内存会一直增长,经过分析发现是程序生成了大量的业务对象无法释放,刚开始以为是JSF自身的原因,对比其他架构的JSF应用后发现不是,对应用进行了dump分析后发现大量对象无法回收,而且都是ViewBean里定义的对象,初步猜测是因为自定义的ViewScope的问题。查看Spring的源代码有这样一段:
// Create bean instance.
if (mbd.isSingleton()) {
//do something
} else if (mbd.isPrototype()) {
//do something
} else {
String scopeName = mbd.getScope();
final Scope scope = this.scopes.get(scopeName);
if (scope == null) {
throw new IllegalStateException("No Scope registered for scope '" + scopeName + "'");
}
try {
Object scopedInstance = scope.get(beanName, new ObjectFactory<Object>() {
public Object getObject() throws BeansException {
beforePrototypeCreation(beanName);
try {
return createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
}
});
bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
}
catch (IllegalStateException ex) {
throw new BeanCreationException(beanName,
"Scope '" + scopeName + "' is not active for the current thread; " +
"consider defining a scoped proxy for this bean if you intend to refer to it from a singleton", ex);
}
}
这个是定义在AbstractBeanFactory.java里的产生新的bean的方法,可以看出新产生的Bean都在容器中注册,在客户端释放引用后并不会被回收,实质上生命周期和Singleton Bean 一样,经过实际测试也是这些ViewBean在容器关闭时才调用@PreDestroy方法。如果访问量大以后,会有大量的ViewBean驻留在内存中,里面声明的内部变量也同样得不到释放,时间一长就造成内存泄漏。
解决办法:
解决办法是利用接口里定义的registerDestructionCallback()的方法手动释放ViewBean,我的方法是在get方法判断View是否改变,如果是则销毁上次的ViewBean。同时在用户Session结束是销毁还没被释放的ViewBean,这样能够保证JVM在垃圾回收时顺利回收掉不用的ViewBean资源。
修改get方法如下:
@Override
public Object get(String s, ObjectFactory<?> objectFactory) {
Map<String, Object> viewMap = FacesContext.getCurrentInstance().getViewRoot().getViewMap();
if (viewMap.containsKey(s)) {
return viewMap.get(s);
} else {
if(viewMap.isEmpty()) {
try {
this.destroy();
} catch (Exception e) {
System.out.println("Destroy view bean failed.");
}
}
Object object = objectFactory.getObject();
viewMap.put(s, object);
return object;
}
}
定义registerDestructionCallback()如下:
@Override
public void registerDestructionCallback(String s, Runnable runnable) {
Map<String, Object> sessionMap = FacesContext.getCurrentInstance().getExternalContext().getSessionMap();
if(!sessionMap.containsKey(DESTROY_CALL_BACK_MAP)){
Map<String, Runnable> destructionCallbacks = new LinkedHashMap<String, Runnable>();
sessionMap.put(DESTROY_CALL_BACK_MAP, destructionCallbacks);
}
((Map<String, Runnable>)sessionMap.get(DESTROY_CALL_BACK_MAP)).put(s, runnable);
}
添加Destroy方法:
public void destroy() throws
Exception {
Map<String, Object> sessionMap = FacesContext.getCurrentInstance().getExternalContext().getSessionMap();
if(sessionMap.containsKey(DESTROY_CALL_BACK_MAP)){
Map<String, Runnable> destructionCallbacks = (Map<String, Runnable>)sessionMap.get(DESTROY_CALL_BACK_MAP);
for (Runnable runnable : destructionCallbacks.values()) {
runnable.run();
}
destructionCallbacks.clear();
}
}
并在session过期关闭时调用destroy方法释放剩下的资源。
在要释放资源的ViewBean里添加@PreDestroy方法
@PreDestroy
public void destroyData(){
//释放资源
}
