【Mybatis源码学习】Sql解析

【Mybatis源码学习】Sql解析

  • 一、前言
  • 二、XMLMapperBuilder
  • 三、XMLStatementBuilder
  • 四、Mapper 接口绑定

一、前言

之前我在【Mybatis源码学习】初始化阶段中重点讲述了核心配置类XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder各自的功能。我们先熟悉下这“三剑客”,看下图即可。
【Mybatis源码学习】Sql解析_第1张图片
本章节中,我们重点跟一下XMLMapperBuilder、XMLStatementBuilder解析sql的源码过程。
XMLMapperBuilder、XMLStatementBuilder均实现了BaseBuilder。
【Mybatis源码学习】Sql解析_第2张图片
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的方法。
【Mybatis源码学习】Sql解析_第3张图片
【Mybatis源码学习】Sql解析_第4张图片
而我这里的mybatis-config.xml文件如下:



<configuration>
    <properties resource="db.properties"/>

    <settings>
        
        <setting name="mapUnderscoreToCamelCase" value="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>

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

    
    <mappers>
        
        <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()方法。
【Mybatis源码学习】Sql解析_第5张图片
这个mapperElement()主要就是解析标签下的内容. 这里贴出这个方法的源码,包含注释。

/**
  * 解析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主要是针对标签下还有标签的解析。而else则是针对标签下还有标签的解析。通常,我们的会有三种形式,且resource、url、class这三个属性只有一个生效。


<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
  <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
  <mapper resource="org/mybatis/builder/PostMapper.xml"/>
mappers>
       
<mappers>
  <mapper url="file:///var/mappers/AuthorMapper.xml"/>
  <mapper url="file:///var/mappers/BlogMapper.xml"/>
  <mapper url="file:///var/mappers/PostMapper.xml"/>
mappers>
      
<mappers>
  <mapper class="org.mybatis.builder.AuthorMapper"/>
  <mapper class="org.mybatis.builder.BlogMapper"/>
  <mapper class="org.mybatis.builder.PostMapper"/>
mappers>

<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:解析,与二级缓存有关.
  • resultMapElements:解析,结果集映射.
  • sqlElement:解析sql节点.
  • buildStatementFromContext:解析标签的过程。主要就是看看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语句之前先解析节点(查询的结果有哪些参数)
        XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
        includeParser.applyIncludes(context.getNode());
    
        // Parse selectKey after includes and remove them.
        //在解析sql语句之前,处理子节点,并在xml节点中删除
        processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
        // Parse the SQL (pre:  and  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");
    
        //根据获取对应的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);
    }
    

    总结下它干了哪些活儿:

    • 解析 节点
    • 解析 节点
    • 解析 SQL,获取 SqlSource
    • 构建 MappedStatement 实例

    解析