现象
在一次重新部署服务后,莫名很多接口会随机的抛出异常,提示:ClassCastException: xxx.xxx.xxx.XXXXXXEntity cannot be cast to yyy.yyy.yyy.YYYYYEntity。XXXXXEntity是固定的,但是YYYYYEntity是随机的,任意一个实体类型。
原因
由于,异常都是在进行数据库查询后出现的,且任何一个接口中的数据库查询后都有可能出现此问题,所以初步怀疑跟系统中mybatis仅有的拦截器---PageHelper分页插件有关。
对比部署服务前后的代码变动,发现,存在一处调用PageHelper.startPage的地方,但后续并未进行数据库查询操作,但将一组XXXXXXEntity数据存放入了Page对象中。至此,大概可以知道原因了:抛出异常的接口中,在进行数据库操作后,查询出来的对象应该包含了前面Page中存放的XXXXXXEntity数据对象。
源码分析
现在,我们来看下,Page对象是什么时候实例化的,及它的生命周期。
Page对象在调用PageHelper.startPage的时候实例化,并存放入ThreadLocal中:
public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count, Boolean reasonable, Boolean pageSizeZero) { //实例化一个Page对象 Page<E> page = new Page(pageNum, pageSize, count); page.setReasonable(reasonable); page.setPageSizeZero(pageSizeZero); Page<E> oldPage = SqlUtil.getLocalPage(); if (oldPage != null && oldPage.isOrderByOnly()) { page.setOrderBy(oldPage.getOrderBy()); } //将Page对象放入ThreadLocal中 SqlUtil.setLocalPage(page); return page; }
在数据库查询结束后,从ThreadLocal中移除:
public Object processPage(Invocation invocation) throws Throwable { Object var3; try { Object result = this._processPage(invocation); var3 = result; } finally { //从ThreadLocal中移除 clearLocalPage(); } return var3; }
public static void clearLocalPage() { LOCAL_PAGE.remove(); }
出现错误的代码,仅调用了Pagehelper.startPage ,并未进行数据库查询操作,所以线程中存放的Page对象并未移除。导致后续再次使用该线程进行数据库查询时,线程中的Page对象仍然存在,Page中存放的XXXXXXEntity数据也会作为此次查询返回的结果的一部分,进而出现ClassCastException异常。
结论
Pagehelper.startPage后必须跟数据库查询操作!