此次打算写一个 Mybatis 源码分析的系列,大体分为html
- Mybatis 启动流程分析
- Mybatis 的SQL 执行流程分析
- Mybatis 的拓展点以及与 Spring Boot 的整合
这篇文章先来分析 Mybati初始化流程,如何读取配置文件到,以及建立出 SqlSession 示例.主要内容包括java
- 读取、解析mybatis 全局配置文件
- 映射 mapper.java 文件
- 解析 mapper.xml 文件
- 解析 mapper.xml 各个节点配置,包括 namespace、缓存、增删改查节点
- Mybatis 缓存机制
- 构建DefaultSqlSessionFactory
什么是 SQLSession
SQLSession对外提供了用户和数据库之间交互须要的全部方法,隐藏了底层的细节。默认实现类是DefaultSqlSessionnode
SQLSession 建立示例
经过一个mybatis 官方提供的示例,看下如何手动建立 SQLSessionmysql
//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();
经过输入流读取mybatis-config.xml 配置文件
接下来就经过new SqlSessionFactoryBuilder() 开始咱们的构建 SQLSession 源码分析spring
//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 { //... 关闭流,抛异常 } }
插句嘴,先看下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()sql
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; }
经过XPathParser 对象来解析 xml 文件成XNode 对象
解析成 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); }
解析config.xml 中的各个节点
接下来就看看下mybatis 是如何一步步读取配置文件的express
/** * 解析 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); } }
解析 mapper 文件
解析 mapper.java 接口到knowMappers 中
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 文件
解析Mapper.xml 中的 SQL 标签apache
//解析的 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自带的缓存有一级缓存和二级缓存
一级缓存
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 缓存对象的时候默认是会对映射对象进行序列号操做的
解析select | insert |update |delete节点
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();
根据 Configuration build 出 DefaultSqlSessionFactory
经过分析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 的建立已分析完毕.
总结
总结一下上述流程:
第一步:经过输入流读取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 标签