【MyBatis】mapper.xml解析及annotation支持源码分析

项目搭建

参考之前的一篇文章【MyBatis】基本使用

XML解析过程分析

解析入口

  • SqlSessionFactoryBean
    SqlSessionFactoryBean.png

    由于SqlSessionFactoryBean实现了InitializingBean接口,所以Spring加载完SqlSessionFactoryBean实例后会调用afterPropertiesSet方法
    org.mybatis.spring.SqlSessionFactoryBean#afterPropertiesSet
  @Override
  public void afterPropertiesSet() throws Exception {
    notNull(dataSource, "Property 'dataSource' is required");
    notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
    state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
              "Property 'configuration' and 'configLocation' can not specified with together");

    this.sqlSessionFactory = buildSqlSessionFactory();
  }

在xml中配置给SqlSessionFactoryBean初始化时会自动将配置拷贝到Configuration中,Configuration其实就是MyBatis的一个配置中心。

  protected SqlSessionFactory buildSqlSessionFactory() throws IOException {

    Configuration configuration;

    XMLConfigBuilder xmlConfigBuilder = null;
    if (this.configuration != null) {
      configuration = this.configuration;
      if (configuration.getVariables() == null) {
        configuration.setVariables(this.configurationProperties);
      } else if (this.configurationProperties != null) {
        configuration.getVariables().putAll(this.configurationProperties);
      }
    } else if (this.configLocation != null) {
      //解析全局配置文件  
      xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
      configuration = xmlConfigBuilder.getConfiguration();
    } else {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");
      }
      configuration = new Configuration();
      if (this.configurationProperties != null) {
        configuration.setVariables(this.configurationProperties);
      }
    }

   ......

    configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource));

    if (!isEmpty(this.mapperLocations)) {
      for (Resource mapperLocation : this.mapperLocations) {
        if (mapperLocation == null) {
          continue;
        }

        try {
          XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
              configuration, mapperLocation.toString(), configuration.getSqlFragments());
          xmlMapperBuilder.parse();
        } catch (Exception e) {
          throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
        } finally {
          ErrorContext.instance().reset();
        }

        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Parsed mapper file: '" + mapperLocation + "'");
        }
      }
    } else {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Property 'mapperLocations' was not specified or no matching resources found");
      }
    }

    return this.sqlSessionFactoryBuilder.build(configuration);
  }

下面详细解释一下其中的几个类

  • Configuration
    MyBatis配置的一个容器,注册了一些默认的属性,以及后面生成的ResultMap、MappedStatement等都会加入这个容器中
package org.apache.ibatis.session;
public class Configuration {

 protected final InterceptorChain interceptorChain = new InterceptorChain();
  protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry();
  protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
  protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();

  protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");
  protected final Map<String, Cache> caches = new StrictMap<Cache>("Caches collection");
  protected final Map<String, ResultMap> resultMaps = new StrictMap<ResultMap>("Result Maps collection");
  protected final Map<String, ParameterMap> parameterMaps = new StrictMap<ParameterMap>("Parameter Maps collection");
  protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<KeyGenerator>("Key Generators collection");
......

public Configuration() {
    typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
    typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);

    typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
    typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
    typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);

    typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
    typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
    typeAliasRegistry.registerAlias("LRU", LruCache.class);
    typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
    typeAliasRegistry.registerAlias("WEAK", WeakCache.class);

    typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);

  ......
    typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
    typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);

    languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
    languageRegistry.register(RawLanguageDriver.class);
  }
}
  • ConfigLocation

是指全局配置文件的路径,因为这些配置都可以直接在SqlSessionFactoryBean 中配置所以现在一般都不再直接新增一个Mybatis的配置文件

 Configuration configuration;

    XMLConfigBuilder xmlConfigBuilder = null;
    if (this.configuration != null) {
      configuration = this.configuration;
      if (configuration.getVariables() == null) {
        configuration.setVariables(this.configurationProperties);
      } else if (this.configurationProperties != null) {
        configuration.getVariables().putAll(this.configurationProperties);
      }
    } else if (this.configLocation != null) {
      xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
      configuration = xmlConfigBuilder.getConfiguration();
    } else {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration");
      }
      configuration = new Configuration();
      if (this.configurationProperties != null) {
        configuration.setVariables(this.configurationProperties);
      }
    }
  • Environment
 if (this.transactionFactory == null) {
      this.transactionFactory = new SpringManagedTransactionFactory();
    }

    configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource));
  • mapperLocations
    mapper文件的路径

开始解析mapper.xml

SqlSessionFactoryBean#buildSqlSessionFactory中遍历mapperLocations,并一个一个的解析mapper文件。

#org.mybatis.spring.SqlSessionFactoryBean#buildSqlSessionFactory
    if (!isEmpty(this.mapperLocations)) {
      for (Resource mapperLocation : this.mapperLocations) {
        if (mapperLocation == null) {
          continue;
        }

        try {
          XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
              configuration, mapperLocation.toString(), configuration.getSqlFragments());
          xmlMapperBuilder.parse();
        } catch (Exception e) {
          throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
        } finally {
          ErrorContext.instance().reset();
        }

        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Parsed mapper file: '" + mapperLocation + "'");
        }
      }
    } else {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Property 'mapperLocations' was not specified or no matching resources found");
      }
    }
#org.apache.ibatis.builder.xml.XMLMapperBuilder#parse
  public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      //解析mapper.xml
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      //annotation支持
      bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }

Mybatis使用SAX解析xml文件,parser.evalNode("/mapper")获取mapper节点的内容。

<mapper namespace="com.excelib.dao.UserMapper">
<insert keyProperty="id" parameterType="User" useGeneratedKeys="true" id="insertUser">
<selectKey order="BEFORE" keyProperty="id" resultType="int">
            select if(max(id) is null ,1,max(id)+2) as newId from user
        </selectKey>
</insert>
<select resultType="User" parameterType="integer" id="getUser">
        select * from user where id = #{id}
    </select>
<select parameterType="integer" id="deleteUser">
        delete from user where id = #{id}
    </select>
</mapper>

依次解析如下的每一个一级节点,XMLMapperBuilder#configurationElement将Xml配置转换为Java对象。

cache-ref 
cache
/mapper/parameterMap
/mapper/resultMap
/mapper/sql
select|insert|update|delete
#org.apache.ibatis.builder.xml.XMLMapperBuilder#configurationElement
  private void configurationElement(XNode context) {
    try {
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        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"));
      //resultMap的解析
      resultMapElements(context.evalNodes("/mapper/resultMap"));
     //sql标签的解析
      sqlElement(context.evalNodes("/mapper/sql"));
     //select 等标签的解析      
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);
    }
  }

ResultMap节点解析

重点关注一下resultMap节点的解析,Mybatis的对ResultMap的解析步骤如下

  1. 遍历XML中所有的ResultMap节点
  2. 读取ResultMap节点的属性
  3. 遍历ResultMap的子节点,将数据库的每一列的映射关系解析成一个ResultMapping对象,添加到集合resultMappings中
  4. 将resultMappings封装到ResultMap中,构造ResultMap对象
  5. 以ResultMap.getId()为key,将ResultMap添加到Configuration.resultMaps中
  • 读取<resultMap/>节点数据
    由于一个mapper中可以含有多个resultMap 所以resultMapElements(context.evalNodes("/mapper/resultMap"));context.evalNodes("/mapper/resultMap")的解析结果其实是一个集合,如下:
[<resultMap type="com.excelib.User" id="userMap">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="age" property="age"/>
<result column="sex" property="sex"/>
</resultMap>]
  • 生成ResultMapping
    开始具体的解析过程,读取resultMap 的子节点生成resultMapping对象,将所有的ResultMapping存放到一个List集合resultMappings中。
#org.apache.ibatis.builder.xml.XMLMapperBuilder#resultMapElements
//遍历所有的resultMap节点
private void resultMapElements(List<XNode> list) throws Exception {
   for (XNode resultMapNode : list) {
     try {
       resultMapElement(resultMapNode);
     } catch (IncompleteElementException e) {
       // ignore, it will be retried
     }
   }
 }
 private ResultMap resultMapElement(XNode resultMapNode) throws Exception {
   return resultMapElement(resultMapNode, Collections.<ResultMapping> emptyList());
 }

 private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {
   ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
   //读取resultMap节点的属性
   String id = resultMapNode.getStringAttribute("id",
       resultMapNode.getValueBasedIdentifier());
   String type = resultMapNode.getStringAttribute("type",
       resultMapNode.getStringAttribute("ofType",
           resultMapNode.getStringAttribute("resultType",
               resultMapNode.getStringAttribute("javaType"))));
   String extend = resultMapNode.getStringAttribute("extends");
   Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
   Class<?> typeClass = resolveClass(type);
   Discriminator discriminator = null;
   List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
   resultMappings.addAll(additionalResultMappings);
   List<XNode> resultChildren = resultMapNode.getChildren();
   // 遍历resultMap的子节点,数据库的每一列的映射关系解析成resultMapping对象
   for (XNode resultChild : resultChildren) {
     if ("constructor".equals(resultChild.getName())) {
       processConstructorElement(resultChild, typeClass, resultMappings);
     } else if ("discriminator".equals(resultChild.getName())) {
       discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
     } else {
       List<ResultFlag> flags = new ArrayList<ResultFlag>();
       if ("id".equals(resultChild.getName())) {
         flags.add(ResultFlag.ID);
       }
       //解析resultMapping
       resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
     }
   }
   //将resultMappings封装到ResultMap中
   ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
   try {
     return resultMapResolver.resolve();
   } catch (IncompleteElementException  e) {
     configuration.addIncompleteResultMap(resultMapResolver);
     throw e;
   }
 }

XMLMapperBuilder#buildResultMappingFromContext将每一个<result column="age" property="age"/>解析成一个ResultMapping

  private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) throws Exception {
    String property;
    if (flags.contains(ResultFlag.CONSTRUCTOR)) {
      property = context.getStringAttribute("name");
    } else {
      property = context.getStringAttribute("property");
    }
    String column = context.getStringAttribute("column");
    String javaType = context.getStringAttribute("javaType");
    String jdbcType = context.getStringAttribute("jdbcType");
    String nestedSelect = context.getStringAttribute("select");
    String nestedResultMap = context.getStringAttribute("resultMap",
        processNestedResultMappings(context, Collections.<ResultMapping> emptyList()));
    String notNullColumn = context.getStringAttribute("notNullColumn");
    String columnPrefix = context.getStringAttribute("columnPrefix");
    String typeHandler = context.getStringAttribute("typeHandler");
    String resultSet = context.getStringAttribute("resultSet");
    String foreignColumn = context.getStringAttribute("foreignColumn");
    boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager"));
    Class<?> javaTypeClass = resolveClass(javaType);
    @SuppressWarnings("unchecked")
    Class<? extends TypeHandler<?>> typeHandlerClass = (Class<? extends TypeHandler<?>>) resolveClass(typeHandler);
    JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
    return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
  }
  • 生成ResultMap并加入Configuration
    实际上是加入了Configuration.resultMaps这个map中,key默认是xml中的id+当前的namespace
#org.apache.ibatis.builder.ResultMapResolver#resolve
  public ResultMap resolve() {
    return assistant.addResultMap(this.id, this.type, this.extend, this.discriminator, this.resultMappings, this.autoMapping);
  }
#org.apache.ibatis.builder.MapperBuilderAssistant#addResultMap
public ResultMap addResultMap(
      String id,
      Class<?> type,
      String extend,
      Discriminator discriminator,
      List<ResultMapping> resultMappings,
      Boolean autoMapping) {
    ......
  //获取该resultMap 的id,默认是xml中的id+namespace
    id = applyCurrentNamespace(id, false);
  //创建ResultMap对象
    ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
        .discriminator(discriminator)
        .build();
     //将resultMap添加到configuration中
    configuration.addResultMap(resultMap);
    return resultMap;
  }

#org.apache.ibatis.session.Configuration#addResultMap
 protected final Map<String, ResultMap> resultMaps = new StrictMap<ResultMap>("Result Maps collection");
 public void addResultMap(ResultMap rm) {
    resultMaps.put(rm.getId(), rm);
    checkLocallyForDiscriminatedNestedResultMaps(rm);
    checkGloballyForDiscriminatedNestedResultMaps(rm);
  }

select|insert|update|delete 标签的解析

  1. 解析select|insert|update|delete等xml标签生成MixedSqlNode;
  2. 封装SqlNode到sqlSource中。SqlSource实际上只是对SqlNode的封装并没有实际生成Sql语句,因为MyBatis的动态Sql是根据查询条件动态拼接生成最终数据库执行的Sql;
  3. 根据解析到的sqlSource,resultMap等配置创建MappedStatement并加入Configuration中,实际上加入了Configuration.mappedStatements这个map中。每一个select|insert|update|delete节点都对应一个MappedStatement。

使用buildStatementFromContext(context.evalNodes("select|insert|update|delete"));来遍历并解析解析mapper中的每一个select|insert|update|delete节点

#org.apache.ibatis.builder.xml.XMLMapperBuilder#buildStatementFromContext(java.util.List<org.apache.ibatis.parsing.XNode>)`
private void buildStatementFromContext(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
      buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
  }
  private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }

 public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");
    //匹配databaseId,如果不匹配则不解析该sql
    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }

    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);
    //获取resultType对应的Class
    Class<?> resultTypeClass = resolveClass(resultType);
    String resultSetType = context.getStringAttribute("resultSetType");
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    //节点名称select|insert|update|delete
    String nodeName = context.getNode().getNodeName();
   //sqlCommandType 是指sql类型,如insert,update等
    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());

    // Parse selectKey after includes and remove them.
    //解析selectkey标签
    processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    //提取sqlSource
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    String resultSets = context.getStringAttribute("resultSets");
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    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;
    }
    //生成MappedStatement并加入configuration中
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }
  • 解析Sql类型
    上面的代码中已经解析出了当前sql的类型select|insert|update|delete
#org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));

public enum SqlCommandType {
  UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
}
  • 解析SqlNode生成SqlSource
    SqlSource实际上只是对SqlNode的封装,并没有实际生成Sql语句,因为MyBatis的动态Sql是根据查询条件动态拼接生成最终数据库执行的Sql;
#org.apache.ibatis.builder.xml.XMLStatementBuilder#parseStatementNode
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
//XMLLanguageDriver#createSqlSource(Configuration, XNode, java.lang.Class<?>)
  @Override
  public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    return builder.parseScriptNode();
  }

#org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseScriptNode
public SqlSource parseScriptNode() {
    //解析xml节点封装成MixedSqlNode 
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    //生成sqlSource
    SqlSource sqlSource = null;
    if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }
  • 解析SqlNode
    将sql中的每一个节点都解析为一个特定的SqlNode,并判断子节点是否是普通的文本节点,如果是动态节点(包含${})则封装成TextSqlNode如果不是封装成StaticTextSqlNode
    如果不是普通文本节点则,则递归解析动态子节点
#org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseDynamicTags
  protected MixedSqlNode parseDynamicTags(XNode node) {
    List<SqlNode> contents = new ArrayList<SqlNode>();
    NodeList children = node.getNode().getChildNodes();
   //遍历所有的子节点
    for (int i = 0; i < children.getLength(); i++) {
      XNode child = node.newXNode(children.item(i));
 //判断子节点是否是普通的文本节点,如果是文本节点则解析结束,如果不是则递归解析动态子节点
      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
        String data = child.getStringBody("");
        TextSqlNode textSqlNode = new TextSqlNode(data);
        if (textSqlNode.isDynamic()) {
          contents.add(textSqlNode);
          isDynamic = true;
        } else {
          //如果不包含${}则是静态文本节点
          contents.add(new StaticTextSqlNode(data));
        }
      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
        String nodeName = child.getNode().getNodeName();
        //寻找NodeHandler 
        NodeHandler handler = nodeHandlerMap.get(nodeName);
        if (handler == null) {
          throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
        }
        //处理子节点
        handler.handleNode(child, contents);
        isDynamic = true;
      }
    }
    return new MixedSqlNode(contents);
  }

上面的解析过程有一点复杂,我们用下面的sql来看一下它的解析过程

<update id="updateUser" parameterType="com.excelib.User">
        update user
        <set>
            <if test="name!=null and name !=''">
                name = #{name},
            </if>
            <if test="age!=null and age !=''">
                age = #{age}
            </if>
        </set>
        where id = #{id}
    </update>

parseDynamicTags方法中NodeList children = node.getNode().getChildNodes();其实拿到的是上面sql的3个子节点的内容

update user
<set><if test="name!=null and name !=''"> name = #{name},</if><if test="age!=null and age !=''">age = #{age}</if></set>
where id = #{id}

依次遍历三个子节点,根据判定条件封装成不同的sqlNode对象


SqlNode.png

sqlNode解析步骤

  1. update user
    判断child.getNode().getNodeType() == Node.TEXT_NODE成立,即为文本节点;然后调用textSqlNode.isDynamic()去判断该节点是否是动态的,这条语句中不包含${}所以不是动态的(GenericTokenParser("${", "}", handler)),最终会调用contents.add(new StaticTextSqlNode(data));

  2. <set>
    child.getNode().getNodeType() == Node.ELEMENT_NODE这个判断是成立,即是它动态节点;然后根据nodeName从nodeHandlerMap查找nodeHandler并调用 handler.handleNode(child, contents);进行动态节点解析。

NodeHandler.png
//org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#initNodeHandlerMap
  private void initNodeHandlerMap() {
    nodeHandlerMap.put("trim", new TrimHandler());
    nodeHandlerMap.put("where", new WhereHandler());
    nodeHandlerMap.put("set", new SetHandler());
    nodeHandlerMap.put("foreach", new ForEachHandler());
    nodeHandlerMap.put("if", new IfHandler());
    nodeHandlerMap.put("choose", new ChooseHandler());
    nodeHandlerMap.put("when", new IfHandler());
    nodeHandlerMap.put("otherwise", new OtherwiseHandler());
    nodeHandlerMap.put("bind", new BindHandler());
  }

这里的nodeName就是set,所以会查找到SetHandler来解析,看一下它的解析方法

//org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.SetHandler#handleNode
@Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
      MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
      SetSqlNode set = new SetSqlNode(configuration, mixedSqlNode);
      targetContents.add(set);
    }

这里又递归调用了parseDynamicTags()解析set节点的子节点,并封装成MixedSqlNode返回。可以预期在这次调用parseDynamicTags()中会有2个if子节点,然后每个if节点又会调用IfHandler.handleNode解析子元素,if的子节点只有文本节点所以解析终止返回一个文本节点,最终一层一层返回。

//org.apache.ibatis.scripting.xmltags.XMLScriptBuilder.IfHandler#handleNode
 @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
      MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
       //读取if节点的test属性
      String test = nodeToHandle.getStringAttribute("test");
      IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
      targetContents.add(ifSqlNode);
    }
  1. where
    也判断child.getNode().getNodeType() == Node.TEXT_NODE成立,即为文本节点,会调用contents.add(new StaticTextSqlNode(data));最终会都会加入contents并返回new MixedSqlNode(contents);

所以最终解析结果,如下:

sql语句 节点类型
update user StaticTextSqlNode
<set> SetSqlNode
<if test="name!=null and name !=''"> IfSqlNode
name = #{name}, StaticTextSqlNode
</if>
<if test="age!=null and age !=''"> IfSqlNode
age = #{age} StaticTextSqlNode
</if>
</set>
where id = #{id} StaticTextSqlNode

调试结果也正是这样


image.png

接下来通过new DynamicSqlSource(configuration, rootSqlNode);封装成SqlSource。
最后将insert|update等解析结果封装成MappedStatement并加入Configuration中builderAssistant.addMappedStatement(...);

//MapperBuilderAssistant#addMappedStatement(.....)
public MappedStatement addMappedStatement(...) {
......
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)
        //获取resultMap
        .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);
    }

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

注解支持

前面分析了configurationElement(parser.evalNode("/mapper"));解析mapper.xml。MyBatis其实也提供注解支持。

MyBatis 常用注解

  1. @SelectProvider
    动态调用某类的某个方法,并使用其返回值作为sql语句
  2. @Select
    使用注解value值作为sql语句
  3. @Select({"<script>",sql,"</script>"})
    动态解析value值生成动态Sql
  4. @Options
    该MappedStatement的配置参数,如:cache timeout等
    具体使用可以参考【MyBatis】基本使用

注解使用示例:

public interface UserMapper {
    public void insertUser(User user);
    public User getUser(Integer id);
    public void updateUser(User user);
    public void deleteUser(Integer id);

    /**
     * 标识提供sql语句的类和方法,mybatis会调用该类的方法获取sql语句,生成MappedStatement注册
     */
    @SelectProvider(type = SqlProvider.class,method = "getQueryUserSql")
    public User queryUserWithSqlProvider(@Param("id") Integer id);

    /**
     * 使用注解配置sql语句
     */
    @Select("select * from user where id = #{id}")
    public User queryUserWithAnnotation(@Param("id") Integer id);

    /**
     * 添加<script> 可以则sql语句可以使用动态标签
     */
    @Select({"<script>",
            "        update user\n" +
            "        <set>\n" +
            "            <if test=\"name!=null and name !=''\">\n" +
            "                name = #{name},\n" +
            "            </if>\n" +
            "            <if test=\"age!=null and age !=''\">\n" +
            "                age = #{age}\n" +
            "            </if>\n" +
            "        </set>\n" +
            "        where id = #{id}\n",
            "</script>"})
    public void updateUserWithAnnotation(@Param("id") Integer id,@Param("name")String name,@Param("age")Integer age);

}

回到一开始解析mapper.xml的入口的地方 XMLMapperBuilder#parse ,其中的 bindMapperForNamespace();就是提供注解支持的入口。首先获取mapper.xml的Namespace,根据nameSpace的值加载Class对象(其实是一个接口)。我们要处理的对象就是该接口里所有方法的注解。

#org.apache.ibatis.builder.xml.XMLMapperBuilder#parse
  public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      //解析mapper.xml
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      //annotation支持
      bindMapperForNamespace();
    }
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }

private void bindMapperForNamespace() {
    //获取当前的Namespace,根据nameSpace的值生成Class,其实是Mapper接口的Class
    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) {
        //判断是否存储了mapper工厂
        if (!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);
          //将class加入Configuration,里面会有对mapper接口的解析
          configuration.addMapper(boundType);
        }
      }
    }
  }

configuration.addMapper(boundType)中提供了对注解mapper的解析,使用MapperAnnotationBuilder.parse()来对该接口的方法上的annotation进行解析,而且使用knownMappers来保存mapper代理工厂,key就是这个mapper接口类型(nameSpace)。这个MapperProxyFactory会用来产生动态代理bean(注入到Spring),当我们调用bean的方法的时候就会回调代理的invoke方法,在该方法中查找mappedStatement来组装sql并调用。

//org.apache.ibatis.session.Configuration#addMapper
 public <T> void addMapper(Class<T> type) {
    mapperRegistry.addMapper(type);
  }
//org.apache.ibatis.binding.MapperRegistry#addMapper
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<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.
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

具体的解析过程

  1. 遍历接口的每一个方法
  2. 首先根据方法注解创建出一个SqlSource。因为涉及到动态sql的解析,所以这一步也比较复杂;
  3. 然后获取可以识别的注解,提取注解配置的属性到对应的Java对象中;
  4. 最终生成一个MappedStatement并注册到Configuration中;
public class MapperAnnotationBuilder {
//org.apache.ibatis.builder.annotation.MapperAnnotationBuilder
 public MapperAnnotationBuilder(Configuration configuration, Class<?> type) {
    String resource = type.getName().replace('.', '/') + ".java (best guess)";
    this.assistant = new MapperBuilderAssistant(configuration, resource);
    this.configuration = configuration;
    this.type = type;

    sqlAnnotationTypes.add(Select.class);
    sqlAnnotationTypes.add(Insert.class);
    sqlAnnotationTypes.add(Update.class);
    sqlAnnotationTypes.add(Delete.class);

    sqlProviderAnnotationTypes.add(SelectProvider.class);
    sqlProviderAnnotationTypes.add(InsertProvider.class);
    sqlProviderAnnotationTypes.add(UpdateProvider.class);
    sqlProviderAnnotationTypes.add(DeleteProvider.class);
  }
public void parse() {
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {
      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();
  }

  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);
      ......
      if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
        // first check for SelectKey annotation - that overrides everything else
        //解析@SelectKey
        SelectKey selectKey = method.getAnnotation(SelectKey.class);
        ......
      } else {
        keyGenerator = NoKeyGenerator.INSTANCE;
      }

      if (options != null) {
        ......
      }

      String resultMapId = null;
      //解析@ResultMap
      ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
      if (resultMapAnnotation != null) {
     ......
      } else if (isSelect) {
        resultMapId = parseResultMap(method);
      }
      assistant.addMappedStatement(......);
     
    }
  }
}

从注解中生成SqlSource的过程,分为两种情况:
一种是 @Select @Insert @Update @Delete注解
一种是使用SelectSqlProvider、InsertSqlProvider、UpdateSqlProvider 、DeleteSqlProvider提供Sql语句 。

//从Annotation中提取Sql语句并交给XMLLanguageDriver解析
private SqlSource getSqlSourceFromAnnotations(Method method, Class<?> parameterType, LanguageDriver languageDriver) {
    try {
      Class<? extends Annotation> sqlAnnotationType = getSqlAnnotationType(method);
      Class<? extends Annotation> sqlProviderAnnotationType = getSqlProviderAnnotationType(method);
      if (sqlAnnotationType != null) {
       //如果是 Select.class Insert.class Update.class Delete.class 则直接将value拿去解析
        if (sqlProviderAnnotationType != null) {
          throw new BindingException("You cannot supply both a static SQL and SqlProvider to method named " + method.getName());
        }
        Annotation sqlAnnotation = method.getAnnotation(sqlAnnotationType);
        final String[] strings = (String[]) sqlAnnotation.getClass().getMethod("value").invoke(sqlAnnotation);
        return buildSqlSourceFromStrings(strings, parameterType, languageDriver);
      } else if (sqlProviderAnnotationType != null) {
         //如果是 SelectSqlProvider.class InsertSqlProvider.class UpdateSqlProvider.class DeleteSqlProvider.class 等类型则封装成ProviderSqlSource
        Annotation sqlProviderAnnotation = method.getAnnotation(sqlProviderAnnotationType);
        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);
    }
  }
  private SqlSource buildSqlSourceFromStrings(String[] strings, Class<?> parameterTypeClass, LanguageDriver languageDriver) {
    final StringBuilder sql = new StringBuilder();
    for (String fragment : strings) {
      sql.append(fragment);
      sql.append(" ");
    }
    return languageDriver.createSqlSource(configuration, sql.toString().trim(), parameterTypeClass);
  }
  • 如果是@Select @Insert @Update @Delete则直接将value拿去XMLLanguageDriver 解析,会判断如果sql是以<script>开头则动态解析sql的子节点,如果不是则直接封装成SqlSource。关于动态解析子节点的直接参考前面mapper.xml 中 insert|update等标签的解析过程(递归生成SqlNode)。
/**
 * @author Eduardo Macarron
 */
public class XMLLanguageDriver implements LanguageDriver {

  @Override
  public ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
    return new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);
  }

  @Override
  public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    return builder.parseScriptNode();
  }

  @Override
  public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
    // issue #3
    if (script.startsWith("<script>")) {
      XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
      return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
    } else {
      // issue #127
      script = PropertyParser.parse(script, configuration.getVariables());
      TextSqlNode textSqlNode = new TextSqlNode(script);
      if (textSqlNode.isDynamic()) {
        return new DynamicSqlSource(configuration, textSqlNode);
      } else {
        return new RawSqlSource(configuration, script, parameterType);
      }
    }
  }
}
  • 如果是SelectSqlProvider InsertSqlProvider UpdateSqlProvider DeleteSqlProvider等类型则封装成ProviderSqlSource。在Excutor执行数据库操作的时候会调用SqlSource.getBoundSql(),这时会首先会从parameterObject抽取出调用参数封装成Map,然后使用该参数反射调用SqlProvider中指定的方法生成sql。
public class ProviderSqlSource implements SqlSource {
  @Override
  public BoundSql getBoundSql(Object parameterObject) {
    SqlSource sqlSource = createSqlSource(parameterObject);
    return sqlSource.getBoundSql(parameterObject);
  }

  private SqlSource createSqlSource(Object parameterObject) {
    try {
      int bindParameterCount = providerMethodParameterTypes.length - (providerContext == null ? 0 : 1);
      String sql;
      if (providerMethodParameterTypes.length == 0) {
        sql = invokeProviderMethod();
      } else if (bindParameterCount == 0) {
        sql = invokeProviderMethod(providerContext);
      } else if (bindParameterCount == 1 &&
              (parameterObject == null || providerMethodParameterTypes[(providerContextIndex == null || providerContextIndex == 1) ? 0 : 1].isAssignableFrom(parameterObject.getClass()))) {
        sql = invokeProviderMethod(extractProviderMethodArguments(parameterObject));
      } else if (parameterObject instanceof Map) {
        @SuppressWarnings("unchecked")
        Map<String, Object> params = (Map<String, Object>) parameterObject;
        sql = invokeProviderMethod(extractProviderMethodArguments(params, providerMethodArgumentNames));
      } else {
        ......
      }
      Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
      return sqlSourceParser.parse(replacePlaceholder(sql), parameterType, new HashMap<String, Object>());
    } catch (BuilderException e) {
      throw e;
    } catch (Exception e) {
    ......
    }
  }

  private Object[] extractProviderMethodArguments(Object parameterObject) {
    if (providerContext != null) {
      Object[] args = new Object[2];
      args[providerContextIndex == 0 ? 1 : 0] = parameterObject;
      args[providerContextIndex] = providerContext;
      return args;
    } else {
      return new Object[] { parameterObject };
    }
  }

  private Object[] extractProviderMethodArguments(Map<String, Object> params, String[] argumentNames) {
    Object[] args = new Object[argumentNames.length];
    for (int i = 0; i < args.length; i++) {
      if (providerContextIndex != null && providerContextIndex == i) {
        args[i] = providerContext;
      } else {
        args[i] = params.get(argumentNames[i]);
      }
    }
    return args;
  }
  private String invokeProviderMethod(Object... args) throws Exception {
    Object targetObject = null;
    if (!Modifier.isStatic(providerMethod.getModifiers())) {
      targetObject = providerType.newInstance();
    }
    CharSequence sql = (CharSequence) providerMethod.invoke(targetObject, args);
    return sql != null ? sql.toString() : null;
  }
}

annotation解析工作都完成后,生成一个MappedStatement并注册到Configuration中。其实无论是解析注解还是解析mapper.xml最终都是一个insert|update|delete|select方法或者节点生成一个SqlSource来构造一个MappedStatement并注册到Configuration中。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342