这是公司的一个重要项目中的真实案例(目前还未证实其它版本是否存在,不过刚看了最新版5.1 .26版本还是没有修复这个操作方式,不过用的小伙伴们要注意了哦):
【该BUG,官方目前最新版本已经修复,详细请参考文章最后,大家注意使用的版本和原因即可】
什么样的情况呢,当在代码中使用connection.close()方法的时候,神奇般的StackOverflow了!没错,这就是JDBC自己导致的死递归,堆栈输出的内容如下所示:
这个堆栈信息可以这样反推程序:
ConnectionImpl.realClose()
-> ConnectionImpl.closeAllOpenStatements()
->StatementImpl.realClose()
->ResultSetImpl.close()
->ResultSetImpl.realClose()
->RowDataDynamic.close()
->StatementImpl.executeSimpleNonQuery()
->ConnectionImpl.execSQL()
->ConnectionImpl.cleanup()
->ConnectionImpl.realClose()//到这里回来了,于是乎接下来的事情,就按照这个顺序一发不可收拾,栈日志TNND几十米长,最后StackOverflowError是必然的了。
这是多么神奇的事情啊,MySQL的JDBC发布的时候难道也没有测试下,但是这种情况据同事介绍也不是每次都会发生,是偶然性情况。于是就要跟一下内在代码是怎么回事。
首先在MySQL JDBC现在的代码中,Connection接口是通过它内部的一个com.mysql.jdbc.ConnectionImpl的实现类来实现的,因此要跟踪close方法,就跟踪它就好了,如下图:
这里进入正轨,realClose方法进去了,这个方法很长,里面涉及到一些判定是否已经关闭、回滚、io关闭等等操作,在io关闭操作之前,需要关闭被打开的Statement信息,换句话说,MySQL在调用Connection.close的时候会自动关闭掉Statement信息,而无需业务代码来编写,不过你也写了也没错,其余代码我们忽略掉,因为不是问题的重点,关键是它内部确实调用了一个这样的方法,如下图所示:
在这个方法里面,会做什么动作呢?简单来说,就是循环,并调用对应的statement的close()方法
注意这里传入了两个参数,分别是false、true,第二个参数对这个问题是有用途的,第二个参数代表是否关闭掉Statement下面的ResultSet,在StatementImpl类具体的实现方法中的部分是:
第一个close自然是close掉当前的ResetSet、第二个是要获取generatedKey的结果集,最后一个closeAllOpenResults是会关闭掉内部记录的一个Set列表,这个列表会在获取GeneratedKeys、getMoreResults(java.sql.Statement.KEEP_CURRENT_RESULT)是增加ResultSet进去。
我们这里主要关心第一个,就是ResultSet的close方法,它ResultSetImpl实现类的close方法,如下所示:
这里依然调用了ResultSet的realClose方法,和日志中输出的内容一致,这个方法的finally部分会调用一个叫做rowData.close()方法:
它的类型是:RowData,是MySQL的一个接口:com.mysql.jdbc.RowData,它的实现类有:
具体使用哪一个,会由一些参数来决定,这个说起来又会涉及到许多源码,与本问题关系不大,暂时不扯开,从堆栈输出中可以看到使用的是第二个RowDataDynamic这个实现类,于是乎打开这个close方法的代码来看看,也很长,不过我们关注关键的部分,那就是它还调用了statement,如下图所示:
这里关闭的时候其实要执行一条语句,它创建了一个Statement,没有什么问题,因为这个Statement和当前ResultSet的Statement不是同一个(也就是这个Statement可能不是用的RowDataDynamic),但是它调用了一个executeSimpleNonQuery,这个方法需要传入Connection对象,显然一个Connection下面不论多少个Statement,这个Connection都是同一个。这个方法内部做了什么呢?
这个代码显然调用了connection的一个executeSQL方法(注意了,这里的MySQLConnection其实是一个接口,是MySQL自己继承于java.sql.Connection的接口,ConnectionImpl也是实现这个接口的,对象始终是同一个)。
接下来又回到ConnectionImpl的execSQL方法里面了,这段代码在内部的抛出异常的时候,且highAvailabilityAsBoolean为false的时候,会调用cleanup方法(默认为false,只有设置了autoReconnect才会变成true,这个参数在初始化Connection的时候被赋值):
这里只有抛出异常的时候会到这个里面来,但是异常确实发生了,而且这种发生往往是偶然的,而且一旦偶然发生,将一发不可收拾(例如网络闪了,或服务器端做了什么kill之类的操作),这个cleanup方法内部就会再次调用realClose方法:
显然,这里的Connection还没有关闭完,所以io不会为空,而且isClose也会返回false,自然会调用realClose方法,这个方法就回到前面的第二幅图的代码了,就这样,程序一发不可收的开始递归了。
使用类似代码的童鞋要注意了,换下版本就好,其余的版本还没看过代码,5.1.16代码路径有所不同,但也有类似的问题,打算抽时间看看5.1.26是否已经修复。
版本中5.1.16中在RowDataDynamic的close()方法也同样调用了realClose,里面并没有调用ConnectionImpl来操作,而是直接用本地的一个ResultSet将其关闭掉了:
另外,需要注意的是,其实StackOverflow往往没有平时Demo演示的那么简单,往往经过复杂的嵌套逻辑,以及希望大量的代码复用,在一些偶然的逻辑结构下导致递归起来,而且这种偶然一旦发生可能就形成一种必然了。
最后,小伙伴们不要认为最新的东西就是最好的哦,哈哈!这几天经过验证,可以很容易重现,测试了5.1.6、5.1.16、5.1.26、5.1.27(最新)全部会抛出错误,不过5.1.16以前的StackOverflowError代码路径不同而已。
测试的顺序:
1、在创建的Statement中,使用:((StatementImpl)statement).enableStreamingResults()
2、在发生close动作之前(可以使用断点或其它某种方式),将服务器端对应的session kill,或者将网络断开,或者直接server重启,相信这种操作线上发生是很正常的,不是故意用变态场景来模拟。
3、调用close方法,立即触发,小伙伴们可以自己模拟哈。
【更新于2015-12-18】:
官方邮件回复已经解决该问题,fix bug的版本为5.1.28,经过验证已经OK,并将同样的程序重现在5.1.27上会出现StackOverflowError。
不过值得注意的是,在5.1.28、5.1.29两个版本中,在连接断开和会话被kill的情况下,如果调用close方法会抛出异常:
com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: No operations allowed after statement closed.
官方后来觉得对于close方法,没有必要抛出这样的异常,因为连接关闭了就关闭了,因为本来这个动作就是做关闭的,确实也应该是这样的,即使官方抛出异常,我们最多打个没用的log,然后忽略掉。所以在5.1.30及以后的版本中,在上述场景调用close方法时是不会抛出异常的。