Mybatis源码分析之如何理解SQLSession初始化
导读:本文共12270.5字符,通常情况下阅读需要41分钟。同时您也可以点击右侧朗读,来听本文内容。按键盘←(左) →(右) 方向键可以翻页。
摘要: 这次打算写一个 Mybatis 源码分析的系列,大致分为Mybatis 启动流程分析Mybatis 的SQL 执行流程分析Mybatis 的拓展点以及与 Spring Boot 的整合这篇文章先来分析 Mybati初始化流程,如何读取配置文件到,以及创建出 SqlSession 示例.主要内容包括读取、解析mybatis 全局配置文件映射 mapper.java... ...
目录
(为您整理了一些要点),点击可以直达。这次打算写一个 Mybatis 源码分析的系列,大致分为
Mybatis 启动流程分析
Mybatis 的SQL 执行流程分析
Mybatis 的拓展点以及与 Spring Boot 的整合
这篇文章先来分析 Mybati初始化流程,如何读取配置文件到,以及创建出 SqlSession 示例.主要内容包括
读取、解析mybatis 全局配置文件
映射 mapper.java 文件
解析 mapper.xml 文件
解析 mapper.xml 各个节点配置,包括 namespace、缓存、增删改查节点
Mybatis 缓存机制
构建DefaultSqlSessionFactory
什么是 SQLSession
SQLSession对外提供了用户和数据库之间交互需要的所有方法,隐藏了底层的细节。默认实现类是DefaultSqlSession
SQLSession 创建示例
通过一个mybatis 官方提供的示例,看下如何手动创建 SQLSession
//Mybatis配置文件,通常包含:数据库连接信息,Mapper.class全限定名包路径,事务配置,插件配置等等Stringresource="org/mybatis/builder/mybatis-config.xml";//以输入流的方式读取配置InputStreaminputStream=Resources.getResourceAsStream(resource);//实例化出SQLSession的必要步骤SqlSessionFactoryBuilder-->SqlSessionFactory-->SqlSessionSqlSessionFactoryBuilderbuilder=newSqlSessionFactoryBuilder();SqlSessionFactoryfactory=builder.build(inputStream);SqlSessionsession=factory.openSession();
通过输入流读取mybatis-config.xml 配置文件
接下来就通过new SqlSessionFactoryBuilder() 开始我们的构建 SQLSession 源码分析
//SqlSessionFactory有4个构造方法,最终都会执行到全参的构造方法publicSqlSessionFactorybuild(Readerreader,Stringenvironment,Propertiesproperties){try{//首先会实例化一个XMLConfigBuilder,这里先有个基本的认知:XMLConfigBuilder就是用来解析XML文件配置的XMLConfigBuilderparser=newXMLConfigBuilder(reader,environment,properties);//经过parser.parse()之后,XML配置文件已经被解析成了Configuration,Configuration对象是包含着mybatis的所有属性.returnbuild(parser.parse());}catch(Exceptione){throwExceptionFactory.wrapException("ErrorbuildingSqlSession.",e);}finally{//...关闭流,抛异常}}
插句嘴,先看下XMLConfigBuilder 类图
MLConfigBuilder : 解析全局配置文件即 mybatis-config.xml
XMLMapperBuilder : 解析 Mapper 文件,配置在mybatis-config.xml 文件中 mapper.java 的包路径
XMLStatementBuilder :解析 mapper 文件的节点中 ,SQL 语句标签:select,update,insert,delete
SQLSourceBuilder:动态解析 SQL 语句,根据 SqlNode 解析 Sql 语句中的标签,比如<trim>,<if>等标签
当然 BaseBuilder 的实现类不仅这 4 个,这里只介绍这 4 类,在后续一步步分析中都能看到这几个的身影 点进去看一下 parser.parse()
publicConfigurationparse(){//若已经解析过了就抛出异常if(parsed){thrownewBuilderException("EachXMLConfigBuildercanonlybeusedonce.");}//设置解析标志位parsed=true;//解析mybatis-config.xml的节点,读取配置文件,加载到Configuration中parseConfiguration(parser.evalNode("/configuration"));returnconfiguration;}
通过XPathParser 对象来解析 xml 文件成XNode 对象
解析成 Configuration 成之前会先将 xml 配置文件解析成 XNode 对象
publicXNodeevalNode(Objectroot,Stringexpression){//mybatis自已定义了一个XPathParser对象来解析xml,其实对Document做了封装Nodenode=(Node)evaluate(expression,root,XPathConstants.NODE);if(node==null){returnnull;}returnnewXNode(this,node,variables);}
解析config.xml 中的各个节点
接下来就看看下mybatis 是如何一步步读取配置文件的
/***解析mybatis-config.xml的configuration节点*解析XML中的各个节点*/privatevoidparseConfiguration(XNoderoot){try{/***解析properties节点*<propertiesresource="mybatis/db.properties"/>*解析到org.apache.ibatis.parsing.XPathParser#variables*org.apache.ibatis.session.Configuration#variables*/propertiesElement(root.evalNode("properties"));/***解析我们的mybatis-config.xml中的settings节点*具体可以配置哪些属性:http://www.mybatis.org/mybatis-3/zh/configuration.html#settings*<settings><settingname="cacheEnabled"value="true"/><settingname="lazyLoadingEnabled"value="true"/><settingname="mapUnderscoreToCamelCase"value="false"/><settingname="localCacheScope"value="SESSION"/><settingname="jdbcTypeForNull"value="OTHER"/>..............</settings>**/Propertiessettings=settingsAsProperties(root.evalNode("settings"));/***基本没有用过该属性*VFS含义是虚拟文件系统;主要是通过程序能够方便读取本地文件系统、FTP文件系统等系统中的文件资源。Mybatis中提供了VFS这个配置,主要是通过该配置可以加载自定义的虚拟文件系统应用程序解析到:org.apache.ibatis.session.Configuration#vfsImpl*/loadCustomVfs(settings);/***指定MyBatis所用日志的具体实现,未指定时将自动查找。*SLF4J|LOG4J|LOG4J2|JDK_LOGGING|COMMONS_LOGGING|STDOUT_LOGGING|NO_LOGGING*解析到org.apache.ibatis.session.Configuration#logImpl*/loadCustomLogImpl(settings);/***解析我们的别名*<typeAliases><typeAliasalias="User"type="com.xxx.entity.User"/></typeAliases><typeAliases><packagename="com.xxx.use"/></typeAliases>解析到oorg.apache.ibatis.session.Configuration#typeAliasRegistry.typeAliases*/typeAliasesElement(root.evalNode("typeAliases"));/***解析我们的插件(比如分页插件)*mybatis自带的*Executor(update,query,flushStatements,commit,rollback,getTransaction,close,isClosed)ParameterHandler(getParameterObject,setParameters)ResultSetHandler(handleResultSets,handleOutputParameters)StatementHandler(prepare,parameterize,batch,update,query)解析到:org.apache.ibatis.session.Configuration#interceptorChain.interceptors*/pluginElement(root.evalNode("plugins"));objectFactoryElement(root.evalNode("objectFactory"));objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));reflectorFactoryElement(root.evalNode("reflectorFactory"));//设置settings和默认值settingsElement(settings);//readitafterobjectFactoryandobjectWrapperFactoryissue#631/***解析我们的mybatis环境,解析DataSource<environmentsdefault="dev"><environmentid="dev"><transactionManagertype="JDBC"/><dataSourcetype="POOLED"><propertyname="driver"value="${jdbc.driver}"/><propertyname="url"value="${jdbc.url}"/><propertyname="username"value="root"/><propertyname="password"value="Zw726515"/></dataSource></environment><environmentid="test"><transactionManagertype="JDBC"/><dataSourcetype="POOLED"><propertyname="driver"value="${jdbc.driver}"/><propertyname="url"value="${jdbc.url}"/><propertyname="username"value="root"/><propertyname="password"value="123456"/></dataSource></environment></environments>*解析到:org.apache.ibatis.session.Configuration#environment*在集成spring情况下由spring-mybatis提供数据源和事务工厂*/environmentsElement(root.evalNode("environments"));/***解析数据库厂商*<databaseIdProvidertype="DB_VENDOR"><propertyname="SQLServer"value="sqlserver"/><propertyname="DB2"value="db2"/><propertyname="Oracle"value="oracle"/><propertyname="MySql"value="mysql"/></databaseIdProvider>*解析到:org.apache.ibatis.session.Configuration#databaseId*/databaseIdProviderElement(root.evalNode("databaseIdProvider"));/***解析我们的类型处理器节点*<typeHandlers><typeHandlerhandler="org.mybatis.example.ExampleTypeHandler"/></typeHandlers>解析到:org.apache.ibatis.session.Configuration#typeHandlerRegistry.typeHandlerMap*/typeHandlerElement(root.evalNode("typeHandlers"));/***最最重要的就是解析我们的mapper*resource:来注册我们的class类路径下的url:来指定我们磁盘下的或者网络资源的class:若注册Mapper不带xml文件的,这里可以直接注册若注册的Mapper带xml文件的,需要把xml文件和mapper文件同名同路径--><mappers><mapperresource="mybatis/mapper/EmployeeMapper.xml"/><mapperclass="com.tuling.mapper.DeptMapper"></mapper><packagename="com.tuling.mapper"></package>--></mappers>*解析mapper:*1.解析mapper.java接口解析到:org.apache.ibatis.session.Configuration#mapperRegistry.knownMappers*2.解析mapper.xml配置*/mapperElement(root.evalNode("mappers"));}catch(Exceptione){thrownewBuilderException("ErrorparsingSQLMapperConfiguration.Cause:"+e,e);}}
解析 mapper 文件
解析 mapper.java 接口到knowMappers 中
privatevoidmapperElement(XNodeparent)throwsException{if(parent!=null){//获取我们mappers节点下的一个一个的mapper节点for(XNodechild:parent.getChildren()){/***指定mapper的4中方式:*1.指定的mapper所在的包路径,批量注册*2.通过resource目录指定*3.通过url指定,从网络资源或者本地磁盘*4.通过class路径注册*///判断我们mapper是不是通过批量注册的if("package".equals(child.getName())){StringmapperPackage=child.getStringAttribute("name");configuration.addMappers(mapperPackage);}else{//判断从classpath下读取我们的mapperStringresource=child.getStringAttribute("resource");//判断是不是从我们的网络资源读取(或者本地磁盘得)Stringurl=child.getStringAttribute("url");//解析这种类型(要求接口和xml在同一个包下)StringmapperClass=child.getStringAttribute("class");//解析mapper文件if(resource!=null&&url==null&&mapperClass==null){ErrorContext.instance().resource(resource);//把mapper文件读取出一个流,是不是似曾相识,最开始的时候读取mybatis-config.xml配置文件也是通过输入流的方式读取的InputStreaminputStream=Resources.getResourceAsStream(resource);//创建读取XmlMapper构建器对象,用于来解析我们的mapper.xml文件,上面提到过的XMLMapperBuilder对象/***读取的mapper文件会被放入到MapperRegistry中的knownMappers中*Map<Class<?>,MapperProxyFactory<?>>knownMappers=newHashMap<>();*为后续创建Mapper代理对象做准备*/XMLMapperBuildermapperParser=newXMLMapperBuilder(inputStream,configuration,resource,configuration.getSqlFragments());//真正的解析我们的mapper.xml配置文件,这里就会来解析我们的sqlmapperParser.parse();}elseif(resource==null&&url!=null&&mapperClass==null){ErrorContext.instance().resource(url);InputStreaminputStream=Resources.getUrlAsStream(url);XMLMapperBuildermapperParser=newXMLMapperBuilder(inputStream,configuration,url,configuration.getSqlFragments());mapperParser.parse();}elseif(resource==null&&url==null&&mapperClass!=null){Class<?>mapperInterface=Resources.classForName(mapperClass);configuration.addMapper(mapperInterface);}else{thrownewBuilderException("Amapperelementmayonlyspecifyaurl,resourceorclass,butnotmorethanone.");}}}}}
解析 mapper.xml 文件
解析Mapper.xml 中的 SQL 标签
//解析的SQL语句节点会放在Configuration.MappedStatement.SqlSource中,SqlSource中包含了一个个的SQLNode,一个标签对应一个SQLNodepublicvoidparse(){//判断当前的Mapper是否被加载过if(!configuration.isResourceLoaded(resource)){//真正的解析我们的mapperconfigurationElement(parser.evalNode("/mapper"));//把资源保存到我们Configuration中configuration.addLoadedResource(resource);bindMapperForNamespace();}parsePendingResultMaps();parsePendingCacheRefs();parsePendingStatements();}
解析 mapper.xml 中的各个节点
//解析我们的<mapper></mapper>节点privatevoidconfigurationElement(XNodecontext){try{/***解析我们的namespace属性*<mappernamespace="com.xx.mapper.xxxMapper">*/Stringnamespace=context.getStringAttribute("namespace");if(namespace==null||namespace.equals("")){thrownewBuilderException("Mapper'snamespacecannotbeempty");}//保存我们当前的namespace并且判断接口完全类名==namespacebuilderAssistant.setCurrentNamespace(namespace);/***解析我们的缓存引用*说明我当前的缓存引用和DeptMapper的缓存引用一致*<cache-refnamespace="com.xx.mapper.xxxMapper"></cache-ref>解析到org.apache.ibatis.session.Configuration#cacheRefMap<当前namespace,ref-namespace>异常下(引用缓存未使用缓存):org.apache.ibatis.session.Configuration#incompleteCacheRefs*/cacheRefElement(context.evalNode("cache-ref"));/***解析我们的cache节点*<cachetype="org.mybatis.caches.ehcache.EhcacheCache"></cache>解析到:org.apache.ibatis.session.Configuration#cachesorg.apache.ibatis.builder.MapperBuilderAssistant#currentCache*/cacheElement(context.evalNode("cache"));/***解析paramterMap节点*/parameterMapElement(context.evalNodes("/mapper/parameterMap"));/***解析我们的resultMap节点*解析到:org.apache.ibatis.session.Configuration#resultMaps*异常org.apache.ibatis.session.Configuration#incompleteResultMaps**/resultMapElements(context.evalNodes("/mapper/resultMap"));/***解析我们通过sql节点*解析到org.apache.ibatis.builder.xml.XMLMapperBuilder#sqlFragments*其实等于org.apache.ibatis.session.Configuration#sqlFragments*因为他们是同一引用,在构建XMLMapperBuilder时把Configuration.getSqlFragments传进去了*/sqlElement(context.evalNodes("/mapper/sql"));/***解析我们的select|insert|update|delete节点*解析到org.apache.ibatis.session.Configuration#mappedStatements*最终SQL节点会被解析成MappedStatement,一个节点就是对应一个MappedStatement*准确的说sql节点被解析成SQLNode封装在MappedStatement.SqlSource中*SQLNode对应的就是sql节点中的子标签,比如<trim>,<if>,<where>等*/buildStatementFromContext(context.evalNodes("select|insert|update|delete"));}catch(Exceptione){thrownewBuilderException("ErrorparsingMapperXML.TheXMLlocationis'"+resource+"'.Cause:"+e,e);}}
着重分析几个解析过程
解析缓存
privatevoidcacheElement(XNodecontext){if(context!=null){/***cache元素可指定如下属性,每种属性的指定都是针对都是针对底层Cache的一种装饰,采用的是装饰器的模式*缓存属性:*1.eviction:缓存过期策略:默认是LRU*LRU–最近最少使用的:移除最长时间不被使用的对象。-->LruCache*FIFO–先进先出:按对象进入缓存的顺序来移除它们。-->FifoCache*SOFT–软引用:移除基于垃圾回收器状态和软引用规则的对象。-->SoftCache*WEAK–弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。-->WeakCache*2.flushInterval:清空缓存的时间间隔,单位毫秒,默认不清空,指定了之后将会用ScheduleCache封装*3.size:缓存对象的大小,默认是1024,其是针对LruCache而言的,LruCache默认只存储最多1024个Key*4.readOnly:默认是false,底层SerializedCache包装,会在写缓存的时候将缓存对象进行序列化,然后在读缓存的时候进行反序列化,这样每次读到的都将是一个新的对象,即使你更改了读取到的结果,也不会影响原来缓存的对象;true-给所有调用者返回缓存对象的相同实例*5.blocking:默认为false,当指定为true时将采用BlockingCache进行封装,在进行增删改之后的并发查询,只会有一条去数据库查询,而不会并发访问*6.type:type属性用来指定当前底层缓存实现类,默认是PerpetualCache,如果我们想使用自定义的Cache,则可以通过该属性来指定,对应的值是我们自定义的Cache的全路径名称*///解析cache节点的type属性Stringtype=context.getStringAttribute("type","PERPETUAL");//根据type的String获取class类型Class<?extendsCache>typeClass=typeAliasRegistry.resolveAlias(type);//获取缓存过期策略:默认是LRUStringeviction=context.getStringAttribute("eviction","LRU");Class<?extendsCache>evictionClass=typeAliasRegistry.resolveAlias(eviction);//flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。LongflushInterval=context.getLongAttribute("flushInterval");//size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是1024。Integersize=context.getIntAttribute("size");//只读)属性可以被设置为true或false。只读的缓存会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。速度上会慢一些,但是更安全,因此默认值是falsebooleanreadWrite=!context.getBooleanAttribute("readOnly",false);booleanblocking=context.getBooleanAttribute("blocking",false);Propertiesprops=context.getChildrenAsProperties();//把缓存节点加入到Configuration中//这里的builder()方法利用责任链方式循环实例化Cache对象builderAssistant.useNewCache(typeClass,evictionClass,flushInterval,size,readWrite,blocking,props);}}
Mybatis 缓存机制
MyBatis自带的缓存有一级缓存和二级缓存
Mybatis一级缓存是指Session缓存。作用域默认是一个SqlSession。默认开启一级缓存,范围有SESSION和STATEMENT两种,默认是SESSION,如果需要更改一级缓存的范围,可以在Mybatis的配置文件中,通过localCacheScope指定
<settingname="localCacheScope"value="STATEMENT"/>
Mybatis的二级缓存是指mapper映射文件。二级缓存的作用域是同一个namespace下的mapper映射文件内容,多个SqlSession共享。二级缓存是默认启用的,但是需要手动在 mapper 文件中设置启动二级缓存
//在mapper.xml文件加上此配置,该mapper文件对应的SQL就开启了缓存<cache/>
或者直接关闭缓存
//在全局配置文件中关闭缓存<settings><settingname="cacheEnabled"value="false"/></settings>
注意:如果开启了二级缓存,查询结果的映射对象一定要实现Serializable ,因为mybatis 缓存对象的时候默认是会对映射对象进行序列号操作的
解析select | insert |update |delete节点
privatevoidbuildStatementFromContext(List<XNode>list,StringrequiredDatabaseId){//循环我们的select|delte|insert|update节点for(XNodecontext:list){//创建一个xmlStatement的构建器对象finalXMLStatementBuilderstatementParser=newXMLStatementBuilder(configuration,builderAssistant,context,requiredDatabaseId);try{//通过该步骤解析之后mapper.xml的sql节点就也被解析了statementParser.parseStatementNode();}catch(IncompleteElementExceptione){configuration.addIncompleteStatement(statementParser);}}}
至此配置mybatis 的配置文件已经解析完成,配置文件已经解析成了Configuration,会到最初,我们的目标是获取 SqlSession 对象,通过new SqlSessionFactoryBuilder().build(reader) 已经构建出了一个SqlSessionFactory 工厂对象,还差一步 SqlSession session = sqlMapper.openSession();
根据 Configuration build 出 DefaultSqlSessionFactory
通过分析DefaultSqlSession 的 openSession() 来实例化 SQLSession 对象
//从session中开启一个数据源privateSqlSessionopenSessionFromDataSource(ExecutorTypeexecType,TransactionIsolationLevellevel,booleanautoCommit){Transactiontx=null;try{//获取环境变量finalEnvironmentenvironment=configuration.getEnvironment();//获取事务工厂finalTransactionFactorytransactionFactory=getTransactionFactoryFromEnvironment(environment);tx=transactionFactory.newTransaction(environment.getDataSource(),level,autoCommit);/***创建一个sql执行器对象*一般情况下若我们的mybaits的全局配置文件的cacheEnabled默认为ture就返回*一个cacheExecutor,若关闭的话返回的就是一个SimpleExecutor*/finalExecutorexecutor=configuration.newExecutor(tx,execType);//创建返回一个DeaultSqlSessoin对象返回returnnewDefaultSqlSession(configuration,executor,autoCommit);}catch(Exceptione){closeTransaction(tx);//mayhavefetchedaconnectionsoletscallclose()throwExceptionFactory.wrapException("Erroropeningsession.Cause:"+e,e);}finally{ErrorContext.instance().reset();}}
仔细一点看你会发现configuration 就是刚才千辛万苦创建出来的 Configuration 对象,包含所有 mybatis 配置信息.至此SQLSession 的创建已分析完毕.
总结
总结一下上述流程:
第一步:通过输入流读取mybatis-config.xml 配置文件
1:通过XPathParser 读取xml 配置文件成 XNode 属性 2:通过 XMLConfigBuilder 解析 mybatis-config.xml 中的各个节点配置,包括
解析properties 节点
解析settings 节点
加载日志框架
解析 typeAliases
解析拓展插架 plugins
解析数据源 DataSource
解析类型处理器 typeHandle
解析 mapper文件
第二步:读取 mapper.java 类
读取方式有 package,resource,url,class ,最终都会放入到 Map<Class<?>, MapperProxyFactory<?>> knownMappers 中
第三步: 读取 mapper.xml 节点
1.同样以输入流的方式读取 mapper.xml 文件 2.通过 XMLMapperBuilder 实例解析 mapper.xml 文件中各个接点属性
解析 namespace 属性
解析缓存引用 cache-ref
解析 cache 节点
解析 resultMap 节点
解析 sql 节点
解析 select | insert |update |delete节点 3.通过 XMLStatementBuilder 解析SQL 标签
第四步:将所有配置属性都封装到 Configuration 对象中,构建出SqlSessionFactory 工厂实例
第五步:从session中开启一个数据源 SqlSessionFactory#openSession(),默认是 DefaultSqlSession
</div> <div class="zixun-tj-product adv-bottom"></div> </div> </div> <div class="prve-next-news">
Mybatis源码分析之如何理解SQLSession初始化的详细内容,希望对您有所帮助,信息来源于网络。