Mybatis源码解析-配置、启动加载

mybatis是目前非常流行的ORM框架,在其基础上还衍生了mybatis-plus)等优秀的框架。mybatis简单,易用,易扩展。 接下来我们用一个简单的例子(几乎包含所有配置)来看一下mybatis相关的配置项 下面是一个包含用户、商品、订单的DAO例子,关系图如下: 02.learn-mybatis-demo.png

mybatis的配置

MyBatis 的配置文件包含了会深深影响 MyBatis 行为的设置和属性信息。 各属性介绍可以参考[官网](mybatis – MyBatis 3 | 配置) 此示例项目的mybatis-config.xml内容如下

<?xml version="1.0" encoding="UTF-8" ?>  
<!DOCTYPE configuration  
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"  
        "https://mybatis.org/dtd/mybatis-3-config.dtd">  
<configuration>  
    <!-- 定义属性值 -->  
    <properties>  
        <property name="username" value="root"/>  
        <property name="id" value="development"/>  
    </properties>  
  
    <!-- 全局配置信息 -->  
    <!-- 一个配置完整的 settings 元素的示例如下:-->  
    <settings>  
        <!-- 全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。-->  
        <setting name="cacheEnabled" value="true"/>  
        <!-- 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置 fetchType 属性来覆盖该项的开关状态。。-->  
        <setting name="lazyLoadingEnabled" value="true"/>  
        <!-- 开启时,任一方法的调用都会加载该对象的所有延迟加载属性。 否则,每个延迟加载属性会按需加载(参考 lazyLoadTriggerMethods)。(在 3.4.1 及之前的版本中默认为 true) -->  
        <setting name="aggressiveLazyLoading" value="true"/>  
        <!-- 是否允许单个语句返回多结果集(需要数据库驱动支持)。 -->  
        <setting name="multipleResultSetsEnabled" value="true"/>  
        <!-- 使用列标签代替列名。实际表现依赖于数据库驱动,具体可参考数据库驱动的相关文档,或通过对比测试来观察。 -->  
        <setting name="useColumnLabel" value="true"/>  
        <!-- 允许 JDBC 支持自动生成主键,需要数据库驱动支持。如果设置为 true,将强制使用自动生成主键。尽管一些数据库驱动不支持此特性,但仍可正常工作(如 Derby)。 -->  
        <setting name="useGeneratedKeys" value="false"/>  
        <!-- 指定 MyBatis 应如何自动映射列到字段或属性。 NONE 表示关闭自动映射;PARTIAL 只会自动映射没有定义嵌套结果映射的字段。 FULL 会自动映射任何复杂的结果集(无论是否嵌套)。 -->  
        <setting name="autoMappingBehavior" value="PARTIAL"/>  
        <!-- 指定发现自动映射目标未知列(或未知属性类型)的行为。  
            NONE: 不做任何反应  
            WARNING: 输出警告日志('org.apache.ibatis.session.AutoMappingUnknownColumnBehavior' 的日志等级必须设置为 WARN)  
            FAILING: 映射失败 (抛出 SqlSessionException) -->        <setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>  
        <!-- 配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(PreparedStatement); BATCH 执行器不仅重用语句还会执行批量更新。 -->  
        <setting name="defaultExecutorType" value="SIMPLE"/>  
        <!-- 设置超时时间,它决定数据库驱动等待数据库响应的秒数。 -->  
        <setting name="defaultStatementTimeout" value="60"/>  
        <!-- 为驱动的结果集获取数量(fetchSize)设置一个建议值。此参数只可以在查询设置中被覆盖。 -->  
        <setting name="defaultFetchSize" value="100"/>  
        <!-- 指定语句默认的滚动策略。  
         FORWARD_ONLY | SCROLL_SENSITIVE | SCROLL_INSENSITIVE | DEFAULT(等同于未设置)-->  
        <setting name="defaultResultSetType" value="DEFAULT"/>  
        <!-- 是否允许在嵌套语句中使用分页(RowBounds)。如果允许使用则设置为 false。 -->  
        <setting name="safeRowBoundsEnabled" value="false"/>  
        <!-- 是否允许在嵌套语句中使用结果处理器(ResultHandler)。如果允许使用则设置为 false。 -->  
        <setting name="safeResultHandlerEnabled" value="true"/>  
        <!-- 是否开启驼峰命名自动映射,即从经典数据库列名 A_COLUMN 映射到经典 Java 属性名 aColumn。 -->  
        <setting name="mapUnderscoreToCamelCase" value="false"/>  
        <!-- MyBatis 利用本地缓存机制(Local Cache)防止循环引用和加速重复的嵌套查询。 默认值为 SESSION,会缓存一个会话中执行的所有查询。  
            若设置值为 STATEMENT,本地缓存将仅用于执行语句,对相同 SqlSession 的不同查询将不会进行缓存。 -->  
        <setting name="localCacheScope" value="SESSION"/>  
        <!-- 当没有为参数指定特定的 JDBC 类型时,空值的默认 JDBC 类型。 某些数据库驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHER。 -->  
        <setting name="jdbcTypeForNull" value="OTHER"/>  
        <!-- 指定对象的哪些方法触发一次延迟加载。 -->  
        <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>  
        <!-- 指定动态 SQL 生成使用的默认脚本语言。 -->  
        <setting name="defaultScriptingLanguage" value="org.apache.ibatis.scripting.xmltags.XMLLanguageDriver"/>  
        <!-- 指定 Enum 使用的默认 TypeHandler 。 -->  
        <setting name="defaultEnumTypeHandler" value="org.apache.ibatis.type.EnumTypeHandler"/>  
        <!-- 指定当结果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)方法,这在依赖于 Map.keySet() 或 null 值进行初始化时比较有用。注意基本类型(int、boolean 等)是不能设置成 null 的。 -->  
        <setting name="callSettersOnNulls" value="false"/>  
        <!-- 当返回行的所有列都是空时,MyBatis默认返回 null。 当开启这个设置时,MyBatis会返回一个空实例。 请注意,它也适用于嵌套的结果集(如集合或关联)。 -->  
        <setting name="returnInstanceForEmptyRow" value="false"/>  
        <!-- 指定 MyBatis 增加到日志名称的前缀。 -->  
        <setting name="logPrefix" value="learn_mybatis_log_"/>  
        <!-- 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。 -->  
        <!--        <setting name="logImpl"-->        <!--                 value="SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING"/>-->  
        <!-- 指定 Mybatis 创建可延迟加载对象所用到的代理工具。 CGLIB | JAVASSIST-->        <setting name="proxyFactory" value="JAVASSIST"/>  
        <!-- 指定 VFS 的实现 -->  
        <!-- <setting name="vfsImpl" value="org.mybatis.example.YourselfVfsImpl"/> -->        <!-- 允许使用方法签名中的名称作为语句参数名称。 为了使用该特性,你的项目必须采用 Java 8 编译,并且加上 -parameters 选项。(新增于 3.4.1) -->  
        <setting name="useActualParamName" value="true"/>  
        <!-- 指定一个提供 Configuration 实例的类。 这个被返回的 Configuration 实例用来加载被反序列化对象的延迟加载属性值。 这个类必须包含一个签名为static Configuration getConfiguration() 的方法。(新增于 3.2.3) -->  
        <!-- <setting name="configurationFactory" value="org.mybatis.example.ConfigurationFactory"/> -->        <!-- 从SQL中删除多余的空格字符。请注意,这也会影响SQL中的文字字符串。 -->  
        <setting name="shrinkWhitespacesInSql" value="true"/>  
  
        <!-- 指定一个拥有 provider 方法的 sql provider 类 (新增于 3.5.6). 这个类适用于指定 sql provider 注解上的type(或 value) 属性(当这些属性在注解中被忽略时)。 (e.g. @SelectProvider) -->        <!-- <setting name="defaultSqlProviderType" value="true"/> -->  
        <!-- 为 'foreach' 标签的 'nullable' 属性指定默认值。 -->  
        <setting name="nullableOnForEach" value="false"/>  
  
        <!-- 当应用构造器自动映射时,参数名称被用来搜索要映射的列,而不再依赖列的顺序。-->  
        <setting name="argNameBasedConstructorAutoMapping" value="false"/>  
    </settings>  
  
  
    <typeAliases>  
        <!-- 配置别名信息 -->  
        <typeAlias alias="CustomerEntity" type="com.mingshashan.mybatis.learn.entity.CustomerEntity"/>  
        <typeAlias alias="OrderEntity" type="com.mingshashan.mybatis.learn.entity.OrderEntity"/>  
        <typeAlias alias="OrderItemEntity" type="com.mingshashan.mybatis.learn.entity.OrderItemEntity"/>  
        <typeAlias alias="ProductEntity" type="com.mingshashan.mybatis.learn.entity.ProductEntity"/>  
        <typeAlias alias="TagEntity" type="com.mingshashan.mybatis.learn.entity.TagEntity"/>  
        <typeAlias alias="CustomCache" type="com.mingshashan.mybatis.learn.mybatis.CustomCache"/>  
    </typeAliases>  
  
    <typeHandlers>  
        <typeHandler handler="com.mingshashan.mybatis.learn.mybatis.handler.AddressInfoHandler"/>  
    </typeHandlers>  
  
    <!-- 每次 MyBatis 创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成实例化工作。  
        默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认无参构造方法,        要么通过存在的参数映射来调用带有参数的构造方法。 如果想覆盖对象工厂的默认行为,可以通过创建自己的对象工厂来实现。 -->  
    <objectFactory type="com.mingshashan.mybatis.learn.mybatis.CustomObjectFactory"/>  
  
    <!-- MyBatis 提供在构造对象的时候,对于指定的对象进行特殊的加工,其配置方式如下 -->  
    <objectWrapperFactory type="com.mingshashan.mybatis.learn.mybatis.CustomMapWrapperFactory"/>  
  
    <reflectorFactory type="com.mingshashan.mybatis.learn.mybatis.CustomReflectorFactory"/>  
  
  
    <environments default="dev">  
        <environment id="dev">  
            <!-- 配置事务管理器的类型 -->  
            <transactionManager type="JDBC"/>  
            <!-- 配置数据源的类型,以及数据库连接的相关信息 -->  
            <dataSource type="POOLED">  
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>  
                <property name="url" value="jdbc:mysql://192.168.75.138:3306/dev?useSSL=false"/>  
                <property name="username" value="root"/>  
                <property name="password" value="000000"/>  
            </dataSource>  
        </environment>  
        <environment id="test">  
            <!-- 配置事务管理器的类型 -->  
            <transactionManager type="JDBC"/>  
            <!-- 如果environment id为test则使用JNDI -->  
            <dataSource type="JNDI">  
                <property name="data_source" value="jdbc/mybatis-jndi"/>  
                <property name="initial_context" value="java:/comp/env"/>  
            </dataSource>  
        </environment>  
    </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>  
  
    <!-- 配置映射配置文件的位置 -->  
    <mappers>  
        <mapper resource="mapper/CustomerMapper.xml"/>  
        <mapper resource="mapper/OrderItemMapper.xml"/>  
        <mapper resource="mapper/OrderMapper.xml"/>  
        <mapper resource="mapper/ProductMapper.xml"/>  
        <mapper resource="mapper/TagMapper.xml"/>  
    </mappers>  
</configuration>

下面是mapper配置,即xml映射文件的配置,我们只看其中的CustomerMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>  
<!DOCTYPE mapper  
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"  
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">  
<mapper namespace="com.mingshashan.mybatis.learn.dao.mapper.CustomerMapper">  
  
    <!--  
    cache- 配置本定命名空间的缓存。  
        type- cache实现类,默认为PERPETUAL,可以使用自定义的cache实现类(别名或完整类名皆可)  
        eviction- 回收算法,默认为LRU,可选的算法有:  
            LRU– 最近最少使用的:移除最长时间不被使用的对象。  
            FIFO– 先进先出:按对象进入缓存的顺序来移除它们。  
            SOFT– 软引用:移除基于垃圾回收器状态和软引用规则的对象。  
            WEAK– 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。  
        flushInterval- 刷新间隔,默认为1个小时,单位毫秒  
        size- 缓存大小,默认大小1024,单位为引用数  
        readOnly- 只读  
    -->  
    <cache type="com.mingshashan.mybatis.learn.mybatis.CustomCache"  
           size="2048" flushInterval="60000">  
        <property name="cacheFile" value="./cache.tmp"/>  
    </cache>  
  
    <!--  
    cache-ref–从其他命名空间引用缓存配置。  
        如果你不想定义自己的cache,可以使用cache-ref引用别的cache。因为每个cache都以namespace为id,所以cache-ref只需要配置一个namespace属性就可以了。需要注意的是,如果cache-ref和cache都配置了,以cache为准。  
    -->  
    <!-- <cache-ref namespace="com.someone.application.data.SomeMapper"/> -->  
  
    <resultMap id="CustomerMap" type="com.mingshashan.mybatis.learn.entity.CustomerEntity">  
        <id property="id" column="id"></id>  
        <result property="name" column="name"></result>  
        <result property="gender" column="gender"></result>  
        <result property="phone" column="phone"></result>  
        <result property="addressList" column="address_info"  
                typeHandler="com.mingshashan.mybatis.learn.mybatis.handler.AddressInfoHandler"  
                javaType="java.util.List"  
                jdbcType="VARCHAR"  
        ></result>  
        <result property="gmtCreated" column="gmt_created"></result>  
        <result property="gmtModified" column="gmt_modified"></result>  
        <result property="isValid" column="is_valid"></result>  
        <collection property="tagEntityList" ofType="TagEntity">  
            <id property="id" column="id"/>  
            <result property="name" column="name"/>  
        </collection>  
    </resultMap>  
  
  
    <sql id="customer_column">  
        id, name, gender, phone, address_info, gmt_created, gmt_modified  
    </sql>  
  
    <insert id="saveCustomer" parameterType="CustomerEntity">  
        INSERT INTO T_ARG_CUSTOMER(  
        <include refid="customer_column"></include>  
        )  
        values(#{id}, #{name}, #{gender}, #{phone}, #{addressList}, #{gmtCreated}, #{gmtModified})  
    </insert>  
  
    <update id="updateCustomer" parameterType="CustomerEntity">  
        update T_ARG_CUSTOMER set  
        name = #{name},  
        gender = #{gender},  
        phone = #{phone},  
        address_info = #{addressList},  
        gmt_modified = #{gmtModified}  
        from t_arg_customer  
        <where>  
            ID = #{id}  
        </where>  
    </update>  
    <update id="deleteCustomerById">  
        update T_ARG_CUSTOMER set is_valid = '0'  
        <where>  
            id = #{id}  
        </where>  
    </update>  
  
    <select id="findById" resultType="CustomerEntity">  
        select  
        <include refid="customer_column"></include>  
        from T_ARG_CUSTOMER  
        <where>  
            id = #{id}  
        </where>  
    </select>  
</mapper>

Mybatis的启动加载

整个过程的时序图大概如下 先是XMLConfigBuilder diagram-5542189421490136722.png 再是XMLMapperBuilder diagram-4007552790264809251.png 然后结合着代码来分析

1.SqlSessionFactoryBuilder

接下来看一下mybatis的启动加载过程,从mybatis的官方文档看mybatis的启动加载入口代码示例:

String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

可以知道,其入口就是new SqlSessionFactoryBuilder().build(inputStream)。接下来我们来分析一下其启动加载过程。 代码如下:

public class SqlSessionFactoryBuilder {
	// 方法入口
	public SqlSessionFactory build(Reader reader, String environment, Properties properties) {  
	  try {  
		// 构造XMLConfigBuilder,将解析逻辑交给XMLConfigBuilder去处理
	    XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);  
	    // 执行解析逻辑。解析完后构造DefaultSqlSessionFactory
	    return build(parser.parse());  
	  } catch (Exception e) {  
	    throw ExceptionFactory.wrapException("Error building SqlSession.", e);  
	  } finally {  
	    ErrorContext.instance().reset();  
	    try {  
	       if (reader != null) {  
	         reader.close();  
	       }  
	    } catch (IOException e) {  
	      // Intentionally ignore. Prefer previous error.  
	    }  
	  }  
	}
}

2.XMLConfigBuilder

parse方法会调用parseConfiguration方法解析mybatis的configuration配置

public class XMLConfigBuilder extends BaseBuilder {
	// 解析配置,即将mybatis-config.xml的xml配置转换为对应的mybatis模型配置
	private void parseConfiguration(XNode root) {  
	  try {  
		// properties标签
	    propertiesElement(root.evalNode("properties"));  
	    // settings标签
	    Properties settings = settingsAsProperties(root.evalNode("settings"));
	    // vfs配置  
	    loadCustomVfs(settings);
	    // 日志配置  
	    loadCustomLogImpl(settings);  
	    // 别名配置
	    typeAliasesElement(root.evalNode("typeAliases")); 
	    // 插件配置 
	    pluginElement(root.evalNode("plugins"));  
	    // 自定义的objectFactory配置
	    objectFactoryElement(root.evalNode("objectFactory"));  
	    // 自定义的objectWrapperFactory配置
	    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));  
	    // 自定义的reflectorFactory配置
	    reflectorFactoryElement(root.evalNode("reflectorFactory"));  
	    // 其他的settings配置
	    settingsElement(settings);  
	    // 在objectFactory and objectWrapperFactory初始化后,配置environments
	    environmentsElement(root.evalNode("environments"));  
	    // databaseIdProvider
	    databaseIdProviderElement(root.evalNode("databaseIdProvider"));  
	    // typeHandlers
	    typeHandlerElement(root.evalNode("typeHandlers"));  
	    // 所有的mappers
	    mapperElement(root.evalNode("mappers"));  
	  } catch (Exception e) {  
	    throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);  
	  }  
	}
}

3.XmlMapperBuilder

可以看到mapperElement会调用XmlMapperBuilder的parse方法去解析处理,其主要代码如下:

public class XMLMapperBuilder extends BaseBuilder {
	public void parse() {  
	//先从loadedResources判断是不是已经加载过
	  if (!configuration.isResourceLoaded(resource)) {  
		// 如果没有加载过,则去处理mapper标签下面的元素
	    configurationElement(parser.evalNode("/mapper"));  
	    configuration.addLoadedResource(resource);  
	    bindMapperForNamespace();  
	  }  
	  
	  parsePendingResultMaps(); // 解析待定ResultMaps 
	  parsePendingCacheRefs();  // 解析待定ChacheRefs
	  parsePendingStatements(); // 解析待定Statements
	}
}

	private void configurationElement(XNode context) {  
	  try {  
	    String namespace = context.getStringAttribute("namespace");  
	    if (namespace == null || namespace.isEmpty()) {  
	      throw new BuilderException("Mapper's namespace cannot be empty");  
	    }  
	    // 用到了MapperBuilderAssistant
	    builderAssistant.setCurrentNamespace(namespace);  
	    // 处理cache-ref
	    cacheRefElement(context.evalNode("cache-ref"));  
	    // 处理cache
	    cacheElement(context.evalNode("cache"));
	    // 处理parameterMap  
	    parameterMapElement(context.evalNodes("/mapper/parameterMap"));  
	    // 处理resultMap
	    resultMapElements(context.evalNodes("/mapper/resultMap"));  
	    // 处理sql
	    sqlElement(context.evalNodes("/mapper/sql"));  
	    // "select|insert|update|delete" 这些statement的处理
	    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);  
	  }  
}

关于上面的

	  parsePendingResultMaps(); // 解析待定ResultMaps 
	  parsePendingCacheRefs();  // 解析待定ChacheRefs
	  parsePendingStatements(); // 解析待定Statements

可以参考:could not find a parent resultmap with id XMLMapperBuilder.parse() -> 存在resultMap继承,父resultMap还未加载的,抛出Could not find a parent resultmap with id xx, 被上层XMLMapperBuilder.resultMapElement()方法捕获, 存储到 this.configuration.addIncompleteResultMap()即 configuration.IncompleteResultMap 列表内,属于不完全加载, 每一次parse循环会执行 this.parsePendingResultMaps() 检验IncompleteResultMap的父(extends) resultMap是否被加载进来,如果上一次parse加载了,这次就可以处理IncompleteResultMap进行完全加载. 由于每次都要循环检验,毕竟有消耗,因此为避免这种消耗,是否可以让父resultMap优先加载呢,这样后面就不会存在IncompleteResultMap , 需要把父mapper.xml放目录结构父层级 或者 以AxxMapper.xml开头排同层级之前(注意:这并不能解决后面checkDaoConfig报错问题)

4.代码调试过程

SqlSessionFactory的build方法为mybatis配置加载的入口 image.png 然后XMLConfigBuilder的parseConfiguration解析mybatis-config.xml的配置 接着调用XMLMapperBuilder的parse方法解析所有的mapper.xml文件 image.png 其parse方法执行如下: 之后调用XMLStatementBuilder的parseStatementNode方法 image.png XMLStatementBuilder会交给MapperBuilderAssistant的addMappedStatement方法由MappedStatement的builder模式构造出MappedStatement image.png 再将构造出的MappedStatement放到Configuration的mappedStatements内部map容器中 image.png

5.Mybatis配置的实例化对象Configuration

整个mybatis的配置会保存在org.apache.ibatis.session.Configuration对象(一个全局的实例) 而可以从SqlSessionFactory接口获取到Configuration,其关系图如下:

diagram-14250686795475816002.png DefaultSqlSessionFactory会持有Configuration对象

public class DefaultSqlSessionFactory implements SqlSessionFactory {  
  
  private final Configuration configuration;  
  
  public DefaultSqlSessionFactory(Configuration configuration) {  
    this.configuration = configuration;  
  }
}

Configuration的结构如下图所示: image.png

通过上面的配置信息,代码调试及代码执行流程,可以看到mybatis整个启动加载过程会解析mybatis配置里面的所有信息。

项目工程地址:learn-mybatis

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章