这篇文章主要介绍“Mybatis源码分析之如何理解SQLSession初始化”,在日常操作中,相信很多人在Mybatis源码分析之如何理解SQLSession初始化问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Mybatis源码分析之如何理解SQLSession初始化”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!
这次打算写一个 Mybatis 源码分析的系列,大致分为
Mybatis 启动流程分析
Mybatis 的SQL 执行流程分析
Mybatis 的拓展点以及与 Spring Boot 的整合
这篇文章先来分析 Mybati初始化流程,如何读取配置文件到,以及创建出 SqlSession 示例.主要内容包括
读取、解析mybatis 全局配置文件
映射 mapper.java 文件
解析 mapper.xml 文件
解析 mapper.xml 各个节点配置,包括 namespace、缓存、增删改查节点
Mybatis 缓存机制
构建DefaultSqlSessionFactory
SQLSession对外提供了用户和数据库之间交互需要的所有方法,隐藏了底层的细节。默认实现类是DefaultSqlSession
通过一个mybatis 官方提供的示例,看下如何手动创建 SQLSession
//Mybatis 配置文件,通常包含:数据库连接信息,Mapper.class 全限定名包路径,事务配置,插件配置等等 String resource = "org/mybatis/builder/mybatis-config.xml"; //以输入流的方式读取配置 InputStream inputStream = Resources.getResourceAsStream(resource); //实例化出 SQLSession 的必要步骤 SqlSessionFactoryBuilder --> SqlSessionFactory --> SqlSession SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); SqlSessionFactory factory = builder.build(inputStream); SqlSession session = factory.openSession();
接下来就通过new SqlSessionFactoryBuilder() 开始我们的构建 SQLSession 源码分析
//SqlSessionFactory 有4 个构造方法,最终都会执行到全参的构造方法 public SqlSessionFactory build(Reader reader, String environment, Properties properties) { try { //首先会实例化一个 XMLConfigBuilder ,这里先有个基本的认知:XMLConfigBuilder 就是用来解析 XML 文件配置的 XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties); //经过parser.parse()之后,XML配置文件已经被解析成了Configuration ,Configuration 对象是包含着mybatis的所有属性. return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { //... 关闭流,抛异常 } }
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()
public Configuration parse() { // 若已经解析过了 就抛出异常 if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } // 设置解析标志位 parsed = true; // 解析mybatis-config.xml的节点,读取配置文件,加载到 Configuration 中 parseConfiguration(parser.evalNode("/configuration")); return configuration; }
解析成 Configuration 成之前会先将 xml 配置文件解析成 XNode 对象
public XNode evalNode(Object root, String expression) { //mybatis 自已定义了一个XPathParser 对象来解析 xml ,其实对Document做了封装 Node node = (Node) evaluate(expression, root, XPathConstants.NODE); if (node == null) { return null; } return new XNode(this, node, variables); }
接下来就看看下mybatis 是如何一步步读取配置文件的
/** * 解析 mybatis-config.xml的 configuration节点 * 解析 XML 中的各个节点 */ private void parseConfiguration(XNode root) { try { /** * 解析 properties节点 * <properties resource="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> <setting name="cacheEnabled" value="true"/> <setting name="lazyLoadingEnabled" value="true"/> <setting name="mapUnderscoreToCamelCase" value="false"/> <setting name="localCacheScope" value="SESSION"/> <setting name="jdbcTypeForNull" value="OTHER"/> .............. </settings> * */ Properties settings = 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> <typeAlias alias="User" type="com.xxx.entity.User"/> </typeAliases> <typeAliases> <package name="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); // read it after objectFactory and objectWrapperFactory issue #631 /** * 解析我们的mybatis环境,解析 DataSource <environments default="dev"> <environment id="dev"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="root"/> <property name="password" value="Zw726515"/> </dataSource> </environment> <environment id="test"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="root"/> <property name="password" value="123456"/> </dataSource> </environment> </environments> * 解析到:org.apache.ibatis.session.Configuration#environment * 在集成spring情况下由 spring-mybatis提供数据源 和事务工厂 */ environmentsElement(root.evalNode("environments")); /** * 解析数据库厂商 * <databaseIdProvider type="DB_VENDOR"> <property name="SQL Server" value="sqlserver"/> <property name="DB2" value="db2"/> <property name="Oracle" value="oracle" /> <property name="MySql" value="mysql" /> </databaseIdProvider> * 解析到:org.apache.ibatis.session.Configuration#databaseId */ databaseIdProviderElement(root.evalNode("databaseIdProvider")); /** * 解析我们的类型处理器节点 * <typeHandlers> <typeHandler handler="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> <mapper resource="mybatis/mapper/EmployeeMapper.xml"/> <mapper class="com.tuling.mapper.DeptMapper"></mapper> <package name="com.tuling.mapper"></package> --> </mappers> * 解析 mapper: * 1.解析mapper.java接口 解析到:org.apache.ibatis.session.Configuration#mapperRegistry.knownMappers * 2.解析 mapper.xml 配置 */ mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } }
private void mapperElement(XNode parent) throws Exception { if (parent != null) { //获取我们mappers节点下的一个一个的mapper节点 for (XNode child : parent.getChildren()) { /** * 指定 mapper 的 4 中方式: * 1.指定的 mapper 所在的包路径,批量注册 * 2.通过 resource 目录指定 * 3.通过 url 指定,从网络资源或者本地磁盘 * 4.通过 class 路径注册 */ //判断我们mapper是不是通过批量注册的 if ("package".equals(child.getName())) { String mapperPackage = child.getStringAttribute("name"); configuration.addMappers(mapperPackage); } else { //判断从classpath下读取我们的mapper String resource = child.getStringAttribute("resource"); //判断是不是从我们的网络资源读取(或者本地磁盘得) String url = child.getStringAttribute("url"); //解析这种类型(要求接口和xml在同一个包下) String mapperClass = child.getStringAttribute("class"); //解析 mapper 文件 if (resource != null && url == null && mapperClass == null) { ErrorContext.instance().resource(resource); // 把mapper文件读取出一个流,是不是似曾相识,最开始的时候读取mybatis-config.xml 配置文件也是通过输入流的方式读取的 InputStream inputStream = Resources.getResourceAsStream(resource); //创建读取XmlMapper构建器对象,用于来解析我们的mapper.xml文件,上面提到过的 XMLMapperBuilder对象 /** * 读取的 mapper 文件会被放入到 MapperRegistry 中的 knownMappers中 * Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>(); * 为后续创建 Mapper 代理对象做准备 */ XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); //真正的解析我们的mapper.xml配置文件,这里就会来解析我们的sql mapperParser.parse(); } else if (resource == null && url != null && mapperClass == null) { ErrorContext.instance().resource(url); InputStream inputStream = Resources.getUrlAsStream(url); XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments()); mapperParser.parse(); } else if (resource == null && url == null && mapperClass != null) { Class<?> mapperInterface = Resources.classForName(mapperClass); configuration.addMapper(mapperInterface); } else { throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one."); } } } } }
解析Mapper.xml 中的 SQL 标签
//解析的 SQL 语句节点会放在Configuration.MappedStatement.SqlSource 中,SqlSource 中包含了一个个的 SQLNode,一个标签对应一个 SQLNode public void parse() { //判断当前的Mapper是否被加载过 if (!configuration.isResourceLoaded(resource)) { //真正的解析我们的mapper configurationElement(parser.evalNode("/mapper")); //把资源保存到我们Configuration中 configuration.addLoadedResource(resource); bindMapperForNamespace(); } parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements(); }
解析 mapper.xml 中的各个节点
//解析我们的<mapper></mapper>节点 private void configurationElement(XNode context) { try { /** * 解析我们的namespace属性 * <mapper namespace="com.xx.mapper.xxxMapper"> */ String namespace = context.getStringAttribute("namespace"); if (namespace == null || namespace.equals("")) { throw new BuilderException("Mapper's namespace cannot be empty"); } //保存我们当前的namespace 并且判断接口完全类名==namespace builderAssistant.setCurrentNamespace(namespace); /** * 解析我们的缓存引用 * 说明我当前的缓存引用和DeptMapper的缓存引用一致 * <cache-ref namespace="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节点 * <cache type="org.mybatis.caches.ehcache.EhcacheCache"></cache> 解析到:org.apache.ibatis.session.Configuration#caches org.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 (Exception e) { throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e); } }
着重分析几个解析过程
private void cacheElement(XNode context) { 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属性 String type = context.getStringAttribute("type", "PERPETUAL"); //根据type的String获取class类型 Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type); //获取缓存过期策略:默认是LRU String eviction = context.getStringAttribute("eviction", "LRU"); Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction); //flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。 Long flushInterval = context.getLongAttribute("flushInterval"); //size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。 Integer size = context.getIntAttribute("size"); //只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false boolean readWrite = !context.getBooleanAttribute("readOnly", false); boolean blocking = context.getBooleanAttribute("blocking", false); Properties props = context.getChildrenAsProperties(); //把缓存节点加入到Configuration中 //这里的 builder()方法利用责任链方式循环实例化Cache 对象 builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props); } }
MyBatis自带的缓存有一级缓存和二级缓存
Mybatis一级缓存是指Session缓存。作用域默认是一个SqlSession。默认开启一级缓存,范围有SESSION和STATEMENT两种,默认是SESSION,如果需要更改一级缓存的范围,可以在Mybatis的配置文件中,通过localCacheScope指定
<setting name="localCacheScope" value="STATEMENT"/>
Mybatis的二级缓存是指mapper映射文件。二级缓存的作用域是同一个namespace下的mapper映射文件内容,多个SqlSession共享。二级缓存是默认启用的,但是需要手动在 mapper 文件中设置启动二级缓存
//在 mapper.xml 文件加上此配置,该 mapper 文件对应的 SQL就开启了缓存 <cache />
或者直接关闭缓存
//在全局配置文件中关闭缓存 <settings> <setting name="cacheEnabled" value="false" /> </settings>
注意:如果开启了二级缓存,查询结果的映射对象一定要实现Serializable ,因为mybatis 缓存对象的时候默认是会对映射对象进行序列号操作的
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) { //循环我们的select|delte|insert|update节点 for (XNode context : list) { //创建一个xmlStatement的构建器对象 final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId); try { //通过该步骤解析之后 mapper.xml 的 sql 节点就也被解析了 statementParser.parseStatementNode(); } catch (IncompleteElementException e) { configuration.addIncompleteStatement(statementParser); } } }
至此配置mybatis 的配置文件已经解析完成,配置文件已经解析成了Configuration,会到最初,我们的目标是获取 SqlSession 对象,通过new SqlSessionFactoryBuilder().build(reader) 已经构建出了一个SqlSessionFactory 工厂对象,还差一步 SqlSession session = sqlMapper.openSession();
通过分析DefaultSqlSession 的 openSession() 来实例化 SQLSession 对象
//从session中开启一个数据源 private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { Transaction tx = null; try { //获取环境变量 final Environment environment = configuration.getEnvironment(); // 获取事务工厂 final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); /** * 创建一个sql执行器对象 * 一般情况下 若我们的mybaits的全局配置文件的cacheEnabled默认为ture就返回 * 一个cacheExecutor,若关闭的话返回的就是一个SimpleExecutor */ final Executor executor = configuration.newExecutor(tx, execType); //创建返回一个DeaultSqlSessoin对象返回 return new DefaultSqlSession(configuration, executor, autoCommit); } catch (Exception e) { closeTransaction(tx); // may have fetched a connection so lets call close() throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
仔细一点看你会发现configuration 就是刚才千辛万苦创建出来的 Configuration 对象,包含所有 mybatis 配置信息.至此SQLSession 的创建已分析完毕.
总结一下上述流程:
1:通过XPathParser 读取xml 配置文件成 XNode 属性 2:通过 XMLConfigBuilder 解析 mybatis-config.xml 中的各个节点配置,包括
解析properties 节点
解析settings 节点
加载日志框架
解析 typeAliases
解析拓展插架 plugins
解析数据源 DataSource
解析类型处理器 typeHandle
解析 mapper文件
读取方式有 package,resource,url,class ,最终都会放入到 Map<Class<?>, MapperProxyFactory<?>> knownMappers 中
1.同样以输入流的方式读取 mapper.xml 文件 2.通过 XMLMapperBuilder 实例解析 mapper.xml 文件中各个接点属性
解析 namespace 属性
解析缓存引用 cache-ref
解析 cache 节点
解析 resultMap 节点
解析 sql 节点
解析 select | insert |update |delete节点 3.通过 XMLStatementBuilder 解析SQL 标签
到此,关于“Mybatis源码分析之如何理解SQLSession初始化”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注亿速云网站,小编会继续努力为大家带来更多实用的文章!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。