来来来,给俏如来扎起。感谢老铁们对俏如来的支持,2021一路有你,2022我们继续加油!你的肯定是我最大的动力
博主在参加博客之星评比,点击链接 , https://bbs.csdn.net/topics/603957267 疯狂打Call!五星好评 ⭐⭐⭐⭐⭐ 感谢。
前言
Mybatis是Java 项目开发使用率非常高的一款持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。
同时Mybatis也是面试过程中被高频问到的一门技术,今天我就带大家一起来对Mybatis的重要原理及其源码进行一个分析。
Mybatis的入门案例
我们需要先写一个简单的入门案例,根据入门来分析Mybatis的执行原理
第一步:我们需要导入Mybatis的基础依赖
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis --><dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>3.4.6</version></dependency><!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.47</version></dependency>
第二步:然后准备一个mybatis的xml配置文件
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPEconfigurationPUBLIC"-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration><propertiesresource="db.properties"></properties><!--导入参数来源--><settings><settingname="logImpl"value="STDOUT_LOGGING"/><!--用于生成可视的sql命令--><settingname="mapUnderscoreToCamelCase"value="true"/></settings><typeAliases><!--如果下面这种,则自动扫描包pojo下的注解类的别名--><packagename="cn.whale.domian"></package></typeAliases><environmentsdefault="development"><environmentid="development"><!--环境--><transactionManagertype="JDBC"/><!--事务管理器--><dataSourcetype="POOLED"><!--数据源--><propertyname="driver"value="${mysql.driver}"/><propertyname="url"value="${mysql.url}"/><propertyname="username"value="${mysql.username}"/><propertyname="password"value="${mysql.password}"/></dataSource></environment></environments><mappers><mapperresource="mapper/StudentMapper.xml"/></mappers></configuration>
数据库配置文件如下
mysql.driver=com.mysql.jdbc.Driver
mysql.url=jdbc:mysql://localhost:3306/test?useSSL=false
mysql.username=root
mysql.password=admin
第三步:然后编写SQL映射文件StudnetMapper.xml , 先来个简单的查询
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPEmapperPUBLIC"-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mappernamespace="cn.whale.mapper.StudentMapper"><resultMapid="resultMap"type="cn.whale.domian.Student"><idcolumn="id"property="id"/><resultcolumn="username"property="username"/></resultMap><selectid="selectAll"resultType="cn.whale.domian.Student">
select * from student
</select><selectid="selectById"resultMap="resultMap">
select id,username from student where id = #{id}
</select></mapper>
第四步:编写实体类,和mapper映射器接口
publicinterfaceStudentMapper{List<Student>selectAll();StudentselectById(Long id);}
第五步:编写测试类
publicclassStudentTest{@Testpublicvoidtest()throwsIOException{//加载配置InputStream inputStream=Resources.getResourceAsStream("mybatis-config.xml");//创建一个sqlSessionFactorySqlSessionFactory sqlSessionFactory =newSqlSessionFactoryBuilder().build(inputStream);//创建SqlSessionSqlSession sqlSession = sqlSessionFactory.openSession();Student student = sqlSession.selectOne("cn.whale.mapper.StudentMapper.selectAll",1L);System.out.println(student);//使用最原始方式: namespace.statementid 执行List<Student> objects = sqlSession.selectList("cn.whale.mapper.StudentMapper.selectAll");
objects.stream().forEach(System.out::println);//使用mapper映射器接口方式执行StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);List<Student> students = mapper.selectAll();
students.stream().forEach(System.out::println);}}
入门案例开发完毕。
Mybatis执行流程
下面是Mybatis的组成结构图,可能你看起来会有点懵逼,但是没关系,每个组件我们都会在后面说道,这里先混个眼熟就行了
这里说几个比较重要的组件
- SqlSessionFactoryBuilder : 用到了建造者模式,用来解析配置文件和创建SqlSessionFactory
- SqlSessionFactory : 用来创建SqlSession的工厂类,全局唯一。
- SqlSession : 这个是MyBatis链接数据库的一次会话对象,如果Mybatis和数据库的链接中断,会话也就结束。所以SqlSession不应该是单利的。
- Configuration :配置对象,主要包括mybatis-config.xml中的配置项目
- Executor : 执行器,SqlSesion底层执行的时候用到
然后我们先来分析一下Mybatis的执行流程,根据上面的测试案例我们可以分为两部分。创建SqlSession和执行SqlSession。首先第一行代码 :
InputStream inputStream= Resources.getResourceAsStream("mybatis-config.xml")
的作用是加载Mybatis配置文件,然后通过
SqlSessionFactoryBuilder().build
解析配置文件,以及mapper.xml 映射文件,并创建一个
SqlSessionFactory
然后是通过
sqlSessionFactory.openSession()
创建SqlSession,SqlSession是和数据库的一次会话对象。然后是通过
sqlSession.selectList
来执行语句,可以通过“namespace”加上“statementid”来找到要执行的SQL,也可以通过获取Mapper映射器接口的方式来执行,第二种方式最终会采用第一种方式去执行SQL。
执行完SQL会有结果集,然后会调用结果映射器处理结果集并返回,下面这个图是宏观层面的,Mybatis执行流程
Mybatis执行流程如下
- 初始化阶段,加载配置文件
- 根据配置文件,创建SqlSessionFactoryBuider,执行build方法来创建SqlSessionFactory,build方法会解析配置文件,然后封装到一个Configuration对象中。Configuration会保存在创建的SqlSessionFactory
- 通过SqlSessionFactory来创建SqlSesion,底层会创建一个Executor执行器保存在SqlSession中
- 然后就是SqlSesson的执行了,SqlSession会调用 executor 执行器去执行
- 执行器中会创建一个StatementHandler,调用StatementHandler去执行Statement语句,当然执行Statement语句前涉及到参数的处理
- 执行完成之后使用ResultSetHandler映射结果为实体对象并返回
源码解析-SqlSessionFactory的创建
代码入手肯定是
new SqlSessionFactoryBuilder().build(inputStream)
,直接看源码见:org.apache.ibatis.session.SqlSessionFactoryBuilder#build(j…)
publicSqlSessionFactorybuild(InputStream inputStream){returnbuild(inputStream,null,null);}publicSqlSessionFactorybuild(InputStream inputStream,String environment,Properties properties){try{//1. 创建一个XML配置的Builder,inputStream是通过Resources.getResourceAsStream加载的xml配置//environment 和 properties都是null//这一步会创建一个 Configuration对象XMLConfigBuilder parser =newXMLConfigBuilder(inputStream, environment, properties);//2. parser.parse()解析一个configuration对象,然后创建SqlSessionFactory returnbuild(parser.parse());}catch(Exception e){throwExceptionFactory.wrapException("Error building SqlSession.", e);}finally{ErrorContext.instance().reset();try{
inputStream.close();}catch(IOException e){// Intentionally ignore. Prefer previous error.}}}
Mybatis的源码相比Spring来说要简单太多了,首先是通过
Resources.getResourceAsStream
加载的xml配置(inputStream)交给 XMLConfigBuilder ,创建一个XML解析器在new XMLConfigBuilder(…)代码中会创建
Configuration
对象。然后调用
parser.parse()
解析配置,解析之后得到一个 Configuration 对象,交给builder方法去创建一个默认的DefaultSqlSessionFactory,同时把Configuraton对象保存给SqlSessionFactory。
XMLConfigBuilder是BaseBuilder的实现类,用作配置文件解析,针对于不同的构建目标还有不同的子类
我们先来看parser.parse()方法的源码,然后再看build方法的源码,见org.apache.ibatis.builder.xml.XMLConfigBuilder#parse
publicConfigurationparse(){//parsed是用来判断是否已经解析过了if(parsed){thrownewBuilderException("Each XMLConfigBuilder can only be used once.");}
parsed =true;//解析配置,使用的是XPathParser解析器来解析一个configuration根节点。parseConfiguration(parser.evalNode("/configuration"));return configuration;}
使用xPathParser解析器来解析配置文件,/configuration 就是mybatis-config.xml配置中的根节点,继续看parseConfiguration方法的源码
//解析配置项目privatevoidparseConfiguration(XNode root){try{//issue #117 read properties first//1.拿到properties元素,也就是 <properties resource="db.properties"></properties>propertiesElement(root.evalNode("properties"));//2.处理 <settings> 元素,并设置到Properties 对象中Properties settings =settingsAsProperties(root.evalNode("settings"));loadCustomVfs(settings);//3.处理 <typeAliases> 用户配置的别名typeAliasesElement(root.evalNode("typeAliases"));//4.处理 <plugins> 插件pluginElement(root.evalNode("plugins"));//5.处理 <objectFactory type=""></objectFactory> 对象工厂objectFactoryElement(root.evalNode("objectFactory"));//用来装饰object的工厂objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));//6.处理反射工厂<reflectorFactory type=""/>reflectorFactoryElement(root.evalNode("reflectorFactory"));//把 settings 设置到COnfiguration对象中settingsElement(settings);// read it after objectFactory and objectWrapperFactory issue #631//7.处理 <environments default="development"> , 数据源就在这个元素里面environmentsElement(root.evalNode("environments"));databaseIdProviderElement(root.evalNode("databaseIdProvider"));//8.处理 <typeHandlers></typeHandlers> 类型处理器typeHandlerElement(root.evalNode("typeHandlers"));//9 处理mappers 映射文件mapperElement(root.evalNode("mappers"));}catch(Exception e){thrownewBuilderException("Error parsing SQL Mapper Configuration. Cause: "+ e, e);}}
这里我们看到,parseConfiguration方法对于mybatis配置文件中的所有项目都做了解析。我大致说一下每个步骤做了啥,具体的细节水友自己去断点看
- propertiesElement(root.evalNode(“properties”)) : 解析 ,加载db.properties中的配置项目,然后会把
properties设置到Configuration的variables
存储起来 - Properties settings = settingsAsProperties(root.evalNode(“settings”)); 处理 元素,转换为Properties ,然后通过 settingsElement(settings); 方法把
settings中的配置保存到Configfiguration
。见org.apache.ibatis.builder.xml.XMLConfigBuilder#settingsElement - typeAliasesElement(root.evalNode(“typeAliases”)); :处理别名配置,如果是通过
<package>
方式配置别名,会把配置的实体类的class添加到Configuration对象的TypeAliasRegistry别名注册器中,TypeAliasRegistry内部维护了一个new HashMap<String, Class<?>>()
,就是以类的简单名字小写为key,以实体类的class为值来映射别名到HashMap。见org.apache.ibatis.type.TypeAliasRegistry 如果是通过<typeAlias type=""
方式直接配置别名,那么就会注册到BaseBuilder
的TypeAliasRegistry
中。 在Configuration对象的构造器中默认映射了很多的内置类型的别名。 - pluginElement(root.evalNode(“plugins”)); 处理插件配置,该方法会解析拦截器配置比如
<plugin interceptor="cn.whale.interceptor.MyInterceptor" ></plugin>
,然后使用反射创建实例,保存到Configuration的InterceptorChain interceptorChain
拦截器执行链中,这里使用到了责任链模式。拦截器我们后面单独写一篇文章来说。 - objectFactoryElement(root.evalNode(“objectFactory”)); 对象工厂处理解析,默认情况Mybaits在映射结果的时候会使用 DefaultObjectFactory 工厂利用反射来实例化实体类,你可以通过
<objectFactory type=""></objectFactory>
配置来创建自定义的实例化工厂。指定自己的实例化方式。该工厂类也是保存到configuration对象的ObjectFactory objectFactory 字段上。 - environmentsElement(root.evalNode(“environments”)); 环境处理,
<environments default="development">
中主要包括事务<transactionManager type="JDBC"/>
和<dataSource type="POOLED">
数据源的配置。environmentsElement方法中会根据type=JDBC
从TypeAliasRegistry
中找到一个JdbcTransactionFactory
事务工厂,使用反射创建实例,根据type="POOLED"找到一个PooledDataSource
,使用反射创建实例,最后创建一个Environment
环境对象,把 JdbcTransactionFactory 事务工厂 和PooledDataSource连接池保存到Environment中
- typeHandlerElement(root.evalNode(“typeHandlers”)); 处理类型处理器
<typeHandlers></typeHandlers>
,typeHandler是用来处理数据库的类型到Java的类型转换的,在 TypeHandlerRegistry的构造器中初始化了内置的很多的类型处理器,比如:VARCHAR类型转换为String就需要用到内置的 StringTypeHandler 处理器。当然我们也可以定义我们自己的类型处理器。 - mapperElement(root.evalNode(“mappers”)); 处理mapper映射文件,比如:
<mapper resource="mapper/StudentMapper.xml"/>
,方法内部会构建 XMLMapperBuilder 去解析xml文件,见org.apache.ibatis.builder.xml.XMLMapperBuilder#parsepublicvoidparse(){//解析 cache , cache,parameterMap,resultMap,sql,select|insert|update|delete//把SQL元素解析后封装成一个MappedStatement,存储到Configuration中的Map<String, MappedStatement>中if(!configuration.isResourceLoaded(resource)){configurationElement(parser.evalNode("/mapper")); configuration.addLoadedResource(resource);//绑定Mapper接口bindMapperForNamespace();}parsePendingResultMaps();parsePendingCacheRefs();parsePendingStatements();}
configurationElement方法底层会解析到mapper.xml文件,包括: cache,parameterMap,resultMap,sql,select | insert | update | delete等元素的解析。然后把SQL语句封装成MappedStatement
,以namespace.id 为 key
把MappedStatement映射到Configuration中的Map<String, MappedStatement>
中。 bindMapperForNamespace();方法中,会根据namespace找到mapper映射器接口,然后添加到 MapperRegistry中。privatevoidbindMapperForNamespace(){String namespace = builderAssistant.getCurrentNamespace();if(namespace !=null){Class<?> boundType =null;try{ boundType =Resources.classForName(namespace);}catch(ClassNotFoundException e){//ignore, bound type is not required}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); configuration.addMapper(boundType);}}}}
MapperRegistry中维护了一个HashMap<Class<?>, MapperProxyFactory<?>>()
,key是mapper接口的class 类型,Value是一个MapperProxyFactory工厂,工厂中维护了mapper接口和接口中的方法如下publicclassMapperProxyFactory<T>{//mapper接口privatefinalClass<T> mapperInterface;//接口中的方法privatefinalMap<Method,MapperMethod> methodCache =newConcurrentHashMap<Method,MapperMethod>();
源码解析-SqlSession的创建
接下来我们分析 sqlSessionFactory.openSession(); ,该方法是通过SqlSessionFactory创建SqlSession见:org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSession()
@OverridepublicSqlSessionopenSession(){returnopenSessionFromDataSource(configuration.getDefaultExecutorType(),null,false);}
这里重configuration中获取默认的executor类型默认是simple,事务的提交方式指定为手动提交,然后交给 openSessionFromDataSource 去处理
// execType :执行器的类型 ; autoCommit :事务提交方式为手动提交privateSqlSessionopenSessionFromDataSource(ExecutorType execType,TransactionIsolationLevel level,boolean autoCommit){Transaction tx =null;try{//1.从Configuration拿到配置对象,配置对象中包括 JdbcTranscationFactory 和 PooledDataSourcefinalEnvironment environment = configuration.getEnvironment();//从environment总获取事务工厂finalTransactionFactory transactionFactory =getTransactionFactoryFromEnvironment(environment);//2.通过事务工厂transactionFactory创建Transaction事务对象,默认实现是JdbcTransaction//JdbcTransaction中包括了Connection连接对象;DataSource 数据源;autoCommmit提交方式为false
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);//3.根据execType创建执行器,默认是SimpleExecutor,//SimpleExecutor继承了BaseExecutor,其中中维护了Transcation对象和PerpetualCache一级缓存一级configuration//SimpleExecutor被包装到 CachingExecutor 总,这里使用了装饰者模式//然后会把Executor添加到Configuration的interceptorChain拦截器链中finalExecutor executor = configuration.newExecutor(tx, execType);//4.创建SqlSession ,默认实现是DefaultSqlSessionreturnnewDefaultSqlSession(configuration, executor, autoCommit);}catch(Exception e){//5.发生异常,关闭事务closeTransaction(tx);// may have fetched a connection so lets call close()throwExceptionFactory.wrapException("Error opening session. Cause: "+ e, e);}finally{ErrorContext.instance().reset();}}
总结一下上面的方法的核心业务
- 从Configuration拿到Environment对象,该对象中包括
JdbcTranscationFactory
事务工厂 和PooledDataSource
连接池 - 从Environment中获取事务工厂TransactionFactory ,通过事务工厂transactionFactory创建Transaction事务对象,默认实现是JdbcTransaction;
JdbcTransaction
中包括了Connection连接对象、DataSource 数据源、autoCommmit提交方式为false。JdbcTransaction中提供了事务的提交,回滚,关闭等方法。 - 然后根据execType创建执行器,默认是
SimpleExecutor
,SimpleExecutor继承了BaseExecutor,BaseExecutor维护了Transcation对象和PerpetualCache
一级缓存、以及Configuration。 如果配置了 cacheEnabled=ture,会用装饰器模式对 executor 进行包装,SimpleExecutor被包装到 CachingExecutor 中,这里使用了装饰者
模式然后会把Executor添加到Configuration的interceptorChain拦截器链中publicExecutornewExecutor(Transaction transaction,ExecutorType executorType){ executorType = executorType ==null? defaultExecutorType : executorType; executorType = executorType ==null?ExecutorType.SIMPLE : executorType;Executor executor;//1.根据类型创建Executorif(ExecutorType.BATCH == executorType){ executor =newBatchExecutor(this, transaction);}elseif(ExecutorType.REUSE == executorType){ executor =newReuseExecutor(this, transaction);}else{ executor =newSimpleExecutor(this, transaction);}if(cacheEnabled){//2.如果开启了一级缓存,默认是true,把SimpleExecutor交给Cachin gExecutor ,装饰者模式 executor =newCachingExecutor(executor);}//3.把executor加入Configuration的interceptorChain拦截器链中 executor =(Executor) interceptorChain.pluginAll(executor);return executor;}
Executor的继承体系如下 - 创建SqlSession ,默认实现是DefaultSqlSession,其中维护了Configuration对象,Executor,autoCommit为false
- 在catch中关闭事务closeTransaction , 底层会调用connection.close关闭事务
这里要单独说一下三个Executor的区别
- SimpleExecutor:每执行一次 SQL就创建一个 Statement 对象,用 完立刻关闭 Statement 对象。
- ReuseExecutor:它能够实现Statement的复用,执行 SQL,会先把SQL 作为 key 查找 Statement 对象, 存在就使用,不存在就创建,用完后把Statement放置于 Map 内不关闭, 供下一次使用。
- BatchExecutor:执行 Update SQL(没有 select,不支持 select),将所 有 SQL都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存 了多个 Statement 对象,每个 Statement 对象都是 addBatch()完毕后,等待逐一执行 executeBatch()批处理。与 JDBC 批处理相同。
总结
我们来总结一下Mybatis的初始化流程
- SqlSessionFactoryBuilder 加载 mybatis-config.xml配置文件,通过XMLConfigBuilder将配置解析为Configuration,并创建一个SqlSessionFactory。
- 对于
<properties>
会解析为Properties并存储到Configuration的variables - 对于
<settings>
会解析为Properties并设置到Configuration,比如:延迟加载配置,二级缓存配置等 - 对于
<typeAliases>
会解析解析类的class注册到Configuration的TypeAliasRegistry中,其中维护了一个new HashMap<String, Class<?>>()
,TypeAliasRegistry默认以为了内置的别名。 - 对于
<plugins>
插件会注册到Configuration中的InterceptorChain拦截器链中,其中维护了List<Interceptor>
- 对于
<objectFactory>
对象工厂则会覆盖Configuration中默认的DefaultObjectFactory,它是用来处理结果时,实例化实体类的工厂。 - 对于
<environments>
会解析出 transactionManager 和 dataSource ,事务使用的是JdbcTranscationFactoy。dataSource使用的是PooledDataSource。然后把JdbcTranscationFactory和PooledDataSource保存到Environment对象中,设置给Congifuration。 - 对于
<typeHandlers>
会解析配置的typeHandler的class注册到TypeHandlerRegistry,其中维护了一个ConcurrentHashMap来存储typeHandler。 - 对于
<mapper >
mapper.xml映射文件会解析其中的 cache,parameterMap,resultMap,sql,select | insert | update | delete等元素的解析。然后把SQL语句封装成MappedStatement,以 namespace.id 为 key把MappedStatement映射到Configuration中的Map<String, MappedStatement>中。然后会根据namespace找到mapper映射器接口,然后添加到 MapperRegistry中。MapperRegistry中维护了一个HashMap<Class<?>, MapperProxyFactory<?>>() ,key是mapper接口的class 类型,Value是一个MapperProxyFactory工厂
- 根据SqlSessionFactory创建SqlSession,默认实现是DefaultSqlSession,其中会创建好 Transaction (默认JdbcTransaction)事务对象,和 Executor (默认SimpleExecutor)执行器。
接下来就是
sqlSession.selectList
执行SQL语句的代码分析了,这个我们就留到下一章来把。
创作不易,如果文章对你有所帮助,请给个好评。
版权归原作者 墨家巨子@俏如来 所有, 如有侵权,请联系我们删除。