Posted on: 2014-01-11, Last modified: 2015-07-31, View: 4077
如果项目是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(){ //释放资源 }