Mybatis源码分析


title: Mybatis源码分析
date: 2021-02-17 11:21:28
tags: mybatis
description: Mybatis源码学习

image

​ (整体架构图)

image

​ (源码包架构图)

1.配置文件解析过程

image

根据配置文件构建SqlSessionFactory

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

​ 我们首先会使用 MyBatis 提供的工具类 Resources 加载配置文件得到一个输入流。然后再通过 SqlSessionFactoryBuilder 对象的 build 方法构建 SqlSessionFactory对象。这里的 build 方法是我们分析配置文件解析过程的入口方法.

build方法创建了配置文件解析器

public SqlSessionFactory build(InputStream inputStream,String environment,Properties properties){
//创建文件解析器
XMLConfigBuilder praser = new XMLConfigBuilder(inputStream, environment, properties);
//调用parse方法解析配置文件,生成Configuration对象
return build(parser.prase());
}

配置文件通过XMLConfigBuilder进行解析,下面是XMLCnfigBuilder的prase方法:

public Configuration parse(){
...
//解析配置
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}

xpath表达式/configuration代表的是配置文件中的结点,这里通过xpath选中这个结点,并传递给parseConfiguration方法:

private void parseConfiguration(XNode root){
	//配置properties配置
	propertiesElement(root.evalNode("properties"));
	//解析settings配置,并将其转换为Properties对象
	Properties settings = settingAsProperties(root.evalNode("settings"));
	//加载vfs
	loadCustomVfs(settings);
	//解析typeAlases配置
	typeAliasesElement(root.evalNode("typeAliases"));
	//解析plugins配置
	pluginElement(root.evalNode("plugins"));
	...
}
1.1解析properties节点

properties节点配置

<properties resource="jdbc.properties">
    <property name="jdbc.username" value="coolblog"/>
    <property name="hello" value="world"/>
</properties>

节点解析主要包含三个步骤,一是解析节点的子节点,并将解析结果设置到 Properties 对象中。二是从文件系统或通过网络读取属性配置,这取决于节点的 resource 和 url 是否为空。最后一步则是将包含属性信息Properties 对象设置到XPathParserConfiguration 中。

1.2解析settings节点

配置节点

<settings>
	<setting name="cacheEnabled" value="true"/>
    <setting name="lazyLoadingEnabled" value="true"/>
    <setting name="autoMppingBehavior" value="PARTAL"/>
</settings>

解析过程:

private Properties settingsAsProperties(XNode context){
	//获取settings子节点中的内容
    Properties props = context.getChildrenAsProperties();
    //创建Configuration类的“元信息”对象
    MetaClass metaConfig = MetaClass.forClass(Configuration.class,localReflectorFactory);
    for(Object key : props.keySet()){
		//监测Configuration中是否存在相关属性
    }
}
  1. 解析 settings 子节点的内容,并将解析结果转成 Properties 对象
  2. 为 Configuration 创建元信息对象
  3. 通过 MetaClass 检测 Configuration 中是否存在某个属性的 setter 方法, 不存在则抛异常
  4. 若通过 MetaClass 的检测,则返回 Properties 对象,方法逻辑结束
1.3设置settings内容到Configuration中

settings节点内容解析出来后,可以将它存放到Configuration对象中,代码主要通过调用Configuration的setter方法

1.4解析typeAliases节点

配置包名的方式,让Mybatis扫描包中的类型,并根据类型得到相应的别名,可配合Alias注解使用,即通过注解为某个类配置别名

<typeAliases>
    <package name="xyz.coolblog.chapter2.model1"/>
    <package name="xyz.coolblog.chapter2.model2"/>
</typeAliases>

手动的方式,明确为某个类型配置别名

<typeAliases>
	<typeAlias alias="article" type="xyz.coolblog.chapter2.model.Article"/>
	<typeAlias alias="author" type="xyz.coolblog.chapter2.model.Author"/>
</typeAliases>
  • 从节点中解析并注册别名

    type属性是必须要配置的,而alias属性则不是必修的。如果使用者未配置alias属性,则需要mybatis自行为目标类型生成别名(使用类名的小写形式作为别名)。若别名为空,注册别名的任务交给registerAlias(Class<?>)方法处理,若不为空,则由registerAlias(String, Class<?>)进行别名注册

  • 从指定的包中解析并注册别名

    查找指定包下的所有类,遍历查找到的类型集合,为每个类型注册别名

    1. 通过 VFS(虚拟文件系统)获取指定包下的所有文件的路径名,
    2. 比如 xyz/coolblog/model/Article.class
    3. 筛选以.class 结尾的文件名
    4. 将路径名转成全限定的类名,通过类加载器加载类名
    5. 对类型进行匹配,若符合匹配规则,则将其放入内部集合中

简单说就是创建了一个Map<string,Class<?>>,解析mybatis的配置文件,将alias元素的值作为Map的key,通过反射机制获得的type元素对应的类名的类作为Map的value值,在真正使用时通过alias别名来获取真正的类。

1.5解析plugins节点

实现一个插件需要比简单,首先需要让插件类实现 Interceptor接口。然后在插件类上添加@Intercepts 和@Signature 注解,用于指定想要拦截的目标方法

<plugins>
	<plugin interceptor="xyz.coolblog.mybatis.ExamplePlugin">
        <property name="key" value="value"/>
    </plugin>
</plugins>

插件的解析过程:获取配置,然后再解析拦截器类型,并实例化拦截器,最后向拦截器中设置属性,并将拦截器添加到Configuration中

1.6解析environments节点

mybatis中,事务管理器和数据源配置在environments节点中

<environments default="development">
	<environment id="development">
    	<transactionManager type="JDBC"/>
        <dataSource type="POOLED">
        	<property name="driver" value="${jdbc.driver}"/>
			<property name="url" value="${jdbc.url}"/>
             <property name="username" value="${jdbc.username}"/>
             <property name="password" value="${jdbc.password}"/>
        </dataSource>
    </environment>
</environments>

解析过程:获取environments的default属性,environment节点的id要与父节点environments的属性default内容一致,解析transactionManager节点,解析dataSource节点,创建DataSource对象,创建Environment对象并设置到configuration中。

1.7解析typeHandlers节点

在向数据库存储或读取数据时,我们需要将数据库字段类型和Java类型进行转换。下面是类型处理器的配置方法:

<!--自动扫描-->
<!--自动扫描注册类型处理器时,应使用@MappedTypes和@MappedJdbcTypes注解配置javaType和jdbcType>
<typeHandlers>
	<package name="xyz.coolblog.handlers"/>
</typeHandlers>

<!--手动配置-->
<typeHandlers>
	<typeHandler jdbcType="TINYINT"
           javaType="xyz.coolblog.constant.ArticleTypeEnum"
           handler="xyz.coolblog.mybatis.ArticleTypeHandler"/>
</typeHandlers>

解析xml并调用不同的类型处理器注册方法

image-20210219153412102

  • 当javaTypeClass != null&&jdbcType != null时,即明确配置了javaType和jdbcType属性的值,调用register(Class,jdbcType,Class)进行注册,即把类型和处理器进行映射

    private void register(Type javaType, JdbcType jdbcType,TypeHandler<?> handler){
        if(javaType != null){
    		//jdbcType到TypeHandler的映射
            Map<JdbcType, TypeHandler<?>> map = TYPE_HANDLER_MAP.get(javaType);
            if(map==null||map == NULL_TYPE_HANDLER_MAP){
                map = new HashMap<JdbcType, TypeHandler<?>>();
                //存储javaType到Map<JdbcType, TypeHandler>的映射
                TYPE_HANDLER_MAP.put(javaType,map);
            }
            map.put(jdbcType,handler);
        }
        //存储所有的TypeHandler
        ALL_TYPE_HANDLERS_MAP.put(handler.getClass(),handler);
    }
    
  • 当javaType != NULL&&JdbcType == null时,即仅设置了javaType的值,调用register(Class<?> javaTypeClass, Class<? > typeHandlerClass),主要做的是尝试从注解中获取JdbcType的值

  • 当javaType == NULL&&JdbcType != null时,即仅设置了javaType的值,调用register(Class<? > typeHandlerClass),主要做的是尝试解析javaType的值

2.映射文件解析过程

Mybatis会在解析配置文件的过程中对映射文件进行解析,解析逻辑封装在mapperElement方法中

//XMLConfigBuilder
private void mapperElement(XNode parent) throws Exception{
    if(parent!=null){
        for(XNode child:parent.getChildren()){
            if("package".equals(child.getName())){
                //获取<package>节点中的name属性
                String mapperPackage = child.getStringAttribute("name");
                //从注定包中查找mapper接口,并根据mapper接口解析映射配置
                configuration.addMappers(mapperPackage);
            }else{
                //获取resource/url/class等属性
                //resource不为空,则从指定路径加载配置
                ...
            }
        }
    }
}

上面代码的主要逻辑是遍历 mappers 的子节点,并根据节点属性值判断通过何种方式加载映射文件或映射信息。这里把配置在注解中的内容称为映射信息,以 XML 为载体的配置称为映射文件。在 MyBatis 中,共有四种加载映射文件或映射信息的方式。第一种是从文件系统中加载映射文件;第二种是通过 URL 的方式加载映射文件;第三种是通过 mapper 接口加载映射信息,映射信息可以配置在注解中,也可以配置在映射文件中。最后一种是通过包扫描的方式获取到某个包下的所有类,并使用第三种方式为每个类解析映射信息。

下面分析映射文件的解析过程,先看映射文件解析入口:

//XMLMapperBuilder
public void parse(){
    //监测映射文件是否已被解析过
    if(!configuration.isResourceLoaded(resource)){
        //解析mapper节点
        configurationElement(parser.evalNode("/mapper"));
        //添加资源路径到“已解析资源集合"中
        configuration.addLoadedResource(resource);
        //通过命名空间绑定Mapper接口
        bindMapperForNamespace();
        
        //处理未完成解析的节点
        parsePendingResultMaps();
        parsePendingCacheRefs();
        parsePendingStatements();
    }
}

映射文件解析入口逻辑包含三个核心操作,如下:

  1. 解析 mapper 节点
  2. 通过命名空间绑定 Mapper 接口
  3. 处理未完成解析的节点
2.1解析映射文件
<mapper namespace="xyz.coolblog.dao.AuthorDao">
	<cache/>
    <resultMap id="authorResult" type="Author">
    	<id property="id" colunn="id"/>
        <result property="name" column="name"/>
        <!--...-->
    </resultMap>
    
    <sql id="table">
		author
    </sql>
    
    <select id="findOne" resultMap="authorResult">
		SELECT
        	id,name,age,sex,email
       	 FROM
        	<include refid="table"/>
         WHERE
        	id=#{id}
    </select>
</mapper>

以上配置中每种节点的解析逻辑都封装在了相应的方法中 ,这 些方法由 XMLMapperBuilder 类的configurationElement 方法统一调用。该方法的逻辑如下:

private void configurationElement(XNode context){
    try{
		//获取mapper命名空间
        String namespace = context.getStringAttribute("namespace");
        //异常处理
    }
    //设置命名空间到builderAssistant
    builderAssistant.setCurrentNamespace(namespace);
    //解析cache-ref节点
    cacheRefElement(context.evalNode("cache-ref"));
    //解析cache节点
    cacheElement(context.evalNode("chche"));
    //解析resultMap节点
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    //解析sql节点 
    sqlElement(context.evalNodes("/mapper/sql"));
    //解析select、delete等节点
    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
}
2.2解析cache节点

mybatis提供两级缓存,一级缓存是SqlSession级别,默认为开启状态;二级缓存配置在映射文件中,使用者需要显示配置才能开启

<cache 
  eviction="FIFO" 
  flushInterval="60000" 
  size="512" 
  readOnly="true"/>

根据上面的配置创建出的缓存有以下特点:

  1. 按先进先出的策略淘汰缓存项
  2. 缓存的容量为 512 个对象引用
  3. 缓存每隔 60 秒刷新一次
  4. 缓存返回的对象是写安全的,即在外部修改对象不会影响到缓存内部存储对象

缓存配置解析:

2.3解析cache-ref节点
2.4解析resultMap节点
<!-- mybatis-config.xml 中 -->
<!--User类-->
<typeAlias type="com.someapp.model.User" alias="User"/>

<!-- SQL 映射 XML 中 -->
<select id="selectUsers" resultType="User">
  select id, username, hashedPassword
  from some_table
  where id = #{id}
</select>

<!--显示配置resultMap-->
<resultMap id="userResultMap" type="User">
  <id property="id" column="user_id" />
  <result property="username" column="user_name"/>
  <result property="password" column="hashed_password"/>
</resultMap>
  1. 获取resultMap节点的各种属性

    private ResultMap resultMapElement(XNode resultMapNode,  
    List<ResultMapping> additionalResultMappings) throws Exception { 
    ErrorContext.instance().activity("processing " +  
     resultMapNode.getValueBasedIdentifier()); 
     
        // 获取 id 和 type 属性 
    String id = resultMapNode.getStringAttribute("id",  
     resultMapNode.getValueBasedIdentifier()); 
        String type = resultMapNode.getStringAttribute("type", 
            resultMapNode.getStringAttribute("ofType", 
                resultMapNode.getStringAttribute("resultType", 
                    resultMapNode.getStringAttribute("javaType")))); 
        // 获取 extends 和 autoMapping 
        String extend = resultMapNode.getStringAttribute("extends"); 
        Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping"); 
     
        // 解析 type 属性对应的类型 
        Class<?> typeClass = resolveClass(type); 
        Discriminator discriminator = null; 
        List<ResultMapping> resultMappings = new ArrayList<ResultMapping>(); 
        resultMappings.addAll(additionalResultMappings);
    
  2. 遍历resultMap的子节点,并根据子节点名称执行相应的解析逻辑

    // 获取并遍历 <resultMap> 的子节点列表
    List<XNode> resultChildren = resultMapNode.getChildren(); 
        for (XNode resultChild : resultChildren) { 
            if ("constructor".equals(resultChild.getName())) { 
                // 解析 constructor 节点,并生成相应的 ResultMapping 
     
              processConstructorElement(resultChild, typeClass, resultMappings); 
            } else if ("discriminator".equals(resultChild.getName())) { 
                // 解析 discriminator 鉴别器节点 
                discriminator = processDiscriminatorElement( 
      resultChild, typeClass, resultMappings); 
            } else { 
                List<ResultFlag> flags = new ArrayList<ResultFlag>(); 
                if ("id".equals(resultChild.getName())) { 
                    // 添加 ID 到 flags 集合中 
                    flags.add(ResultFlag.ID); 
                } 
                // 解析 id 和 property 节点,并生成相应的 ResultMapping 
                resultMappings.add(buildResultMappingFromContext( 
     resultChild, typeClass, flags)); 
            } 
        } 
    ResultMapResolver resultMapResolver = new ResultMapResolver( 
     builderAssistant, id, typeClass, extend, discriminator,  
     resultMappings, autoMapping);
    
  3. 构建 ResultMap 对象

  4. 若构建过程中发生异常,则将 resultMapResolver 添加到
    incompleteResultMaps 集合中

    try { 
            // 根据前面获取到的信息构建 ResultMap 对象 
            return resultMapResolver.resolve(); 
        } catch (IncompleteElementException e) { 
            /* 
             * 如果发生 IncompleteElementException 异常, 
             * 这里将 resultMapResolver 添加到 incompleteResultMaps 集合中 
             */  
            configuration.addIncompleteResultMap(resultMapResolver); 
            throw e; 
        } 
    }
    
    • 解析id和result节点

    • resultMap 属性的解析过程
      要相对复杂一些。该属性存在于和节点

    • <!--通过resultMap属性引用其他的resultMap节点-->
      <resultMap id="articleResult" type="Article"> 
          <id property="id" column="id"/> 
          <result property="title" column="article_title"/> 
      <association property="article_author" column="article_author_id"  
       resultMap="authorResult"/> 
      </resultMap> 
       
      <resultMap id="authorResult" type="Author"> 
          <id property="id" column="author_id"/> 
          <result property="name" column="author_name"/> 
      </resultMap>
      
    • <!--采取resultMap嵌套的方式进行配置-->
      <resultMap id="articleResult" type="Article"> 
          <id property="id" column="id"/> 
          <result property="title" column="article_title"/> 
         <association property="article_author" javaType="Author"> 
              <id property="id" column="author_id"/> 
              <result property="name" column="author_name"/> 
          </association> 
      </resultMap>
      
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值