MyBatis是纸老虎吗?(五)

最近看到这样一则消息《全球首位AI工程师诞生,“码农”未来会消失?》,文章提到百度董事长兼首席执行官李彦宏认为未来将不会存在“程序员”这种职业。行业大佬的这种说法,让我异常恐慌。难道程序员就这样被淘汰了?AI真的要打败创造它的造物主吗?在这个急速发展的社会中,从事程序员工作的我们究竟该怎么办呢?或许《MyBatis是纸老虎吗?》系列文章会为我们带来一些不一样的答案。

《MyBatis是纸老虎吗?(四)》这篇文章中我们一起学习了MyBatis配置文件中的plugins元素,梳理了该元素的解析过程。通过这篇文章我们知道什么是MyBatis拦截器,了解了该控件基本用法,学会了自定义该控件的方法。今天我将继续学习MyBatis框架。我希望通过这篇文章捋清MyBatis配置文件中的mappers元素。

1 mappers元素的定义

大家都知道使用MyBatis框架时,需要定义一个配置文件(在这个系列文章中我们也一直强调这个文件),而mappers元素就是定义在这个文件中的。先来看下面这样一段代码:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <mappers>
        <mapper resource="user.xml"/>
    </mappers>
</configuration>

在这段代码中,我们通过mappers元素将自定义的user.xml文件引入到MyBatis框架中,由其对这个文件进行管理。那这个文件是怎么被MyBatis框架解析和管理的呢?

2 mappers元素的解析

《MyBatis是纸老虎吗?(三)》《MyBatis是纸老虎吗?(四)》这两篇文章中我们着重介绍了MyBatis框架解析MyBatis配置文件及相关元素(plugins)的流程。本小节将继续前一篇文章的思路,介绍MyBatis配置文件中mappers元素的解析流程。这次我们不再啰嗦直进入主题:进入XMLConfigBuilder类【这个类的主要作用是解析MyBatis配置文件】的parse()方法中,然后继续进入该类的parseConfiguration()方法,接着重点关注该方法中这样一行代码mappersElement(root.evalNode("mappers")),下面一起看一下mappersElement()方法的源码(注意该方法会接收一个XNode元素,root.evalNode("mappers")这句的主要作用是解析配置文件中的mappers元素):

private void mappersElement(XNode context) throws Exception {
  if (context == null) {
    return;
  }
  for (XNode child : context.getChildren()) {
    if ("package".equals(child.getName())) {
      String mapperPackage = child.getStringAttribute("name");
      configuration.addMappers(mapperPackage);
    } else {
      String resource = child.getStringAttribute("resource");
      String url = child.getStringAttribute("url");
      String mapperClass = child.getStringAttribute("class");
      if (resource != null && url == null && mapperClass == null) {
        ErrorContext.instance().resource(resource);
        try (InputStream inputStream = Resources.getResourceAsStream(resource)) {
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource,
              configuration.getSqlFragments());
          mapperParser.parse();
        }
      } else if (resource == null && url != null && mapperClass == null) {
        ErrorContext.instance().resource(url);
        try (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.");
      }
    }
  }
}

从源码不难看出mappers的子元素有:package(通过name属性指定MyBatis框架要扫描包路径)、mapper(通过resource属性指定自定义sql文件路径、通过url属性指定、通过class属性指定Dao接口文件路径。注意:mapper标签中只能存在这三个属性中的一个,不能同时存在多个。如果配置文件配置合理,接下来就开始解析流程。这里我们以mapper+resource的形式来梳理,先看源码中的一个小技巧,如下所示:

try(InputStream inputStream = Resources.getResourceAsStream(resource)) {

    ……

}

之前我们写流代码时,一般都用try{}catch{}finally{}格式,在finally中对流进行关闭,但是这种写法我们不需要去关注流关闭的操作,因为InputStream实现了Closeable接口,在这种写法中java会自动关闭InputStream对象

下面就让我们深入研究一下try分支。这段代码分支中有一个XMLMapperBuilder类,通过名字可以判断出该类主要用于解析MyBatis中的mapper文件,即sql定义文件。这段代码的详情如下所示:

XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource,
    configuration.getSqlFragments());
mapperParser.parse();

创建XMLMapperBuilder对象时,会接收三个参数,它们分别为:mapper文件流、Configuration对象、mapper文件路径、Map<String, XNode>对象(位于Configuration对象中,实际名称为sqlFragments)。接着会调用XMLMapperBuilder对象中的parse方法开始解析操作,先来看一下这个方法(XMLMapperBuilder#parse())的源码:

public void parse() {
  if (!configuration.isResourceLoaded(resource)) {
    configurationElement(parser.evalNode("/mapper"));
    configuration.addLoadedResource(resource);
    bindMapperForNamespace();
  }
  parsePendingResultMaps();
  parsePendingCacheRefs();
  parsePendingStatements();
}

这个方法首先会调用Configuration中的isResourceLoaded()方法判断当前的mapper文件是否被加载过,该方法的源码如下所示:

public boolean isResourceLoaded(String resource) {
  return loadedResources.contains(resource);
}

该源码中的loadedResources变量的定义位于Configuration类中,其源码为:Set<String> loadedResources = new HashSet<>()。由于调用时,该属性中尚未有数据,所以该方法会返回false。回到XMLMapperBuilder的parse()方法中,由于调用返回了false,所以这个方法会走进if分支中,然后调用XMMapperBuilder中的configurationElement()方法解析mapper文件,接着调用Configuration中的addLoadedResource()方法将已经解析的resource添加到Configuration中的loadedResources变量中,以防止重复解析。接着在调用XMMapperBuilder类中的bindMapperForNamespace()方法。下面让我们先来看一下XMMapperBuilder中的configurationElement()方法的源码:

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");
    }
    // 
    builderAssistant.setCurrentNamespace(namespace);
    cacheRefElement(context.evalNode("cache-ref"));
    cacheElement(context.evalNode("cache"));
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    sqlElement(context.evalNodes("/mapper/sql"));
    // 注意context.evalNodes()方法会解析出满足参数条件的xml节点数据,比如这里的select、insert、update及delete
  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);
  }
}

这个方法的处理逻辑和MyBatis配置文件的处理逻辑是一样的,就是逐个元素解析。从这个源码不难发现mapper文件中的重要数据有

  1. mapper元素上的namespace属性(该属性值一般是相应Dao文件的包名+接口,注意这个属性值不能为空)
  2. mapper元素中可以定义的子元素有:cache-ref、cache、parameterMap、resultMap、sql、select、insert、update、delete。其中工作中最常用的是resultMap、sql、select、insert、update及delete。resultMap用于定义sql查询结果和目标对象属性之间的映射关系;sql用于定义一些公共的数据,比如select语句中的查询字段和条件语句等;select、insert、update及delete则主要用于定义相应的sql语句。个人理解parameterMap用于定义查询参数和字段之间的映射关系,而cache和cache-ref则用于定义与缓存相关的信息

下面先来看一个mapper文件,这个文件中的基本符合上面根据代码梳理出来的信息,具体如下所示:

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="org.com.chinasofti.springtransaction.UserDao">

    <!--<cache></cache>
    <cache-ref namespace=""/>
    <parameterMap id="" type=""></parameterMap>-->

    <!-- table 实体映射 -->
    <resultMap id="userDomain" type="user">
        <id property="id" column="id"/>
        <result property="loginName" column="login_name"/>
        <result property="nickName" column="nick_name"/>
        <result property="userName" column="user_name"/>
        <result property="cellphone" column="cellphone"/>
        <result property="gender" column="gender"/>
        <result property="birthday" column="birthday"/>
        <result property="chIdCard" column="ch_id_card"/>
        <result property="email" column="email"/>
        <result property="socialMeda" column="social_meda"/>
        <result property="socialtype" column="social_type"/>
        <result property="pass" column="pass"/>
        <result property="modiTimes" column="modi_times"/>
        <result property="modiDate" column="modi_date"/>
        <result property="cardNo" column="card_no"/>
        <result property="integral" column="integral"/>
        <result property="rMoney" column="r_money"/>
        <result property="gMoney" column="g_money"/>
        <result property="stat" column="stat"/>
        <result property="insTime" column="ins_time"/>
        <result property="uptTime" column="upt_time"/>
        <result property="rmk1" column="rmk1"/>
        <result property="rmk2" column="rmk2"/>
        <result property="rmk3" column="rmk3"/>
        <result property="rmk4" column="rmk4"/>
    </resultMap>

    <!-- dto 实体映射 -->
    <resultMap id="userDtoDomain" type="userDto">
        <id property="id" column="id"/>
        <result property="nickName" column="nick_name"/>
        <result property="userName" column="user_name"/>
        <result property="cellphone" column="cellphone"/>
        <result property="gender" column="gender"/>
        <result property="birthday" column="birthday"/>
        <result property="chIdCard" column="ch_id_card"/>
        <result property="email" column="email"/>
        <result property="socialMeda" column="social_meda"/>
        <result property="socialtype" column="social_type"/>
        <result property="cardNo" column="card_no"/>
        <result property="integral" column="integral"/>
        <result property="rMoney" column="r_money"/>
        <result property="gMoney" column="g_money"/>
        <result property="insTime" column="ins_time"/>
    </resultMap>

    <!-- 查询字段 -->
    <sql id="userColumn">
        `id`,
        `login_name`,
        `nick_name`,
        `user_name`,
        `cellphone`,
        `gender`,
        `birthday`,
        `ch_id_card`,
        `email`,
        `social_meda`,
        `social_type`,
        `pass`,
        `modi_times`,
        `modi_date`,
        `card_no`,
        `integral`,
        `r_money`,
        `g_money`,
        `stat`,
        `ins_time`,
        `upt_time`,
        `rmk1`,
        `rmk2`,
        `rmk3`,
        `rmk4`
    </sql>

    <!-- 查询条件,根据主键查询、昵称模糊查询、姓名模糊查询、性别查询、电话模糊查询及他们之间的组合查询 -->
    <sql id="queryConditon">
        <where>
            <if test="id != null">AND `id`=#{id}</if>
            <if test="nickName != null and nickName != ''.toString()">AND `nick_name` LIKE '%${nickName}%'</if>
            <if test="userName != null and userName != ''.toString()">AND `user_name` LIKE '%${userName}%'</if>
            <if test="gender != null and gender != ''.toString()">AND `gender` = '${gender}'</if>
            <if test="cellphone != null and cellphone != ''.toString()">AND `cellphone` LIKE '%${cellphone}%'</if>
        </where>
    </sql>

    <!-- =============================================================================================================================================================== -->
    <!-- 删除数据,依据主键进行删除 -->
    <delete id="deleteById" parameterType="long">
        DELETE FROM `tbl_user` WHERE `id` = #{id}
    </delete>

    <!-- 删除数据,依据 id 集合批量删除 -->
    <delete id="deleteByIds" parameterType="userDto">
        DELETE FROM `tbl_user` WHERE `id` IN <foreach collection="serialNumber" open="(" close=")" item="item" separator=",">#{item}</foreach>
    </delete>
    <!-- =============================================================================================================================================================== -->

    <!-- =============================================================================================================================================================== -->
    <!-- 新增数据,依据非空字段进行新增 -->
    <insert id="insertBySelect" parameterType="user" useGeneratedKeys="true">
        INSERT INTO `tbl_user`
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="id != null">`id`,</if>
            <if test="loginName != null and loginName != ''.toString()">`login_name`,</if>
            <if test="nickName != null and nickName != ''.toString()">`nick_name`,</if>
            <if test="userName != null and userName != ''.toString()">`user_name`,</if>
            <if test="cellphone != null and cellphone != ''.toString()">`cellphone`,</if>
            <if test="gender != null">`gender`,</if>
            <if test="birthday != null">`birthday`,</if>
            <if test="chIdCard != null and chIdCard != ''.toString()">`ch_id_card`,</if>
            <if test="email != null and email != ''.toString()">`email`,</if>
            <if test="socialMeda != null and socialMeda != ''.toString()">`social_meda`,</if>
            <if test="socialtype != null">`social_type`,</if>
            <if test="pass != null and pass != ''.toString()">`pass`,</if>
            <if test="modiTimes != null">`modi_times`,</if>
            <if test="modiDate != null">`modi_date`,</if>
            <if test="cardNo != null">`card_no`,</if>
            <if test="integral != null">`integral`,</if>
            <if test="rMoney != null">`r_money`,</if>
            <if test="gMoney != null">`g_money`,</if>
            <if test="stat != null">`stat`,</if>
            <if test="insTime != null">`ins_time`,</if>
            <if test="uptTime != null">`upt_time`,</if>
            <if test="rmk1 != null and rmk1 != ''.toString()">`rmk1`,</if>
            <if test="rmk2 != null and rmk2 != ''.toString()">`rmk2`,</if>
            <if test="rmk3 != null and rmk3 != ''.toString()">`rmk3`,</if>
            <if test="rmk4 != null and rmk4 != ''.toString()">`rmk4`,</if>
        </trim>
        <trim prefix="VALUES (" suffix=")" suffixOverrides=",">
            <if test="id != null">#{id},</if>
            <if test="loginName != null and loginName != ''.toString()">#{loginName},</if>
            <if test="nickName != null and nickName != ''.toString()">#{nickName},</if>
            <if test="userName != null and userName != ''.toString()">#{userName},</if>
            <if test="cellphone != null and cellphone != ''.toString()">#{cellphone},</if>
            <if test="gender != null">#{gender},</if>
            <if test="birthday != null">#{birthday},</if>
            <if test="chIdCard != null and chIdCard != ''.toString()">#{chIdCard},</if>
            <if test="email != null and email != ''.toString()">#{email},</if>
            <if test="socialMeda != null and socialMeda != ''.toString()">#{socialMeda},</if>
            <if test="socialtype != null">#{socialtype},</if>
            <if test="pass != null and pass != ''.toString()">#{pass},</if>
            <if test="modiTimes != null">#{modiTimes},</if>
            <if test="modiDate != null">#{modiDate},</if>
            <if test="cardNo != null and cardNo != ''.toString()">#{cardNo},</if>
            <if test="integral != null">#{integral},</if>
            <if test="rMoney != null">#{rMoney},</if>
            <if test="gMoney != null">#{gMoney},</if>
            <if test="stat != null">#{stat},</if>
            <if test="insTime != null">#{insTime},</if>
            <if test="uptTime != null">#{uptTime},</if>
            <if test="rmk1 != null and rmk1 != ''.toString()">#{rmk1},</if>
            <if test="rmk2 != null and rmk2 != ''.toString()">#{rmk2},</if>
            <if test="rmk3 != null and rmk3 != ''.toString()">#{rmk3},</if>
            <if test="rmk4 != null and rmk4 != ''.toString()">#{rmk4},</if>
        </trim>
    </insert>
    <!-- =============================================================================================================================================================== -->

    <!-- =============================================================================================================================================================== -->
    <!-- 修改数据,依据非空字段进行修改 -->
    <update id="modifySelectById" parameterType="user">
        UPDATE `tbl_user`
        <set>
            <if test="loginName != null and loginName != ''.toString()">`login_name` = #{loginName},</if>
            <if test="nickName != null and nickName != ''.toString()">`nick_name` = #{nickName},</if>
            <if test="userName != null and userName != ''.toString()">`user_name` = #{userName},</if>
            <if test="cellphone != null and cellphone != ''.toString()">`cellphone` = #{cellphone},</if>
            <if test="gender != null">`gender` = #{gender},</if>
            <if test="birthday != null">`birthday` = #{birthday},</if>
            <if test="chIdCard != null and chIdCard != ''.toString()">`ch_id_card` = #{chIdCard},</if>
            <if test="email != null and email != ''.toString()">`email` = #{email},</if>
            <if test="socialMeda != null and socialMeda != ''.toString()">`social_meda` = #{socialMeda},</if>
            <if test="socialtype != null">`social_type` = #{socialtype},</if>
            <if test="pass != null and pass != ''.toString()">`pass` = #{pass},</if>
            <if test="modiTimes != null">`modi_times` = #{modiTimes},</if>
            <if test="modiDate != null">`modi_date` = #{modiDate},</if>
            <if test="cardNo != null and cardNo != ''.toString()">`card_no` = #{cardNo},</if>
            <if test="integral != null">`integral` = #{integral},</if>
            <if test="rMoney != null">`r_money` = #{rMoney},</if>
            <if test="gMoney != null">`g_money` = #{gMoney},</if>
            <if test="stat != null">`stat` = #{stat},</if>
            <if test="insTime != null">`ins_time` = #{insTime},</if>
            <if test="uptTime != null">`upt_time` = #{uptTime},</if>
            <if test="rmk1 != null and rmk1 != ''.toString()">`rmk1` = #{rmk1},</if>
            <if test="rmk2 != null and rmk2 != ''.toString()">`rmk2` = #{rmk2},</if>
            <if test="rmk3 != null and rmk3 != ''.toString()">`rmk3` = #{rmk3},</if>
            <if test="rmk4 != null and rmk4 != ''.toString()">`rmk4` = #{rmk4},</if>
        </set>
        WHERE `id` = #{id}
    </update>
    <!-- =============================================================================================================================================================== -->

    <!-- =============================================================================================================================================================== -->
    <!-- 依据主键进行查询 -->
    <select id="queryById" parameterType="long" resultMap="userDtoDomain">
        SELECT
        <include refid="userColumn"/>
        FROM `tbl_user` WHERE `id` = #{id}
    </select>

    <!-- 依据指定条件进行查询 -->
    <select id="queryByCondition" parameterType="userDto" resultMap="userDtoDomain">
        SELECT
        <include refid="userColumn"/>
        FROM `tbl_user` <include refid="queryConditon"></include>
        <!-- <foreach collection="sorts" open="ORDER BY" close="" item="item" separator=",">${item.sort} ${item.order}</foreach>LIMIT #{start}, #{rows} -->
    </select>

    <!-- 依据指定条件进行查询 -->
    <select id="queryCountByCondition" parameterType="userDto" resultType="long">
        SELECT COUNT(*) FROM `tbl_user` <include refid="queryConditon"></include>
    </select>
    <!-- =============================================================================================================================================================== -->

</mapper>

下面就一起来看一下select、insert、update及delete语句的解析逻辑,首先看一下这个解析过程中涉及到的一些方法的源码:

private void buildStatementFromContext(List<XNode> list) {
  // 注意这里接到的是一个XNode对象组成的集合,由于MyBatis配置文件中未指定databaseId,所以这里不会执行if分支中的代码
  if (configuration.getDatabaseId() != null) {
    buildStatementFromContext(list, configuration.getDatabaseId());
  }
  buildStatementFromContext(list, null);
}

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
  for (XNode context : list) {
    // 注意下面这段代码的主要目的是将mapper文件中定义的sql语句解析为Statement
    final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context,
        requiredDatabaseId);
    try {
      statementParser.parseStatementNode();
    } catch (IncompleteElementException e) {
      configuration.addIncompleteStatement(statementParser);
    }
  }
}

从源码不难看出buildStatementFromContext()方法的主要作用就是循环遍历list参数,然后创建XMLStatementBuilder对象,接着调用该对象上的parseStatementNode()方法解析mapper文件中定义的sql语句。下图展示的是buildStatementFromContext()方法运行时状态图,从图中可以看出list参数就是在mapper文件中定义的7个sql语句(具体可以参看上面的mapper文件案例),详细信息如下图所示:

注意这段代码中有一个XMLStatementBuilder类,这个类的主要作用是解析在mapper文件中定义的sql语句,比如select、insert等。下面看一下XMLStatementBuilder类中的parseStatementNode()方法的源码:

public void parseStatementNode() {
  String id = context.getStringAttribute("id");
  String databaseId = context.getStringAttribute("databaseId");

  if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
    return;
  }

  String nodeName = context.getNode().getNodeName();
  SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
  boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
  boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
  boolean useCache = context.getBooleanAttribute("useCache", isSelect);
  boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

  // Include Fragments before parsing
  XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
  includeParser.applyIncludes(context.getNode());

  String parameterType = context.getStringAttribute("parameterType");
  Class<?> parameterTypeClass = resolveClass(parameterType);

  String lang = context.getStringAttribute("lang");
  LanguageDriver langDriver = getLanguageDriver(lang);

  // Parse selectKey after includes and remove them.
  processSelectKeyNodes(id, parameterTypeClass, langDriver);

  // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
  KeyGenerator keyGenerator;
  String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
  keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
  if (configuration.hasKeyGenerator(keyStatementId)) {
    keyGenerator = configuration.getKeyGenerator(keyStatementId);
  } else {
    keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
        configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
            ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
  }

  SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
  StatementType statementType = StatementType
      .valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
  Integer fetchSize = context.getIntAttribute("fetchSize");
  Integer timeout = context.getIntAttribute("timeout");
  String parameterMap = context.getStringAttribute("parameterMap");
  String resultType = context.getStringAttribute("resultType");
  Class<?> resultTypeClass = resolveClass(resultType);
  String resultMap = context.getStringAttribute("resultMap");
  if (resultTypeClass == null && resultMap == null) {
    resultTypeClass = MapperAnnotationBuilder.getMethodReturnType(builderAssistant.getCurrentNamespace(), id);
  }
  String resultSetType = context.getStringAttribute("resultSetType");
  ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
  if (resultSetTypeEnum == null) {
    resultSetTypeEnum = configuration.getDefaultResultSetType();
  }
  String keyProperty = context.getStringAttribute("keyProperty");
  String keyColumn = context.getStringAttribute("keyColumn");
  String resultSets = context.getStringAttribute("resultSets");
  boolean dirtySelect = context.getBooleanAttribute("affectData", Boolean.FALSE);

  builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap,
      parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered,
      keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets, dirtySelect);
}

这个方法的源码比较长,主要过程有这样几个:1)解析在select、delete、insert、update标签上定义的id属性。2)解析节点名称,比如delete;接着将节点名称解析为SqlCommandType中的枚举(SqlCommandType是一个枚举类,其中的枚举值有UNKNOWN、INSERT、UPDATE、DELETE、SELECT、FLUSH),如果是delete,解析的结果是DELETE;接着就是根据这个枚举值确定isSelect的值(这里是false,因为解析出来的是DELETE);紧接着再解析标签上的flushCache、useCache、resultOrdered属性(注意这几个属性位于select标签上)。3)接着解析标签中的include子标签。4)然后就是解析标签上的parameterType属性,这个想必大家都非常熟悉了,就是sql语句需要的参数的类型,后面会调用resolveClass()方法去解析别名对应的实际类型。5)解析标签上的lang属性并加载对应的驱动。5)后面就是标签中其他属性的解析,比如statementType、fetchSize、timeout、parameterMap、resultType、resultMap、resultSetType、keyColumn、resultSets等等(关于这些属性的解析,这里就不再介绍了,有兴趣可以翻阅并跟踪一下源码)。6)将解析的这些数据添加到BuilderAssistant对象中,通过调用addMappedStatement()方法完成此操作。注意:BuilderAssistant对象在XMLMapperBuilder类的configurationElement(XNode context)方法中出现过,它是XMLMapperBuilder类中的一个属性,之后在创建XMLStatementBuilder对象时,会将其传递给XMLStatementBuilder对象,所以我们在XMLStatementBuilderparseStatementNode()方法中看到的BuilderAssistant对象就是XMMapperBuilder类中那个。这里我们有必要了解一下这个类的继承结构,不过这个已经在前面文章中梳理过了,有兴趣的话可以看一下《MyBatis是纸老虎吗?(三)》这篇博文。未来方便阅读,这里再贴一下这个结构图:

好了继续回到上面梳理的第六步,这一步中会调用MapperBuilderAssistant类中的addMappedStatement()方法完成mapper中相关sql语句信息的存储,该方法的源码为:

public MappedStatement addMappedStatement(String id, SqlSource sqlSource, StatementType statementType,
    SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout, String parameterMap, Class<?> parameterType,
    String resultMap, Class<?> resultType, ResultSetType resultSetType, boolean flushCache, boolean useCache,
    boolean resultOrdered, KeyGenerator keyGenerator, String keyProperty, String keyColumn, String databaseId,
    LanguageDriver lang, String resultSets, boolean dirtySelect) {

  if (unresolvedCacheRef) {
    throw new IncompleteElementException("Cache-ref not yet resolved");
  }

  id = applyCurrentNamespace(id, false);

  MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
      .resource(resource).fetchSize(fetchSize).timeout(timeout).statementType(statementType)
      .keyGenerator(keyGenerator).keyProperty(keyProperty).keyColumn(keyColumn).databaseId(databaseId).lang(lang)
      .resultOrdered(resultOrdered).resultSets(resultSets)
      .resultMaps(getStatementResultMaps(resultMap, resultType, id)).resultSetType(resultSetType)
      .flushCacheRequired(flushCache).useCache(useCache).cache(currentCache).dirtySelect(dirtySelect);

  ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
  if (statementParameterMap != null) {
    statementBuilder.parameterMap(statementParameterMap);
  }

  MappedStatement statement = statementBuilder.build();
  configuration.addMappedStatement(statement);
  return statement;
}

首先看id = applyCurrentNamespace(id, false)这一行代码,它的主要作用就是将前面解析出来的namespace值和mapper中定义的sql语句的id值合并组成一个唯一的数据,比如本节案例中的org.com.chinasofti.springtransaction.UserDao.deleteById。这里有一点需要注意以下applyCurrentNamespace()方法中的currentNamespace的值来源于XMLMapperBuilder中configurationElement ()方法的builderAssistant.setCurrentNamespace()一句。接着创建MappedStatement.Builder对象(这里是一个标准的建造者模式),该对象持有了mapper文件中配置sql语句时设置的所有信息。然后调用本类中的getStatementparameterMap()方法获得一个ParameterMap对象,如果这个对象不为空,则将其设置到MappedStatement.Builder对象的parameterMap属性上。最后调用MappedStatement.Builder对象上的builder()方法创建一个MappedStatement对象并将这个对象添加到Configuration对象的mappedStatement属性上,这是一个Map<String, MappedStatement>类型的map对象。下面看一下MappedStatement对象的详细信息,如下图所示:

由此不难看出MappedStatement存储了sql语句中的所有相关信息,比如sql语句的id、sql语句类型(参见statementType)、参数及响应结果(分别参见parameterMap和resultMaps)、sql语句(参见sqlSource)等等。另外最终解析出来的这个MappedStatement对象被存储到Configuration对象的mappedStatement属性中。关于MappedStatement类的源码这里就不在罗列了,有兴趣的可以翻阅一下MyBatis源码。

下面让我们回到XMLMapperBuilder类的parse()方法中,继续看if分支中的最后一行代码,这里会调用bindMapperForNamespace()方法,该方法的源码如下所示:

private void bindMapperForNamespace() {
  String namespace = builderAssistant.getCurrentNamespace();
  if (namespace != null) {
    Class<?> boundType = null;
    try {
      boundType = Resources.classForName(namespace);
    } catch (ClassNotFoundException e) {
      // ignore, bound type is not required
    }
    if (boundType != null && !configuration.hasMapper(boundType)) {
      // Spring may not know the real resource name so we set a flag
      // to prevent loading again this resource from the mapper interface
      // look at MapperAnnotationBuilder#loadXmlResource
      configuration.addLoadedResource("namespace:" + namespace);
      configuration.addMapper(boundType);
    }
  }
}

再开始介绍该方法前,先来看一下configurationElement()方法执行后Configuration对象的变化,这里主要关注的是Configuration对象中的mappedStatements属性(该属性存储了所有解析出来的sql语句的详细信息),具体如下图所示:

注意这个属性中会将每个sql语句存储两次,一个是以id名为key,一个是以namespace+id为key。为什么这里会注册两个呢?这是因为mappedStatements的实际类型为StrictMap,这是一个继承了ConcurrentHashMap的类,其源码如下所示:

class StrictMap<V> extends ConcurrentHashMap<String, V> {

  private static final long serialVersionUID = -4950446264854982944L;
  private final String name;
  private BiFunction<V, V, String> conflictMessageProducer;

  public StrictMap(String name, int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor);
    this.name = name;
  }

  public StrictMap(String name, int initialCapacity) {
    super(initialCapacity);
    this.name = name;
  }

  public StrictMap(String name) {
    this.name = name;
  }

  public StrictMap(String name, Map<String, ? extends V> m) {
    super(m);
    this.name = name;
  }

  /**
   * Assign a function for producing a conflict error message when contains value with the same key.
   * <p>
   * function arguments are 1st is saved value and 2nd is target value.
   *
   * @param conflictMessageProducer
   *          A function for producing a conflict error message
   *
   * @return a conflict error message
   *
   * @since 3.5.0
   */
  public StrictMap<V> conflictMessageProducer(BiFunction<V, V, String> conflictMessageProducer) {
    this.conflictMessageProducer = conflictMessageProducer;
    return this;
  }

  @Override
  @SuppressWarnings("unchecked")
  public V put(String key, V value) {
    if (containsKey(key)) {
      throw new IllegalArgumentException(name + " already contains key " + key
          + (conflictMessageProducer == null ? "" : conflictMessageProducer.apply(super.get(key), value)));
    }
    if (key.contains(".")) {
      final String shortKey = getShortName(key);
      if (super.get(shortKey) == null) {
        super.put(shortKey, value);
      } else {
        super.put(shortKey, (V) new Ambiguity(shortKey));
      }
    }
    return super.put(key, value);
  }

  @Override
  public boolean containsKey(Object key) {
    if (key == null) {
      return false;
    }

    return super.get(key) != null;
  }

  @Override
  public V get(Object key) {
    V value = super.get(key);
    if (value == null) {
      throw new IllegalArgumentException(name + " does not contain value for " + key);
    }
    if (value instanceof Ambiguity) {
      throw new IllegalArgumentException(((Ambiguity) value).getSubject() + " is ambiguous in " + name
          + " (try using the full name including the namespace, or rename one of the entries)");
    }
    return value;
  }

  protected static class Ambiguity {
    private final String subject;

    public Ambiguity(String subject) {
      this.subject = subject;
    }

    public String getSubject() {
      return subject;
    }
  }

  private String getShortName(String key) {
    final String[] keyParts = key.split("\\.");
    return keyParts[keyParts.length - 1];
  }
}

其泛型类型为MappedStatement,调用该Map类的put方法时,会解析出sql的id值(短id值),然后执行两次put,一次是短key,一次是namespace+key。

下面让我们继续看bindMapperForNamespace()方法的处理逻辑:该方法会拿到MapperBuilderAssistant对象上的命名空间。接着判断该命名空间是否为空,如果不为空,则用Resources加载类,并将结果赋值给Class<?>类型的boundType变量;如果为空则直接结束。紧接着判断boundType变量是否为空,以及Configuration对象是否包含boundType,这个判断最终用的是MapperRegistry对象上的hasMapper()方法判断的,这里我们拿到的数据是org.com.chinasofti.springtransaction.UserDao,并且经过Configuration对象的判断后,可以执行if分支,所以接下来会调用Configuration对象上的addLoadedResource()方法将namespace: org.com.chinasofti.springtransaction.UserDao存放到Configuration对象中的loadedResources属性中(注意此时该属性中已经有user.xml了)。接着继续调用Configuration对象上的addMapper()方法,该方法接收一个Class<?>类型的参数,最终该方法会走到MapperRegistry类中的addMapper(Class<T>)方法中,该方法的源码如下所示:

public <T> void addMapper(Class<T> type) {
  if (type.isInterface()) {
    if (hasMapper(type)) {
      throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
    }
    boolean loadCompleted = false;
    try {
      knownMappers.put(type, new MapperProxyFactory<>(type));
      // It's important that the type is added before the parser is run
      // otherwise the binding may automatically be attempted by the
      // mapper parser. If the type is already known, it won't try.
      MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
      parser.parse();
      loadCompleted = true;
    } finally {
      if (!loadCompleted) {
        knownMappers.remove(type);
      }
    }
  }
}

从源码可知,该方法会首先判断传递进来的type是否是一个接口,如果是则继续,否则不做任何处理。然后再次调用MapperRegistry类中的hasMapper()方法进行判断,看看该对象上的knownMappers属性中是否存在这个接口,如果存在则直接抛出异常,否则继续。接着首先定义一个loadCompleted对象,赋值为false,然后向knownMappers中存放数据,其中key为代表接口的Class<?>对象,value为MapperProxyFactory对象,该对象持有一个Class<?>类型的数据。接下来创建MapperAnnotationBuilder对象,该对象持有一个Configuration对象和一个Class<?>对象,然后调用MapperAnnotationBuilder对象上的parse()方法。该方法首先拿到type所代表的接口的字符串,然后判断这个数据是否被加载过(就是调用Configuration类中的isResourceLoaded ()方法去判断的,与mapper文件解析时调用的方法是一致的),如果没有被加载过,则执行if分支,先是加载该接口对应的xml资源,即loadXmlResource()方法(注意这个方法会首先判断namespace:+接口全包名是否被加载过,就是调用Configuration对象上的isResourceLoaded()方法进行判断的,注意在调用bindMapperForNamespace()方法时,会向loadedResources中添加一个namespace:+接口全包名,所以调用该方法后,不会执行这个方法的具体加载逻辑),接着获取当前接口的命名空间(同时设置该数据到MapperBuilderAssistant对象上,这个逻辑与解析mapper文件时的逻辑是一样的),解析cache和cacheRef,遍历接口中的所有方法,并进行处理(这里会首先判断这个方法是否是继承过来的,如果不是则继续判断该方法上是否存在Select、SelectProvider及ResultMap注解,如果存在则先解析ResultMap,否则就调用本类中的parseStatement()方法去解析sql语句,与xml解析不同的是,这里解析的是注解)。最后调用本类中的parsePendingMethods()方法解析那些没有解析完成的方法。如果感兴趣,可以看一下MapperAnnotationBuilder的源码。

3 总结

很幸运,经过繁杂啰嗦的讨论,我们终于可以腾出脑子梳理一下了。在本篇文章中,我们着重梳理了mappers元素的解析过程,以及mapper元素中的子元素mapper所表示的sql配置文件的解析流程。其实这篇文章的重点就是mapper文件的解析,而这个重点中的重点就是sql命令的解析(在MyBatis中mapper文件中的sql命令是通过MapperStatement类来表示的)。下面就本篇的知识点做个简单的梳理:

  1. 通过这篇文章我们知道mapper文件中的sql命令在java中是通过MapperStatement类来表示的,在mapper中,我们可以配置的元素有很多,最常见的是resultMap、insert、delete、update、select及sql等
  2. 通过这篇文章我们知道解析mapper文件的核心类是XMLMapperBuilder,这个类中的configurationElement ()方法执行具体的解析逻辑。mapper元素的解析入口位于XMLMapperBuilder类的parse()方法中。具体调用路径是这样的:XMLConfigBuilder类的mappersElement()方法【用于解析MyBatis配置文件中的mappers元素】,接着该方法调用XMLMapperBuilder类中的parse()方法【该方法先调用本例的configurationElement()方法】
  3. 通过这篇文章我们知道解析mapper中sql命令的核心类是XMLStatementBuilder,该类中的parseStatementNode()方法是执行解析逻辑的核心。其调用路径为:XMLMapperBuilder#buildStatementFromContext()->XMLMapperBuilder#buildStateemntFromContext()->XMLStatementBuilder#parseStatementNode()
  4. 通过这篇文章我们还知道MapperAnnotationBuilder类也可以解析出MapperStatement命令,不过这个解析是基于注解完成的(本篇文章没有过多着墨,有兴趣的可以配置一下,然后跟踪一下代码)
  5. 还是通过这篇文章我们知道了这些解析出来的MapperStatement对象最终会被存储到Configuration对象中Map<String, MappedStatement>类型的mappedStatements属性中
  6. 又是通过这篇文章我们知道了xml格式的Mapper文件解析后,会主动触发注解式MapperStatement命令的解析流程,这个入口位于XMLStatementBuilder的parse()方法中,这个方法中的bindMapperForNamespace()方法调用就会触发这个逻辑
  7. 最终还是通过这篇文章我们知道在MyBatis配置文件中指定sql配置文件的方式有两种:一种是通过package元素,一种是通过mapper元素(注意这个元素中不能同时出现resource、url、class中的任意两个或三个同时出现,也就是说mapper元素上只能出现一个resource或一个url或一个class)
  • 28
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

机器挖掘工

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值