MyBatis是纸老虎吗?(五)

news2024/9/27 9:23:16

最近看到这样一则消息《全球首位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)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1538924.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

YOLOv8 | 注意力机制 | ShuffleAttention注意力机制 提升检测精度

YOLOv8成功添加ShuffleAttention ⭐欢迎大家订阅我的专栏一起学习⭐ &#x1f680;&#x1f680;&#x1f680;订阅专栏&#xff0c;更新及时查看不迷路&#x1f680;&#x1f680;&#x1f680; YOLOv5涨点专栏&#xff1a;http://t.csdnimg.cn/1Aqzu YOLOv8涨点专栏…

力扣236 二叉树的最近公共祖先 Java版本

文章目录 题目描述代码 题目描述 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。 百度百科中最近公共祖先的定义为&#xff1a;“对于有根树 T 的两个节点 p、q&#xff0c;最近公共祖先表示为一个节点 x&#xff0c;满足 x 是 p、q 的祖先且 x 的深度尽可能大&…

PDF文件如何以数字进行批量重命名?以数字重命名的PDF文件

在日常生活和工作中&#xff0c;我们经常需要处理大量的PDF文件&#xff0c;如文档、报告、合同等。为了更高效地管理这些文件&#xff0c;一个有效的方式就是对它们进行批量命名。批量命名不仅能提高文件的组织性&#xff0c;还能节省大量时间。下面&#xff0c;我们将详细介绍…

【数据分析案列】--- 北京某平台二手房可视化数据分析

一、引言 本案列基于北京某平台的二手房数据&#xff0c;通过数据可视化的方式对二手房市场进行分析。通过对获取的数据进行清冼&#xff08;至关重要&#xff09;&#xff0c;对房屋价格、面积、有无电梯等因素的可视化展示&#xff0c;我们可以深入了解北京二手房市场的特点…

docker学习笔记 三-----docker安装部署

我使用的部署环境是centos 7.9 1、安装依赖工具 yum install -y yum-utils device-mapper-persistent-data lvm2 安装完成如下图 2、添加docker的软件信息源 yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo url地址为如…

飞桨AI应用@riscv OpenKylin

在riscv编译安装飞桨PaddlePaddle参见&#xff1a; 算能RISC-V通用云编译飞桨paddlepaddleopenKylin留档_在riscv下进行paddlelite源码编译-CSDN博客 安装好飞桨&#xff0c;就可以用飞桨进行推理了。刚开始计划用ONNX推理&#xff0c;但是在算能云没有装上&#xff0c;所以最…

C语言——程序拷贝文件

问题如下&#xff1a; 写一个程序拷贝文件&#xff1a; 使用所学文件操作&#xff0c;在当前目录下放一个文件data.txt&#xff0c;写一个程序&#xff0c;将data.txt文件拷贝一份&#xff0c;生成data_copy.txt文件。 基本思路&#xff1a; 打开文件data.txt&#xff0c;读…

服务器中了.[hpssupfast@mailfence.com].Elbie勒索病毒,数据还能恢复吗?

引言&#xff1a; .[hpssupfastmailfence.com].Elbie勒索病毒是一种网络攻击病毒&#xff0c;它会在感染用户的计算机系统中放置恶意软件&#xff0c;该软件会对用户的文件进行加密并要求支付赎金以解密文件。这种病毒通常通过网络钓鱼、木马植入等方式传播&#xff0c;利用用户…

【Ubuntu 22.04 LTS】安装vmware提示没有兼容的gcc

在ubuntu 22.04 上运行wmware时显示找不到兼容的gcc 这里要求的是12.3.0版本&#xff0c;我查看了自己的gcc版本是上面的11.4.0 在ask ubuntu上找到了解决方法 尝试了这一条 三条命令执行完成之后&#xff0c;再次运行vm&#xff0c;没有提示gcc的问题 点击install下载相应模…

Unity vision pro模拟器开发教程-附常见问题解决方案

前言 庄生晓梦迷蝴蝶&#xff0c;望帝春心托杜鹃 废话 去年苹果发布会上&#xff0c;推出了Vision Pro这一款XR产品。并且宣布Unity作为其主要合作伙伴&#xff0c;负责开发XR的开发产品。 这消息一出&#xff0c;当晚Unity的股价直接被熔断。产品发布之后&#xff0c;一直等…

深度强化学习(九)(改进策略梯度)

深度强化学习&#xff08;九&#xff09;&#xff08;改进策略梯度&#xff09; 一.带基线的策略梯度方法 Theorem: 设 b b b 是任意的函数, b b b与 A A A无关。把 b b b 作为动作价值函数 Q π ( S , A ) Q_\pi(S, A) Qπ​(S,A) 的基线, 对策略梯度没有影响: ∇ θ J …

实例:NX二次开发使用链表进行拉伸功能(链表相关功能练习)

一、概述 在进行批量操作时经常会利用链表进行存放相应特征的TAG值&#xff0c;以便后续操作&#xff0c;最常见的就是拉伸功能。这里我们以拉伸功能为例子进行说明。 二、常用链表相关函数 UF_MODL_create_list 创建一个链表&#xff0c;并返回链表的头指针。…

Codeforces Round #936 (Div. 2)B~D

1946B - Maximum Sum 可以想到&#xff0c;每次都将最大连续子序列放到该子序列的最后&#xff0c;也就是每一轮都能将最大连续子序列倍增一次填到数组中&#xff0c;最终求结果 // Problem: B. Maximum Sum // Contest: Codeforces - Codeforces Round 936 (Div. 2) // URL: …

【Flink】Flink 中的时间和窗口之窗口其他API的使用

1. 窗口的其他API简介 对于一个窗口算子而言&#xff0c;窗口分配器和窗口函数是必不可少的。除此之外&#xff0c;Flink 还提供了其他一些可选的 API&#xff0c;可以更加灵活地控制窗口行为。 1.1 触发器&#xff08;Trigger&#xff09; 触发器主要是用来控制窗口什么时候…

算法系列--动态规划--子序列(2)

&#x1f495;"你可以说我贱&#xff0c;但你不能说我的爱贱。"&#x1f495; 作者&#xff1a;Mylvzi 文章主要内容&#xff1a;算法系列–动态规划–子序列(2) 今天带来的是算法系列--动态规划--子序列(2),包含了关于子序列问题中较难的几道题目(尤其是通过二维状…

uni-app打包证书android

Android平台打包发布apk应用&#xff0c;需要使用数字证书&#xff08;.keystore文件&#xff09;进行签名&#xff0c;用于表明开发者身份。 Android证书的生成是自助和免费的&#xff0c;不需要审批或付费。 可以使用JRE环境中的keytool命令生成。 以下是windows平台生成证…

springboot实现文件上传

SpringBoot默认静态资源访问方式 首先想到的就是可以通过SpringBoot通常访问静态资源的方式&#xff0c;当访问&#xff1a;项目根路径 / 静态文件名时&#xff0c;SpringBoot会依次去类路径下的四个静态资源目录下查找&#xff08;默认配置&#xff09;。 在资源文件resour…

极大提高工作效率的 Linux 命令

作为一名软件开发人员&#xff0c;掌握 Linux 命令是必不可少的技能。即使你使用 Windows 或 macOS&#xff0c;你总会遇到需要使用 Linux 命令的场合。例如&#xff0c;大多数 Docker 镜像都基于 Linux 系统。要进行 DevOps 工作&#xff0c;你需要熟悉Linux&#xff0c;至少要…

Redis中的缓存穿透

缓存穿透 缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在&#xff0c;导致这些请求直接到了数据库上&#xff0c;对数据库造成了巨大的压力&#xff0c;可能造成数据库宕机。 常见的解决方案&#xff1a; 1&#xff09;缓存无效 key 如果缓存和数据库中都查不到某…

【漏洞复现】WordPress Plugin NotificationX 存在sql注入CVE-2024-1698

漏洞描述 WordPress和WordPress plugin都是WordPress基金会的产品。WordPress是一套使用PHP语言开发的博客平台。该平台支持在PHP和MySQL的服务器上架设个人博客网站。WordPress plugin是一个应用插件。 WordPress Plugin NotificationX 存在安全漏洞,该漏洞源于对用户提供的…