之前写过Mybatis之SqlSessionFactory源码解析,但在写这篇文章的时候,发现之前写不是那么好理解,没有给人一种看了就懂的感觉,所以在写这篇文章的时候,会穿插的介绍之前已经分析过的知识和源码,争取这次能够看完这篇文章能够深入了解其内部是怎么实现以及Mybatis的优势。
概述
我们继续借用上次使用过的Mybatis整体的一个流程图,大体的讲下每个节点被用作什么,之后再详细的分析每个节点的作用以及实现原理。
- SqlSessionFactoryBuilder.build(in): MyBatis整体入口,通过该方法会最终构建一个SqlSessionFactory(默认是DefaultSqlSessionFactory)实例,SqlSessionFactory用于获取SqlSession,并且SqlSessionFactory中包含着一个非常重要的属性-Configuration实例,Configuration实例贯穿着整个MyBatis框架,这里先了解下,之后会详解Configuation的功能和特点。
重点: Configuration - factory.openSession(): 获取SqlSession实例,该方法主要是通过SqlSessionFactory()实例获取到一个用于执行具体的SQL方法的对象-DefaultSqlSession。SqlSession实例包含了Configuration实例,该连接的事务级别以及执行该次方法的执行器Executor。
Executor(执行器):用于调用具体的方法的顶层接口,提供包括查询、更新、事务提交、回滚等方法。在SqlSession实例中会根据入参exeType创建相应的Executor执行器,包括SimpleExecutor、ReuseExecutor、BatchExecutor执行器,从这可以看出Executor采用了策略模式实现。 - sqlSession.selectOne()/update()/insert()/delete():调用数据库查询/更新/删除方法。其内部是通过Executor相应的方法。
- executor.doQuery()/doUpdate(): executor是具体的执行器,它会在方法体内根据statementId获取到RoutingStatementHandler实例,RoutingStatementHandler中封装了具体的StatementHandler以及一些公用的方法,因此RoutingStatementHandler使用了类似装饰模式的特点。并且Executor执行器在这一步会获取到SQL,并且做参数的解析和封装,最后调用StatementHandler执行最后的操作。
- statement.execute():
- resultHandler.handleResultSets():
源码分析
Configuration
Configuration是mybatis-config.xml和mapper.xml在Java对象的映射,是MyBatis至关重要的一个对象,贯穿整个MyBatis。Configuration包括默认的属性配置,关键类实例化以及保存statementId和MapperStatement、resultId和ResutMap映射关系。部分源码如下:
1 | public class Configuration { |
上述便是Configuration的一些比较常用的属性以及之后在做后边的源码分析时会用到的相关属性,其中最为重要的便是mappedStatements和resultMaps,如果开启了二级缓存则caches也会使用到。我们并没有分析mappedStatements和resultMaps的数据来源,这个在Mybatis之SqlSessionFactory中已经详细介绍过了,这里就不做分析了。Configuration实例除了上述规定后续操作使用到的属性之外,还提供了一些用于创建具体实例的方法,类似于工厂一样,如下:
1 | /** |
上边Configuration类似于一个工厂,创建后续会使用到的处理类,包括:Executor(执行器),用于创建StatementHandler和管理StatementHandler生成的Statement对象;StatementHandler用于创建包装后的Statement实现类;ParameterHandler用于处理SQL的输入参数;ResultHandler用于处理Statement的返回结果。上述只是简单的创建处理类的方法,之后小节我们会一一讲解上述的处理类和其中所用到的方法。
MappedStatement
MapperStatement是mapper.xml中select/update/insert/update标签(以下针对上述的标签统称为标签,其余会做声明)所对应的Java实体对象,在MyBatis占用着举足轻重的作用,生成MappedStatement的方法在XmlStatementBuilder类中,源码如下:
1 | public void parseStatementNode() { |
上述代码则是MappedStatement的生成方法,MappedStatement中的大部分属性在我们的XML配置中都能对应起来,只要掌握了Xml中的配置,这些属性不难理解。但是有几个比较特殊,像SqlSource、StatementType,这些我们都可以在这里先了解下,知道是在这里生成的,我们之后再分析其实现原理和源码。
还有一点我觉得MappedStatement中的实现挺有意思的,就是MapperBuilderAssistant,这个类是在解析mapper.xml时候生成的工具类,主要是为解决同一个命名空间下需要公用的部分和一些m命名空间中需要用的方法,像:<cache>标签,resource资源,创建Cache实例方法,添加ResultMap方法等,体现了设计原则中的单一原则(应该仅有一个原因涉及类的变化),虽然这里不是一个原因,但是也只涉及到命名空间这一层,未涉及其它方面,所以觉得比较好。
SqlSession
上边的小节将之前讲过的Configuration对象和MappedStatement对象再稍微的讲解了一遍,这对接下来的分析很有帮助。
在未接触Spring和MyBatis结合之前,我们通常都会使用SqlSessionFactory打开一个SqlSession会话来进行操作,SqlSession提供了执行select、update、insert、update等方法。在之后学习了Spring和MyBatis结合之后,我们使用了mapper接口的形式,其实内部也是使用sqlSession操作,只是获取SqlSession和执行具体SqlSession方法的操作由Spring AOP将其封装起来,这小节我们不关注这个,我们还是先看SqlSession的获取和SqlSession的内部实现。源码如下:
1 | private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { |
上边便是是获取SqlSession会话的方法,有上述代码可以看出最后返回的是一个DefaultSqlSession实例,其中包括了全局的Configuration配置,执行器Executor以及是否自动提交的属性,并且在之前获取了事务管理对象tx,这里说下事务管理对象tx,我们知道MyBatis默认使用的Jdbc的事务管理配置,虽然有manage的事务管理配置,但是manage配置是将事务管理交给了Spring这种框架进行事务管理,由此在单独使用MyBatis时,都是使用JdbcTranaction这个事务实例,上述也使用的是JdbcTranaction,这里了解一下。
执行器Executor,之前我们在分析Configuration类的时候有一个newExecutor方法,configuration.newExecutor()方法最后返回的实例主要是根据我们获取SqlSession会话时传进来的executorType参数来判断返回哪一类型的执行器,包括ReuseExecutor、BatchExecutor、SimpleExecutor和CacheingExecutor,CacheingExecutor使用了装饰者模式,将执行创建的执行器包装起来并附加了缓存的功能,最后是否返回CachingExecutor需要根据MyBatis全局配置的cacheEnable属性来决定。 获取Executor源码如下:
1 | /** |
上边介绍了获取SqlSession的方法,获取SqlSession时又实例化了Executor,接下来便是我们执行我们的查询操作,例如:sqlSession.selectOne(“com.xxx.dao.selectStudent”, student)。那么我们就继续看看SqlSession都提供了哪些方法并且内部的实现,下边的代码是比较重要的方法。
1 | public class DefaultSqlSession implements SqlSession { |
上述便是SqlSession提供的方法,其中有部分重载的方法因为篇幅原因没有罗列出来,但是最终调用的也就是上述已列出来的方法,我们只需要分析上述方法即可。通过上边的源码我们可以看到,上边比较重要的是selectList、update、selectCursor、select以及事务操作的相关方法。其中selectList、update、selectCursor、select方法都是先通过statement参数获取configuration全局配置中相对应的MappedStatement实例,然后在调用executor的相关方法,因此内部的操作是由Executor实现的。接下来我们就分析Executor类几个实现类和实现类具体都做了什么。Let’s go!
Executor
Executor是MyBatis的执行器,主要用于获取Statementhandler,并且通过StatementHandler创建JDBC的Statement对象.Executor有五种实现,其中CachingExecutor使用了装饰者模式提供了缓存的功能;BaseExecutor是一个基础实现,提供了一些公用的方法;SimleExecutor、ReuseExecutor、BatchExecutor三种在BaseExecutor的基础上针对不同场景做了不同的实现。我们接下来一一分析。
BaseExecutor
在学习BaseExecutor之前,我们先回顾下MyBatis的知识。MyBatis提供了会话缓存和二级缓存,会话缓存指的是的同一个会话中,如果拥有相同的sql和查询条件, 则会返回缓存中的内容而不需要查询数据库。二级缓存和会话缓存相似,只不过二级缓存的作用域是同一个命名空间下。会话缓存就是在BaseExecutor中实现。
BaseExecutor类提供了很多方法,其中最主要的便是query和update方法,因此我们就以query和update方法作为切入点来分析,首先先看query方法。
1 | @Override |
上边的代码是BaseExecutor查询入口的代码,首先会获取BoundSql实例,这个BoundSql是干什么用的呢,我们先在这里先笼统的记一下这个BoundSql就是一个XMl中标签里的sql语句在java内部的一个映射,在之后分析SqlSource的时候会介绍BoundSql;第二步创建缓存的键,怎么创建那?将MappedStatement实例的键、RowBounds中下标和结果输出数量、Sql语句以及查询参数作为条件,获取上述条件的hashCode,然后按照一定的算法计算出最终的hashCode,并将上述条件保存到集合中,具体的实现自己可以看下源码;第三部调用重载方法查询。代码如下:
1 | @Override |
上述的代码就是最重要的查询方法,可以看出会先判断会话缓存中是否有相同的键,如果有则从缓存中返回,否则查询数据库。queryFromDatabase会调用继承类的doQuery方法并且更新缓存。
update
1 | @Override |
update方法相对比较简单,再调用继承类的doupdate方法之前都需要清空会话缓存。
BaseExecutor最主要的两个方法已经分析完了,我们可以看出MyBatis将会话缓存的部分交给了基础的BaseExecutor实现,而其他执行器值针对自己需要的功能做加工即可,不用关心缓存的处理。
SimpleExecutor
SimpleExecutor是MyBatis若不开启二级缓存的情况下默认返回的实现类。SimpleExecutor提供了简单doUpdate和doQuery方法,我们先看doQuery方法。
doQuery
1 | @Override |
上边的代码就是SimpleExecutor查询的方法,其中的cofiguration.newStatementHandler方法在分析Configuration章节的时候已经介绍过了,Configuration内部虽然返回的是RoutingStatementHandler方法,但是RoutingStatementHandler的构造方法根据不同的statementType返回PrepareStatementHandler、SimpleStatementHandler、CallableStatementHandler实例。
紧接着调用prepareStatement方法,prepareStatement方法内部主要是创建Statement实例,首先创建数据库连接,然后根据不同的StatementHandler创建不同的Statement实例(这个等会分析StatementHandler实例时在分析);然后statement对象做输入参数的绑定;最后返回Statement对象。
从上述可以看出SimpleExecutor主要是做创建StatementHandler类,通过StatementHandler创建Statement对象,创建Statement对象成功之后,后续的操作即是JDBC的Statement查询、更新。还有一点说明下:ReuseExecutor的doQuery方法和SimpleExecutor是一样的,只是prepareStatement方法的内部实现不同,因此在分析ReuseExecutor和BatchExecutor时,只分析了其prepareStatement方法。
doUpdate
1 | @Override |
doUpdate方法和doQuery方法内部代码一模一样,因此不在此分析了。
ReuseExecutor
ReuseExecutor中有一个非常重要的属性statementMap,它的key是执行的sql,value值是Statement实例。通过这个属性才实现了ReuseExecutor的可重用Statement功能。
1 | private final Map<String, Statement> statementMap = new HashMap<>(); |
prepareStatement
1 | private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { |
ReuseExecutor相对于SimpleExecutor只是在prepareStatement方法中首先会从statementMap属性中尝试获取Statement对象,如果获取到则设置超时时间,未获取到则创建Statement对象并将其添加到statementMap属性中。因此从上述可以看出,如果我们在一个sqlSession会话中会多次调用相同sql语句,则可以使用ReuseExecutor,避免了多次创建和销毁Statement对象造成的空间和时间浪费。
BatchExecutor
BatchExecutor从字面意思就能够看出其主要针对的是批量的操作,批量操作有几个很重要的属性。BatchExecutor和SimpleExecutor的主要不同是doUpdate方法,因此我们着重分析doUpdate方法。
1 | private final List<Statement> statementList = new ArrayList<>(); |
doUpdate
1 | public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException { |
BatchExecutor的doUpdate方法相对SimpleExecutor方法来说好像复杂很多,但是仔细分析的话会发现还是相对简单的。BatchExecutor主要是针对于相同的SQL和MappedStatement实例,如果Sql字符串和MappedStatement实例相等的话,则获取statementList集合的最后一个Statement对象,并为其处理输入参数,这样便可以针对于拥有相同SQL和MappedStatement对象的批量执行。综上,我们可以针对于循环新增、更新、删除的操作采用BatchExecutor类。
StatementHandler
上边的一个章节分析了Executor的使用以及doQuery/doUpdate方法的内部实现,在doQuery和doUpdate方法内部都使用了configuration.newStatementHandler方法用于创建StatementHandler,那么我们继续分析MyBatis的重要组件StatementHandler。
StatementHandler初始化
1 | public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { |
StatementHandler的初始化方法在Configuration类中,在上述方法中看到返回的是一个RoutingStatementHandler实例,RoutingStatementHandler实例的构造方法中会根据MappedStatement的statementType创建相应的StatementHandler。源码如下:
1 | private final StatementHandler delegate; |
上边的代码很清晰的表明在RoutingStatementHandler的构造函数会根据statementType创建不同的StatementHandler。RoutingStatementHandler使用了外观模式,使用者不关心内部的实现,可以实现程序解耦并且对外透明,安全性较高。
我们在日常的开发使用中,使用最多的应该是PrepareStatementHandler,PrepareStatementHandler可以预编译SQL,从而防止SQL注入,提高安全性。因此我们本小节就重点分析PrepareStatementHandler。在上小节中我们知道在Executor执行器的prepareStatement方法中都会调用一个statementHandler.prepare方法,prepare方法的实现在BaseStatementHandler中,是几个StatementHandler公用的方法,实现如下:
1 | @Override |
prepare方法中最为重要的是instantiateStatement方法,instantiateStatement是个抽象方法,具体的实现在BaseStatementHandler的继承类中,PrepareStatementHandler的prepare方法实现在下方。setStatementTimeout和setFetchSize是公用方法,设置Statement的执行超时时间和期待返回的数据结果集数量。
1 | @Override |
上述代码目的就是为了创建一个Statement对象。对于这个Statement对象需要判断是否需要有自增长的列和输出结果要求进行创建,就不详细分析了,相信使用过JDBC的大家都会对这个connection.prepareStatement方法非常清楚。截至到此,我们分析了在Executor类中调用的prepare方法,紧接着会调用handler.parameterize(stmt)方法,那么我们继续看StatementHandler另一个很重要的方法,封装参数parameterize。
1 | @Override |
上述代码只有一句话,类似于使用一个专门的参数处理器来处理这个statement对象,通过查看类的继承图发现ParameterHandler只有一个实现类为DefaultParameterHandler,其setParameters(statement)方法如下:
1 | @Override |
上述的代码有点复杂,因为牵扯到之后需要介绍到的SqlSource和BoundSql的部分方法,而且MyBatis中因为有ObjectFactory对于创建实体对象的设置,因此代码看起来相对比较困难。详细的逻辑如下:
- boundSql.hasAdditionalParameter(propertyName):用于获取SQL中#{}的属性的设置。
- boundSql.hasAdditionalParameter(propertyName): 判断是否拥有这个属性,但是看了Sqlsource和BoundSql的相关源码,还是没有了解这个针对的是什么情况?希望之后有哪位大佬看到了可以给解释下。
- parameterObject == null: 对于输入参数为空的,则直接将value值设置为空。
- typeHandlerRegistry.hasTypeHandler(parameterObject.getClass()): 在我们的类型处理器中如果有对应的类型处理函数则将该对象赋值给value,之后使用类型处理函数设置。
- 否则就是实体对象,这种通过MetaObject获取实体对象中相应的属性值。
- 最后使用类型处理函数处理相应的类型,并将其设置到statement对象中。
小结
StatementHandler只分析了其中的PrepareStatementHandler对象。StatementHandler因为相对简单,只是创建了Statement对象,在处理输入参数没有做任何操作,一般我们使用也比较少,所以就不分析这个了,相信掌握PrepareStatementHandler的看到StatementHandler的代码,会觉得非常的简单。CallableStatementHandler主要是针对函数和存储过程的调用,CallableStatementHandler和PrepareStatementHandler方法类似,只是多了一个对于#{}中mode为out类型做了参数处理,其它和PrepareStatementHandler相似,因此也不在这里分析了。StatementHandler除了上述讲的prepare()和instantiateStatement方法,还有query、update、queryCursor方法,这些方法内部使用JDBC进行操作,对于有输出结果的需要通过输出类型函数进行处理。
SqlSource
暂无内容
BoundSql
暂无内容
总结
本篇详细讲解了MyBatis的整体运行流程和里边一些相对比较重要的组件,但在最后没有分析SqlSource和BoundSql的相关内容,因为我觉得这两个组件相对比较复杂,自己一时半会组织不好语言去详细的讲解清楚这两个组件,所以待之后完善。
这篇文章写完花了大概两周的时间,倒不是因为自己比较忙或是没时间写的原因,只是在写的过程中总是发现自己当时掌握的有点相对简单,不够深入,也不知道该怎么去整理章节去讲清楚MyBatis中的相关内容,幸运的是最后自己还是写完了这篇文章,也通过写这篇文章让自己明白了很多之前漏掉的内容,让自己更加的深入的了解和使用MyBatis。最后,给自己鼓个劲,虽然写的不够好,但是只要坚持,我相信你一定会做的很好,有句俗话说得好:“只要你一直努力,最坏的结果也只是大器晚成”。加油!