警惕手工加载spring配置文件导致数据库session无法释放
问题现象:在做web应用时会碰到这种情况,某些地方无法通过web当中的ApplicationContext来获得springIOC容器提供的bean,比如提供给外界的webservice接口,这个时候就需要手工通过ClassPathXmlApplicationContext等方式来获取ApplicationContext,代码如下:
ApplicationContext context = new ClassPathXmlApplicationContext(
"applicationContext-*.xml");
IXXXService xxxservice = (IXXXService ) context
.getBean("xxxservice ");
这是一段很典型的加载。
然而,正是这种看似到处都是的加载却为后面的BUG埋下伏笔。
xxxservice是具体的业务类,它向下与DAO依赖并控制着事务,这里代表了一个经典而且简单的service,具体配置略去,值得一提的是scope,这里没有指定,默认的是单例。
一切都是那么顺利,像这样的service代码写的应该不下几百个,可能诸位写的更多,过程依然很陶醉,修改完毕。测试,再测试。会发现随着service的执行,session数在增加,没有减少的意思。是的,当时就是这样。
解决思路:这种错误出现在久经考验的框架当中,我心里是相当不安的,居然会有这种低级趣味的错误。整理思路开始分析:这段代码唯一与以前不同的地方就是,我们在web应用中,是通过容器加载提供bean的,只有容器启动的时候才会加载xml。那么重点就应该是关注XML的加载方式了。
在这里我们用的是ApplicationContext接口。注意看spring文档3.5.1.2.2 在非web应用中优雅地关闭springioc容器。它这里用到的是AbstractApplicationContext,在取得bean后,再执行一个context.registerShutdownHook();
这里实验一把,将ApplicationContext改成AbstractApplicationContext,执行context.close()。结果出来了,session已被正常回收,真相渐渐浮出水面。
AbstractApplicationContext context = new ClassPathXmlApplicationContext("classpath*:config/spring/beans.xml");
ILoginService loginService = (ILoginService)context.getBean("loginService");
context.registerShutdownHook();
Map<String, String> infoMap = loginService.getLoginInfo(name);
context.close();
java.lang.Exception: DEBUG -- CLOSE BY CLIENT STACK TRACE
Successfully destroyed PooledConnection: com.mchange.v2.c3p0.impl.NewPooledConnection@14366f3
结论:每次加载context的做法相当于每次都生成了一次新的spring容器,在默认单例的情况下,如果不及时关闭context。service所依赖的DAO当中创建的dataSource也一直存在(包括所有的单例情况下所生成的类),从日志看,service事务管辖中的session确实已经关闭,但SessionFactory还是存在的。只有在容器关闭的情况下,并指定了dataSource实例配置中的destroy- method="close",dataSource单例才会被释放。
spring文档当中对生命周期也描述的很清楚。通过DisposableBean或者指定destroy-method都能很好的释放单例对象。而prototype类型的对象需要客户端显式的指定释放,释放对象完全是客户端控制,spring不负责释放。
所以,要改善context的加载方式,尽量的少多次去加载,实在没办法的情况下,一定要记得关闭。
最后,写代码的随意性,图省事,不经思考,是造成这种BUG的罪恶根源。