【MyBatis源码学习】Sql解析


一、前言

之前我在【MyBatis源码学习】初始化阶段 中重点讲述了核心配置类XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder各自的功能。我们先熟悉下这“三剑客”,看下图即可。

"三剑客"

本章节中,我们重点跟一下XMLMapperBuilder、XMLStatementBuilder解析sql的源码过程。
XMLMapperBuilder、XMLStatementBuilder均实现了BaseBuilder。

继承关系

XMLMapperBuilder主要功能:
遍历mybatis中mapperLocations属性中的xml文件中每个节点的Builder,比如user.xml,内部会使用XMLStatementBuilder处理xml中的每个节点。
XMLStatementBuilder主要功能:
解析xml文件中各个节点,比如select,insert,update,delete节点,内部会使用XMLScriptBuilder处理节点的sql部分,遍历产生的数据会丢到Configuration的mappedStatements中。

【MyBatis源码学习】初始化阶段中,我们分析了XMLConfigBuilder的主要工作流程。还是以下面的代码为例,重点分析下XMLMapperBuilder和XMLStatementBuilder解析sql的流程。

@Before
public void init() throws IOException {
    //--------------------第一阶段---------------------------
    // 1.读取mybatis配置文件创SqlSessionFactory
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    // 1.读取mybatis配置文件创SqlSessionFactory
    sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    inputStream.close();
}

进入new SqlSessionFactoryBuilder().build(inputStream)方法,我们看看parse的方法。

build方法
parse方法

而我这里的mybatis-config.xml文件如下:

<?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>
    <properties resource="db.properties"/>

    <settings>
        <!-- 设置自动驼峰转换		 -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <!-- 开启懒加载 -->
        <!-- 当启用时,有延迟加载属性的对象在被调用时将会完全加载任意属性。否则,每种属性将会按需要加载。默认:true -->
        <setting name="aggressiveLazyLoading" value="false"/>
    </settings>

    <!-- 别名定义 -->
    <typeAliases>
        <package name="com.enjoylearning.mybatis.entity"/>
    </typeAliases>

    <plugins>
        <plugin interceptor="com.enjoylearning.mybatis.Interceptors.ThresholdInterceptor">
            <property name="threshold" value="10"/>
        </plugin>
        <plugin interceptor="com.github.pagehelper.PageInterceptor">
            <property name="pageSizeZero" value="true"/>
        </plugin>
    </plugins>

    <!--配置environment环境 -->
    <environments default="development">
        <!-- 环境配置1,每个SqlSessionFactory对应一个环境 -->
        <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>

    <!-- 映射文件,mapper的配置文件 -->
    <mappers>
        <!--直接映射到相应的mapper文件 -->
        <mapper resource="sqlmapper/UserInfoMapper.xml"/>
        <mapper resource="sqlmapper/TUserTestMapper.xml"/>
        <mapper resource="sqlmapper/TRoleMapper.xml"/>
        <mapper resource="sqlmapper/TJobHistoryMapper.xml"/>
        <mapper resource="sqlmapper/TPositionMapper.xml"/>
        <mapper resource="sqlmapper/THealthReportFemaleMapper.xml"/>
        <mapper resource="sqlmapper/THealthReportMaleMapper.xml"/>
    </mappers>
</configuration>  

二、XMLMapperBuilder

对于parser.evalNode(“/configuration”),这里我就不赘述了,重点关注parseConfiguration里面的mapperElement()方法。

mapperElement()方法

这个mapperElement()主要就是解析<mappers>标签下的内容. 这里贴出这个方法的源码,包含注释。

/**
  * 解析mappers节点,例如:
  * <mappers>
  *    <mapper resource="com/github/yeecode/mybatisDemo/UserDao.xml"/>
  *    <package name="com.github.yeecode.mybatisDemo" />
  * </mappers>
  * @param parent mappers节点
  * @throws Exception
  */
 private void mapperElement(XNode parent) throws Exception {
   if (parent != null) {
     for (XNode child : parent.getChildren()) {
       // 处理mappers的子节点,即mapper节点或者package节点
       if ("package".equals(child.getName())) { // package节点
         // 取出包路径
         String mapperPackage = child.getStringAttribute("name");
         // 全部加入Mappers中
         configuration.addMappers(mapperPackage);
       } else {
         // resource、url、class这三个属性只有一个生效
         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);
           // 获取文件的输入流
           InputStream inputStream = Resources.getResourceAsStream(resource);
           // 使用XMLMapperBuilder解析映射文件
           XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
           mapperParser.parse();
         } else if (resource == null && url != null && mapperClass == null) {
           ErrorContext.instance().resource(url);
           // 从网络获得输入流
           InputStream inputStream = Resources.getUrlAsStream(url);
           // 使用XMLMapperBuilder解析映射文件
           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.");
         }
       }
     }
   }
 }

for循环里的if主要是针对<mappers>标签下还有<package>标签的解析。而else则是针对<mappers>标签下还有<mapper>标签的解析。通常,我们的<mapper>会有三种形式,且resource、url、class这三个属性只有一个生效。

<!-- 映射器 1使用类路径-->
<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
  <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
  <mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
       <!-- 2使用绝对url路径-->
<mappers>
  <mapper url="file:///var/mappers/AuthorMapper.xml"/>
  <mapper url="file:///var/mappers/BlogMapper.xml"/>
  <mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>
      <!-- 3使用java类名-->
<mappers>
  <mapper class="org.mybatis.builder.AuthorMapper"/>
  <mapper class="org.mybatis.builder.BlogMapper"/>
  <mapper class="org.mybatis.builder.PostMapper"/>
</mappers>
<!-- 4自动扫描包下所有映射器 -->
<mappers>
  <package name="org.mybatis.builder"/>
</mappers>

这里关注两个方法:

  • mapperParser.parse();
  • configuration.addMapper(mapperInterface);

它们最终的目的就是将映射器的class对象,以及其代理类设置到集合中,采用的是JDK代理。
我们先看addMapper()做了些啥。

//将mapper接口的工厂类添加到mapper注册中心
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 {
  	//实例化Mapper接口的代理工程类,并将信息添加至knownMappers
      knownMappers.put(type, new MapperProxyFactory<T>(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.
      //解析接口上的注解信息,并添加至configuration对象
      MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
      parser.parse();
      loadCompleted = true;
    } finally {
      if (!loadCompleted) {
        knownMappers.remove(type);
      }
    }
  }
}

而XMLMapperBuilder的parse()方法:

public void parse() {
    //判断是否已经加载该配置文件
    if (!configuration.isResourceLoaded(resource)) {
        configurationElement(parser.evalNode("/mapper"));//处理mapper节点
        configuration.addLoadedResource(resource);//将mapper文件添加到configuration.loadedResources中
        bindMapperForNamespace();//注册mapper接口
    }
    //处理解析失败的ResultMap节点
    parsePendingResultMaps();
    //处理解析失败的CacheRef节点
    parsePendingCacheRefs();
    //处理解析失败的Sql语句节点
    parsePendingStatements();
}

我们在看看MapperAnnotationBuilder的parse方法,该类主要是以注解的方式构建mapper,用的比较少。

/**
  * 解析包含注解的接口文档
  */
 public void parse() {
   String resource = type.toString();
   // 防止重复分析
   if (!configuration.isResourceLoaded(resource)) {
     // 寻找类名称对应的 resource路径下是否有 xml 配置,如果有则直接解析掉,这样就支持注解和xml一起混合使用了
     loadXmlResource();
     // 记录资源路径
     configuration.addLoadedResource(resource);
     // 设置命名空间
     assistant.setCurrentNamespace(type.getName());
     // 处理缓存
     parseCache();
     parseCacheRef();
     Method[] methods = type.getMethods();
     for (Method method : methods) {
       try {
         // 排除桥接方法,桥接方法是为了匹配泛型的类型擦除而由编译器自动引入的,并非用户编写的方法,因此要排除掉。
         // issue #237
         if (!method.isBridge()) {
           // 解析该方法
           parseStatement(method);
         }
       } catch (IncompleteElementException e) {
         // 异常方法暂存起来
         configuration.addIncompleteMethod(new MethodResolver(this, method));
       }
     }
   }
   // 处理异常的方法
   parsePendingMethods();
 }

关键看看parseStatement()方法,主要就是解析注解上的信息。再通过getSqlSourceFromAnnotations方法获取sqlSource.

/**
  * 解析该方法,主要是解析方法上的注解信息
  * @param method
  */
 void parseStatement(Method method) {
   // 通过字方法获取参数类型
   Class<?> parameterTypeClass = getParameterType(method);
   // 获取方法的脚本语言渠道
   LanguageDriver languageDriver = getLanguageDriver(method);
   // 通过注解获取SqlSource
   SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
   if (sqlSource != null) {
     // 获取方法上可能存在的配置信息,配置信息由@Options注解指定
     Options options = method.getAnnotation(Options.class);
     final String mappedStatementId = type.getName() + "." + method.getName();
     Integer fetchSize = null;
     Integer timeout = null;
     StatementType statementType = StatementType.PREPARED;
     ResultSetType resultSetType = configuration.getDefaultResultSetType();
     SqlCommandType sqlCommandType = getSqlCommandType(method);
     boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
     boolean flushCache = !isSelect;
     boolean useCache = isSelect;

     KeyGenerator keyGenerator;
     String keyProperty = null;
     String keyColumn = null;
     if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
       // first check for SelectKey annotation - that overrides everything else
       SelectKey selectKey = method.getAnnotation(SelectKey.class);
       if (selectKey != null) {
         keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver);
         keyProperty = selectKey.keyProperty();
       } else if (options == null) {
         keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
       } else {
         keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
         keyProperty = options.keyProperty();
         keyColumn = options.keyColumn();
       }
     } else {
       keyGenerator = NoKeyGenerator.INSTANCE;
     }

     if (options != null) {
       if (FlushCachePolicy.TRUE.equals(options.flushCache())) {
         flushCache = true;
       } else if (FlushCachePolicy.FALSE.equals(options.flushCache())) {
         flushCache = false;
       }
       useCache = options.useCache();
       fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348
       timeout = options.timeout() > -1 ? options.timeout() : null;
       statementType = options.statementType();
       if (options.resultSetType() != ResultSetType.DEFAULT) {
         resultSetType = options.resultSetType();
       }
     }

     String resultMapId = null;
     ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
     if (resultMapAnnotation != null) {
       resultMapId = String.join(",", resultMapAnnotation.value());
     } else if (isSelect) {
       resultMapId = parseResultMap(method);
     }

     // 将获取到的信息存入 configuration
     assistant.addMappedStatement(
         mappedStatementId,
         sqlSource,
         statementType,
         sqlCommandType,
         fetchSize,
         timeout,
         // ParameterMapID
         null,
         parameterTypeClass,
         resultMapId,
         getReturnType(method),
         resultSetType,
         flushCache,
         useCache,
         // TODO gcode issue #577
         false,
         keyGenerator,
         keyProperty,
         keyColumn,
         // DatabaseID
         null,
         languageDriver,
         // ResultSets
         options != null ? nullOrEmpty(options.resultSets()) : null);
   }
 }

getSqlSourceFromAnnotations()方法。

/**
  * 通过注解获取SqlSource对象
  * 
  * @param method 含有注解的方法
  * @param parameterType 参数类型
  * @param languageDriver 语言渠道
  * @return SqlSource对象
  */
 private SqlSource getSqlSourceFromAnnotations(Method method, Class<?> parameterType, LanguageDriver languageDriver) {
   try {
     // 遍历寻找是否有 Select、Insert、Update、Delete四个注解之一
     Class<? extends Annotation> sqlAnnotationType = getSqlAnnotationType(method);
     // 遍历寻找是否有 SelectProvider、InsertProvider、UpdateProvider、DeleteProvider 四个注解之一
     Class<? extends Annotation> sqlProviderAnnotationType = getSqlProviderAnnotationType(method);
     if (sqlAnnotationType != null) {
       if (sqlProviderAnnotationType != null) {
         // 两类注解不能同时使用
         throw new BindingException("You cannot supply both a static SQL and SqlProvider to method named " + method.getName());
       }
       // 取出Select、Insert、Update、Delete四个注解之一
       Annotation sqlAnnotation = method.getAnnotation(sqlAnnotationType);
       // 取出value值
       final String[] strings = (String[]) sqlAnnotation.getClass().getMethod("value").invoke(sqlAnnotation);
       // 基于字符串构建SqlSource,直接注解获取SQL
       return buildSqlSourceFromStrings(strings, parameterType, languageDriver);
     } else if (sqlProviderAnnotationType != null) {
       // // 取出 SelectProvider、InsertProvider、UpdateProvider、DeleteProvider 四个注解之一
       Annotation sqlProviderAnnotation = method.getAnnotation(sqlProviderAnnotationType);
       // 根据对应的方法获取SqlSource,间接注解获取SQL
       return new ProviderSqlSource(assistant.getConfiguration(), sqlProviderAnnotation, type, method);
     }
     return null;
   } catch (Exception e) {
     throw new BuilderException("Could not find value method on SQL annotation.  Cause: " + e, e);
   }
 }

总结下解析mapper的parse()方法有两种方式:

  • XMLMapperBuilder的parse方法
  • MapperAnnotationBuilder的parse方法

MapperAnnotationBuilder的parse方法与XMLMapperBuilder的parse方法逻辑上略有不同,主要体现在对节点的解析上。

上面只是大致分析了*mapper.java与*mapper.xml映射注册的过程,由于我们的*mapper.xml还有很多其他的xml标签,这里我们需要具体了解下其中的解析流程。还是回到XMLMapperBuilder的parse()方法。
我们看看configurationElement()方法干了些啥。

/**
  * 解析映射文件的下层节点
  * @param context 映射文件根节点
  */
 private void configurationElement(XNode context) {
   try {
    //获取mapper节点的namespace属性
     String namespace = context.getStringAttribute("namespace");
     if (namespace == null || namespace.equals("")) {
         throw new BuilderException("Mapper's namespace cannot be empty");
     }
     //设置builderAssistant的namespace属性
     builderAssistant.setCurrentNamespace(namespace);
     //解析cache-ref节点
     cacheRefElement(context.evalNode("cache-ref"));
     //重点分析 :解析cache节点----------------1-------------------二级缓存
     cacheElement(context.evalNode("cache"));
     //解析parameterMap节点(已废弃)
     parameterMapElement(context.evalNodes("/mapper/parameterMap"));
     //重点分析 :解析resultMap节点(基于数据结果去理解)----------------2-------------------
     resultMapElements(context.evalNodes("/mapper/resultMap"));
     //解析sql节点
     sqlElement(context.evalNodes("/mapper/sql"));
     //重点分析 :解析select、insert、update、delete节点 ----------------3-------------------sql解析
     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);
   }
 }

重点关注这几个方法:

  • cacheElement:解析<cache>,与二级缓存有关.
  • resultMapElements:解析<resultMap>,结果集映射.
  • sqlElement:解析sql节点<sql>.
  • buildStatementFromContext:解析<select>、<insert>、<update>、<delete>节点.

而当前我们的UserInfoMapper如下:

UserInfoMapper.xml

接下来,我们关注buildStatementFromContext解析<select>、<insert>、<update>、<delete>标签的过程。主要就是看看XMLStatementBuilder是怎么干活儿的。

三、XMLStatementBuilder

//解析select、insert、update、delete节点
private void buildStatementFromContext(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
        buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
}

//处理所有的sql语句节点并注册至configuration对象
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
        //创建XMLStatementBuilder 专门用于解析sql语句节点
        final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
        try {
            //解析sql语句节点,并将解析结果存储到 configuration 的 mappedStatements 集合中
            statementParser.parseStatementNode();
        } catch (IncompleteElementException e) {
            configuration.addIncompleteStatement(statementParser);
        }
    }
}

点进去看看parseStatementNode()方法。

public void parseStatementNode() {
    //获取sql节点的id
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
        return;
    }
    /*获取sql节点的各种属性*/
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultType = context.getStringAttribute("resultType");
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    Class<?> resultTypeClass = resolveClass(resultType);
    String resultSetType = context.getStringAttribute("resultSetType");
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);

    //根据sql节点的名称获取SqlCommandType(INSERT, UPDATE, DELETE, SELECT)
    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
    //在解析sql语句之前先解析<include>节点(查询的结果有哪些参数)
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    // Parse selectKey after includes and remove them.
    //在解析sql语句之前,处理<selectKey>子节点,并在xml节点中删除
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    //解析sql语句是解析mapper.xml的核心,实例化sqlSource,使用sqlSource封装sql语句
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    //获取resultSets属性
    String resultSets = context.getStringAttribute("resultSets");
    //获取主键信息keyProperty
    String keyProperty = context.getStringAttribute("keyProperty");
    //获取主键信息keyColumn
    String keyColumn = context.getStringAttribute("keyColumn");

    //根据<selectKey>获取对应的SelectKeyGenerator的id
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);


    //获取keyGenerator对象,如果是insert类型的sql语句,会使用KeyGenerator接口获取数据库生产的id;
    if (configuration.hasKeyGenerator(keyStatementId)) {
        keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
        keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
                configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
                ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }

    //通过builderAssistant实例化MappedStatement,并注册至configuration对象
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
            fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
            resultSetTypeEnum, flushCache, useCache, resultOrdered,
            keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

总结下它干了哪些活儿:

  • 解析 <include> 节点
  • 解析 <selectKey> 节点
  • 解析 SQL,获取 SqlSource
  • 构建 MappedStatement 实例

解析<select>、<insert>、<update>、<delete>这些标签的关键方法在这里

SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

看看这个方法的一般实现类XMLScriptBuilder.

@Override
//解析xml文件中的sql语句并封装成SqlSource
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
  XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
  return builder.parseScriptNode();
}

//解析sql脚本,返回SqlSource
public SqlSource parseScriptNode() {
  MixedSqlNode rootSqlNode = parseDynamicTags(context);
  SqlSource sqlSource = null;
  if (isDynamic) {
    sqlSource = new DynamicSqlSource(configuration, rootSqlNode);//动态sql的解析
  } else {
    sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);//非动态sql的解析
  }
  //实际返回的都是StaticSqlSource,可以直接让数据库执行的sql语句,包含?占位符
  return sqlSource;
}

方法parseDynamicTags()就是真正去解析sql语句的淫儿了。当执行createSqlSource()方法中的new XMLScriptBuilder(configuration, script, parameterType)时,我们看下,它初始化了些什么东东。

XMLScriptBuilder构造方法

可以看到关键的部分,对于不同的标签,比如<trim>、<where>等,都有不同的处理策略。在执行parseDynamicTags()时,会判断节点是否包含一些动态标记,比如 ${} 占位符以及动态 SQL 节点等。若包含动态标记,则会将 isDynamic 设为 true。后续可根据 isDynamic 创建不同的 SqlSource。

SqlSource

通常,我们在xml内写sql都是片段的去编写,主要语句写完了,还会有些动态标签包裹的语句,对于mybatis来说,每个片段都会解析成为一个sqlNode存起来,我们先来回忆一下sqlNode有哪些实现类。

sqlNode实现类

这些 SqlNode 是如何生成的呢?我们看看parseDynamicTags()中的这段代码:

handler.handleNode(child, contents);

它有很多个实现。

handleNode的那些实现

到此 SQL 语句的解析过程搞完了。但我们只是完成了xml解析为sqlSource的流程,而一般sql语句还会有一些附加属性,需要mybatis去解析出来,封装至MappedStatement当中,最终还要把它注册到configuration。回到XMLStatementBuilder的parseStatementNode()方法。

parseStatementNode()

具体方法实现:

//添加MappedStatement对象
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) {

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

  id = applyCurrentNamespace(id, false);
  boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

  //使用建造者模式创建一个mappedStatment
  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(valueOrDefault(flushCache, !isSelect))
      .useCache(valueOrDefault(useCache, isSelect))
      .cache(currentCache);

  ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
  if (statementParameterMap != null) {
    statementBuilder.parameterMap(statementParameterMap);
  }
  //使用建造者模式创建一个mappedStatment
  MappedStatement statement = statementBuilder.build();
  //将mappedStatment注册到configuration
  configuration.addMappedStatement(statement);
  return statement;
}

以上就是生成MappedStatement的过程。但是还没完,*mapper.java还没有和*mapper.xml对应的sql绑定起来。接下来就是绑定过程了。

四、Mapper 接口绑定

返回到XMLMapperBuilder的parse()方法。

public void parse() {
   //判断是否已经加载该配置文件
   if (!configuration.isResourceLoaded(resource)) {
       configurationElement(parser.evalNode("/mapper"));//处理mapper节点
       configuration.addLoadedResource(resource);//将mapper文件添加到configuration.loadedResources中
       bindMapperForNamespace();//注册mapper接口
   }
   //处理解析失败的ResultMap节点
   parsePendingResultMaps();
   //处理解析失败的CacheRef节点
   parsePendingCacheRefs();
   //处理解析失败的Sql语句节点
   parsePendingStatements();
}

主要关注这个方法bindMapperForNamespace().

//注册mapper接口
private void bindMapperForNamespace() {
    //获取命名空间
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
        Class<?> boundType = null;
        try {
            //通过命名空间获取mapper接口的class对象
            boundType = Resources.classForName(namespace);
        } catch (ClassNotFoundException e) {
            //ignore, bound type is not required
        }
        if (boundType != null) {
            if (!configuration.hasMapper(boundType)) {//是否已经注册过该mapper接口?
                // 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.loadedResource集合中
                configuration.addLoadedResource("namespace:" + namespace);
                //将mapper接口添加到mapper注册中心
                configuration.addMapper(boundType);
            }
        }
    }
}

绑定完正常的节点后,还要处理一些不正常的。

//处理解析失败的ResultMap节点
parsePendingResultMaps();
//处理解析失败的CacheRef节点
parsePendingCacheRefs();
//处理解析失败的Sql语句节点
parsePendingStatements();

比如这种cacheRef标签

<!-- 映射文件1 -->
<mapper namespace="com.enjoylearning.mybatis.dao.Mapper1">
    <!-- 引用映射文件2中配置的缓存 -->
    <cache-ref namespace="com.enjoylearning.mybatis.dao.Mapper2"/>
</mapper>
 
<!-- 映射文件2 -->
<mapper namespace="com.enjoylearning.mybatis.dao.Mapper2">
    <cache/>
</mapper>

Game over.


文章作者: Kezade
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Kezade !
评论
  目录