MyBatis 中接口代理机制及使用 - sqlSession.getMapper() 的使用问题
MyBatis 中接口代理机制及使用 - sqlSession.getMapper()
的使用问题
写在前面
做该笔记时,我还不了解 Java 的动态代理技术,也没有系统地追一遍 sqlSession.getMapper()
以及 mapper
调用接口方法的底层源码。
如果已经了解这些知识,就明白接口代理类 mapper
本质上是对 sqlSession
进行代理,sqlSession
就是目标对象,诸如 sqlSession.insert()
之类的方法就是目标方法。
由于 sqlSession
存在线程安全的问题,自然对其进行代理的接口代理类 mapper
也会存在线程安全的问题,毕竟代理方法底层还是需要调用目标对象的目标方法。这就把问题从根本上解释清楚了,而不是像下文这样解释得有些隔靴搔痒。
另外,Mybatis 的接口代理机制实际是用 Java 的动态代理技术来实现的,并没有直接使用到 javassist。Mybatis 中内置的 javassist 应该是用在了其他地方。
关于 MyBatis
的接口代理机制的源码分析,补充写了一篇笔记,如果可以的话,建议先阅读源码分析,本笔记相对而言,就没那么重要了。
sqlSession.getMapper()
的使用问题
在 AccountServiceImpl
中处理业务逻辑时,我们需要调 Dao 层来操作数据库。为了解耦,在 AccountServiceImpl
中是面向 AccountDao
接口来操作数据库。对于 AccountDao
接口属性,我们需要赋予接口对应的实现类,才能使用。
第 8 章中,老杜提到了下面三种方式,来给 AccountDao
接口提供对应的实现类:
1 | public class AccountServiceImpl implements AccountService { |
其中方式三,测试后发现存在问题:只能转账一次,第二次转账就会报错。
问题溯源
在 accountDao
属性所在行和 transfer()
中的首行打上断点,然后以 Debug 模式启动 Tomcat:
1 | public class AccountServiceImpl implements AccountService { |
Tomcat 服务器启动后,此时 accountDao
属性所在行的断点未被触发,说明还未执行初始化操作。
这与 Servlet 的生命周期有关,如果不做明确配置,默认情况下,Servlet 创建于用户第一次调用对应于该 Servlet 的 URL 时。
目前还未访问 /bank/transfer
,所以 AccountServlet
还未被创建,自然 accountDao
属性还未被初始化。
1 |
|
在转账页面输入信息并点击转账后,第一次访问 /bank/transfer
,此时需要创建 AccountServlet
,其内部的 accountService
属性需要先创建 AccountServiceImpl
,其内部的 accountDao
属性又需要先通过接口代理机制,获取到接口代理实现类。
因此 accountDao
属性所在行被触发,执行完当前行后,发现 accountDao
的接口代理类中,实际上持有一个 SqlSession
对象,这个对象,就是调用 getMapper()
方法的 SqlSession
实例对象。
直接跳到 transfer()
首行的断点处,执行完该行后,发现 transfer()
首行获取到的 sqlSession
和 accountDao
接口代理类中持有的 sqlSession
是同一个!
这一点不难解释,第一次访问 /bank/transfer
时,AccountServlet
才被初始化,从而 accountDao
接口属性在第一次访问时,才通过接口代理机制后获取代理类,之后又需要执行 transfer()
方法,两个断点所在行的代码,是在一个线程下执行的,自然获取到的是同一个 SqlSession
对象。
第一次 transfer()
方法执行完以后,sqlSession
会被关闭,这意味着 accountDao
接口代理类中持有的 sqlSession
也被关闭了。执行完成后,页面跳转到转账成功页面,同时数据库中表也相应完成了转账的数据修改。
紧接着,回到转账页面,第二次发起转账请求,第二次执行 transfer()
方法时,首行获取到的 sqlSession
是新的刚开启的 SqlSession
对象,而此时 accountDao
属性中持有的 sqlSession
是第一次请求获取到的且已经被关闭,在执行 accountDao.selectByActno(fromActno)
时,程序抛出异常,转账失败:
1 | Error querying database. Cause: org.apache.ibatis.executor.ExecutorException: Executor was closed. |
问题发生的原因和解决方案
直接说原因:在第二次转账的时候,transfer()
中 accountDao.selectByActno
方法底层需要使用 accountDao
内部持有的 sqlSession
来执行数据库操作(而不是 transfer()
首行创建的新 sqlSession
),但是 accountDao
持有的 sqlSession
在第一次转账完成后就被关闭了。
那我们自己实现的接口代理机制 GenerateDaoProxy.generate()
为什么第二次转账依然正常?
因为 GenerateDaoProxy.generate()
生成的接口代理,其内部并不持有一个 SqlSession
对象,每次调用接口的方法,其方法实现中都是先 SqlSessionUtil.openSession()
重新获取 SqlSession
对象来操作数据库。
1 | // 拼接方法体内容的首行:SqlSessionUtil.openSession() 重新获取 SqlSession 对象 |
至于最开始调用 GenerateDaoProxy.generate()
方法传入的 SqlSession
,也是用来实现完所有的接口方法后,就不再使用了。
所以,老杜的 GenerateDaoProxy.generate()
方法还需要一点小改进:就是在方法最后,要手动关闭方法传入的 SqlSession
参数。
那解决方案是什么呢?
我先说一种错误的解决方案:修改 SqlSessionUtil
,让 SqlSessionUtil
内部长期持有一个 SqlSession
静态对象,该 SqlSession
对象不会放到 ThreadLocal
中,而是让各种接口代理类去长期持有。
这样的解决方案错在哪里?
回忆上面说的问题的原因,transfer()
中 accountDao.selectByActno
方法底层需要使用 accountDao
内部持有的 sqlSession
来执行数据库操作。
如果采用这种解决方案,那么 accountDao
调用任何接口方法,使用的永远是 SqlSessionUtil
中的 SqlSession
静态对象,而不是 transfer()
方法首行创建的 SqlSession
对象。这就导致了:
SqlSessionUtil
中的SqlSession
静态对象在每次 Dao 接口代理类调用接口方法时,都执行了数据库操作,但永远不做提交。transfer()
方法首行创建的SqlSession
对象没有做任何操作,就提交了。
1 | public class AccountServiceImpl implements AccountService { |
另外,各种 Dao 接口代理类共用同一个 SqlSession
的设计,还存在一个问题:每次调用方法时,都是通过同一个 SqlSession
对象做数据库操作,这在并发场景下显然存在线程安全问题!
那么正确的解决方案是什么呢?
能想到的正确的解决方案还是有的,但是都需要生成多次 AccountDao
接口的代理类,而且,这似乎是无法避免的。
比如说,每次执行转账操作,都使用 transfer()
首行获取到的 sqlSession
来重新生成一次 AccountDao
接口的代理类。如下所示:
1 | public class AccountServiceImpl implements AccountService { |
每次转账操作,accountDao
持有的 sqlSession
总是本次转账操作中新获取的 sqlSession
,这样问题就得到解决了。
但不优雅的地方在于,每次调用 transfer()
方法都需要重新生成接口代理。
当然有解决方案,可以减少重新生成接口代理类的次数。但我认为,需要生成多次接口代理似乎是无法避免的,因为接口的代理类从设计上来看就是线程不安全的:
sqlSession.getMapper()
生成的接口代理类内部,会持有一个 sqlSession
对象,该 sqlSession
对象就是生成接口代理时所用到的 sqlSession
对象,而每次调用接口方法,都会使用代理类内部的 sqlSession
来执行数据库操作。
SqlSession
是线程不安全的,而 Mybatis 生成的接口代理类又和 SqlSession
强绑定,所以接口代理类也是有线程安全问题的。
既然存在线程安全问题,就需要像 SqlSession
那样,在多线程环境下,做线程的隔离,每个线程一个 SqlSession
,每个线程生成一次接口代理类!
遗留问题
在学习完 Spring 之后,再回过头来想想 Mybatis 的接口代理机制生成的接口代理类,是如何保证线程安全的?
已解决:参考 SSM 整合 - Spring 与 MyBatis 的整合细节 > 细节五:在 Spring 声明式事务下,Spring 和 Mybatis 的两个事务管理器是如何协同工作的?