Mybatis搞两下(sqlsession,动态代理)
本文主要介绍SqlSession的创建流程和Mybatis的动态代理,其中动态代理实操了源码,我觉得值得一看,能加深你对Mybatis底层文件的映射,代理类进行的CRUD的操作,即使他本身也是通过SqlSession来执行增删改查的操作。
SqlSession详解
每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的。SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先配置的 Configuration 实例来构建出 SqlSessionFactory 实例。
SqlSession创建流程:
这里出现了很多个对象, 包括但不限于SqlSessionFactoryBuilder,SqlSessionFactory,SqlSession等,这些对象的生命周期和作用域顺便说一下
SqlSessionFactoyBuilder
SqlSessionFactoryBuilder这个类的作用就是为了创建SqlSessionFactory的,一旦SqlSessionFactory创建完毕,SqlSessionFactoryBuilder就没有存在的价值了,就应该被销毁。所以SqlSessionFactoryBuilder最好的作用域就是方法体内(及作为一个本地方法变量),用完即销毁。生命周期也就是调用方法的开始到结束。
SqlSessionFactory
SqlSessionFactory可以被认为是一个数据库连接池,它的作用是创建SqlSession接口对象。因为MyBatis的本质就是Java对数据库的操作,所以SqlSessionFactory的生命周期在于于整个MyBatis的应用之中,所以一旦创建了SqlSessionFactory的生命周期就等同于MyBatis的应用周期。
由于SqlSessionFactory是一个对数据库的连接池,所以它占据着数据库的连接资源。如果创建多个SqlSessionFactory,那么就存在多个数据库连接池,这样不利于对数据资源的控制,也会导致连接资源被消耗光,出现系统宕机等情况,所以尽量避免发生这样的情况。因此在一般的应用中我们往往希望SqlSessionfactory作为一个单例,让它在应用中不共享。
SqlSession
刚才说SqlSessionFactory可以看成数据库连接池,那么SqlSession就相当于一个数据库连接(Connection对象),你可以在一个事务里面执行多条SQL,然后通过它的commit、rollback等方法,提交或者回滚事务。所以它应该存活在一个业务请求中,处理完整个请求后,应该关闭这条连接,让它归还给SqlSessionFactory,否则数据库资源就很快被消耗精光,系统应付瘫痪,所以用try…catch…fanally语句来保证其正确关闭。
每个线程都应该有自己的SqlSession实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。
Mapper
Mapper是一个接口,它由SqlSession所创建,所以它的最大生命周期至多和SqlSession保持一致,尽管它很好用,但是由于SqlSession关闭,它的数据库连接资源也会消失,所以它的生命周期应该小于等于SqlSession的生命周期。Mapper代表是一个请求中的业务处理,所以它应该在一个请求中,一旦处理完了相关的业务,就应该废弃它。
Mybatis的生命周期图
具体实战这里不做赘述,详情见上一篇博客,本篇继续磕理论。
浅谈Mybatis的动态代理
相信很多人跟我一样,每次用Mybatis的时候,都知道怎么用,不就是定义×××Mapper接口类,并利用它来实现CRUD操作,那×××Mapper.java和×××Mapper.xml到底怎么关联起来的?
这一个java一个xml,在不装插件的情况下,就像八竿子打不着的两个文件,咱也不能跳,也不能对应,很难想象他们有千丝万缕的关系。
Mapper插件
这里顺便推荐一个IDEA里的插件,可以在java和xml里对应的Mapper方法进行跳转
讲了一些题外话,来看看Mybatis利用动态代理的技术帮我们生成代理类的具体实现机制
MyBatis动态代理实现
//根据id查询用户
@Test
public void getUserById(){
SqlSession sqlSession = MybatisUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.getUserById(1);
System.out.println(user);
sqlSession.close();
}
我先从具体实现类里截出一段代码,可以看到这里我们是用sqlSession.getMapper()方法或得了UserMapper的对象,但是我们实际上是获取了UserMapper接口的代理类,然后再由代理类执行的方法。那代理类如何生成的?先看看SqlSessionFactory工厂创建的过程,比如读取mybatis-config配置文件,读取我们的Mapper.xml文件等。
mybatis-config配置文件读取
这里用new SqlSessionFactoryBuilder().build(inputStream)来创建sqlSessionFactory工厂,我们点进build的源码康康
可以看到对于配置文件的解析,调动三参build方法,具体解析代码又在这个build方法里,
继续套娃,还是调用了parse()方法,
不难发现,这个parseConfiguration方法源码里都是对mybatis全局配置文件里各个标签元素的解析,最后返回一个Configuration对象,这个对象包含了Mybatis所有的配置信息了。
概括一下,他是从XMLConfigBuilder里构建,Mybatis通过XMLConfigBuilder读取mybatis-config.xml中的配置信息,保存在Configuration中。
Mapper.xml读取
别忘了mapper的映射也是在mybatis-config.xml里配置的哦,所以有没有眼尖的发现,mapper文件的读取其实已经出现在刚才上面某张图里啦,
解析mapper映射文件
mapperElement(root.evalNode("mappers"));
咱也不墨迹了,进去看看吧
来了来了,parse()他又来了,跟上面配置文件里的是不是很像?点进去得到这个方法的完整信息。
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
//这里是解析mapper元素的方法
configuration.addLoadedResource(resource);
bindMapperForNamespace();
//这里根据namespace属性来生成动态代理类
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
可以看到configurationElement方法在这里很关键,我们打个断点直接进去康康。
由于今天讲的是动态代理,这里buildStatementFromContext解析select|insert|update|delete这些操作就不细看了,我们发现configurationElement首行就拿下了namespace的信息,存储在namespace里,然后存在了builderAssistant对象里。
回到上一步parse()中的最后一步,bindMapperForNamespace()里,他有没有去找builderAssistant去拿namespace对象呢?我们带着好奇心去找他对质一下,我怀疑他肯定摸了。
动态代理来了
bindMapperForNamespace()源码去一下
你看他摸了,他真的摸了。好了不开玩笑了,这里显然是代理最重要的部分,拿出来康康。
private void bindMapperForNamespace() {
String namespace = builderAssistant.getCurrentNamespace();
//获取mapper里的namespace对象
if (namespace != null) {
Class<?> boundType = null;
try {
//获取namespace属性对应的Class对象,存在boundType(绑定类型)里
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
//ignore, bound type is not required
//如果classnotfind,就是如果没有,我们就忽略就行了,上面一行源码里的注释也可以发现,绑定类型不是非必须的
}
if (boundType != null) {
//先判断是否加载
if (!configuration.hasMapper(boundType)) {
// Spring may not know the real resource name so we set a flag
// to prevent loading again this resource from the mapper interface
// look at MapperAnnotationBuilder#loadXmlResource
configuration.addLoadedResource("namespace:" + namespace);
//如果有对应类,就调用add*方法把他添加到configuration里
configuration.addMapper(boundType);
}
}
}
}
我们发现这里最后一步把绑定对象直接调用addMapper方法丢到Configuration类里了,大家想想这个addMapper和之前测试类里的getMapper一样?
千呼万唤始出来,来吧来吧
public class Configuration {
*********
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
***********
}
这里面的getMapper方法,将工作继续交到MapperRegistry的getMapper的方法中,要找到真正的代理类,我们还得往下走,他调用了mapperRegistry的getMapper方法,
进!
@SuppressWarnings("unchecked")
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
看到这里终于看到一个眼熟的东西了,proxy是啥?是不是我们一直找的东西,你瞅mapperProxyFactory啥意思
咱也能猜到,这是通过这个工厂来生成我们Mapper映射器的代理类吧
往下看,工厂是不是newInstance(sqlSession)了啊,那咱进去看看呗
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
根据参数类型发现,这里调用了第二个newInstance方法,创建了一个MapperProxy对象,然后又作为参数,返回到第一个方法,最后根据这个对象创建的代理类并返回,到这一步其实该拿的都拿到了,代理类已经啥都有了。
但是好像跟我们了解的JDK动态代理好像不太一样?InvocationHandler呢?invoke()呢?咱再找找。仔细一看,不就在这里嘛
当然是这个代理类实现的InvocationHandler接口,重写的invoke()方法了。
可以看到invoke最后调用execute
再进execute,发现神奇的东西来了,MapperMethod里的execute里真是啥都有,咱的CRUD操作全都封装在这里,sqlSession接口调用的最后也来到了这里,和我们自己实现的UserDao接口里直接用的SqlSession实现类的方法是一样的,这就是动态代理实现的全过程了。
感觉讲的还不是很清楚,画个getMapper方法的流程图吧
我们通过SqlSession的getMapper方法获得接口代理来进行CRUD操作,其底层走的还是SqlSession的使用方法
再来一个总体的流程图