如何为Spring CDI自定义View Scope
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(){
        //释放资源
    }

 

Go
Friend Links:
Sonft