MyBatis 源码分析-01-解析sql

说明:

version :

  • MyBatis : 3.5.5
  • 项目地址 :MyBatis-source-learn

image-20210325142809324

文章主要是通过源码的方式,分别介绍了MyBatis在解析XML文件以及Annotation注解获取sql文件是所做的事情,文章略长,建议读者耐心看完.

PS:

  • 文章中有一些东西未提到,不代表重要,文章主要是站在程序的执行的流程上进行的写作
  • MyBatis 代码不同版本之间会存在些微的不同,请自行处理
  • 文中内容大部分只需要跟着debug走一遍即可,需要重点解析的我会有说明的..
  • 重点内容:

代码案例:

configuration.addMapper(BlogMapper.class)

通过这个方法, 可以获得每个方法的statement,resultset,sql命令

大致顺序是: 1. 处理方法参数 -2. 处理sql-3. 处理statement 4. 处理结果集 (eg: getReturnType(method),)

BlogMapper.java

这个类参考管方给的例子

package io.github.whywhathow.MyBatissourcelearn.Mapper;

import io.github.whywhathow.MyBatissourcelearn.entity.Blog;
import org.apache.ibatis.annotations.Select;

/**
 * @program: MyBatis-source-learn
 * @description:
 * @author: WhyWhatHow
 * @create: 2020-10-26 20:53
 **/
public interface BlogMapper {
//    @Select("SELECT * FROM blog WHERE id = #{id}")
    Blog selectBlog(int id);

    @Select("select * from blog where name = #{name}")
    Blog selectBlogByName(String name);
    @Select("select * from blog where name = #{name} and  id = #{id}")
    Blog selectBlogByNameAndId(String name,Integer id );
    
    Blog findByName(String name);
}

BlogMapper.xml

<?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="io.github.whywhathow.MyBatissourcelearn.Mapper.BlogMapper">
    <select id="selectBlog" resultType="io.github.whywhathow.MyBatissourcelearn.entity.Blog">
    select * from Blog where id = #{id}
  </select>

    <select id="findByName" resultType="io.github.whywhathow.MyBatissourcelearn.entity.Blog">
    </select>
</mapper>

测试方法:

  • CreateSqlSession.java
 public static void main(String[] args) {
//         1. 连接数据库,配置基本参数
        DataSource dataSource = getDataSource();
        TransactionFactory transactionFactory = new JdbcTransactionFactory();
        Environment environment = new Environment("development",  transactionFactory, dataSource);
         Configuration configuration = new Configuration(environment);
//         2.  处理XXXMapper, 生成SqlSource(即处理xml 以及注解生成的SQL语句 与PreparedStatement绑定)
        configuration.addMapper(BlogMapper.class);
        // 3.  获取SqlSession
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
        SqlSession sqlSession = sqlSessionFactory.openSession();
//         4. 获取mapper,执行方法
        BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);
        for (int i = 0; i < 10; i++) {
            Blog blog = mapper.selectBlog(1);
            System.out.println("i: "+blog);
        }
    }
  • configuration.addMapper(BlogMapper.class);

源码分析:

addMapper(BlogMapper.class) 过程分析.

Configuration.java

public <T> void addMapper(Class<T> type) {
   mapperRegistry.addMapper(type);
 }

MapperRegistry.java

重要的方法: parser.parse();

public <T> void addMapper(Class<T> type) {
  // 1.判断是否是接口, 是否已经添加该Mapper
  if (type.isInterface()) {
    if (hasMapper(type)) {
      throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
    }
    boolean loadCompleted = false;
    try {
        //1.1 未添加该mapper . add
      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.
      //1.2 获取 sql 绑定 statement
      MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
      parser.parse();
      loadCompleted = true;
    } finally {
      if (!loadCompleted) {
        knownMappers.remove(type);
      }
    }
  }
}

MapperAnnotationBuilder.java

step:

  • 解析xml方式获取sql

    • loadXmlResource(); // 通过xml 格式进行解析
  • 解析注解的方式获取SQL

    • parseStatement(method); // 通过注解的方式进行解析
public void parse() {
  //1. 获取xml路径
  String resource = type.toString();
  if (!configuration.isResourceLoaded(resource)) {
    //2. 尝试获取xml源文件, 若是mapper是通过xml解析的话,此方法结束后,configuration就获得了这个XXMapper.xml文件的所有信息
    loadXmlResource();
    //3. 加入configuration,一下方法尝试通过注解方式进行解析
    configuration.addLoadedResource(resource);
    //4. 设置名称空间
    assistant.setCurrentNamespace(type.getName());
    parseCache();
    parseCacheRef();
    // 5. 尝试通过注解解析, xml的直接跳过
    for (Method method : type.getMethods()) {
      if (!canHaveStatement(method)) {
        continue;
      }
      if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
          && method.getAnnotation(ResultMap.class) == null) {
        parseResultMap(method);
      }
      try {
          // 6. 解释statement ,xml 也是直接跳过
        parseStatement(method);
      } catch (IncompleteElementException e) {
        configuration.addIncompleteMethod(new MethodResolver(this, method));
      }
    }
  }
  // 7. 清理未完成的方法
  parsePendingMethods();
}

XML解析,获取SQL:

综述: XMLStatementBuilder.java:.parseStatementNode();

1. 处理xml文件的过程
MapperANnotationBuilder.java: loadXMlResource() : 加载xml文件
    XMLMapperBuilder.java :  xmlParser.parse(); 
		XMLMapperBuilder.java: configurationElement(parser.evalNode("/mapper"));
			XMLMapperBuilder.java: buildStatementFromContext();
				XMLStatementBuilder.java:.parseStatementNode(); //解析单条SQL解析单条SQL

--------------------------------------------------------------------------
2. 获取sql的过程(SqlSource的生成)
ps: 即XMLStatementBuilder.java:.parseStatementNode(); 解析单条SQL中的sqlSource 的生成
				

ps: 注解方式类似, 不再赘述

!important, 关注重点: 解析单条SQL,其他部分内容Debug一遍即可

loadXMlResource()

**处理XML文件,根据xxxMapper.xml生成对应的Mappedstatement,resultMap… **

  • MapperAnnotationBuilder.java
    
    /**
    * https://www.whywhathow.fun
    *  
    **/
      private void loadXmlResource() {
        // Spring may not know the real resource name so we check a flag
        // to prevent loading again a resource twice
        // this flag is set at XMLMapperBuilder#bindMapperForNamespace
        //1. xml文件的默认位置与mapper.java文件的包文件命名相同, 这个地方容易出错
        if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
          String xmlResource = type.getName().replace('.', '/') + ".xml";
          // #1347
          InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
          if (inputStream == null) {
            // Search XML mapper that is not in the module but in the classpath.
            try {
              inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
            } catch (IOException e2) {
              // ignore, resource is not required
            }
          }
          if (inputStream != null) {
            XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
            // 解析xmlMapper文件,这个方法过后,sql就解析完成了
            xmlParser.parse();
          }
        }
      }
    XMLMapperBuilder.parse()

XMl文件的总体解析流程 :parse()

public void parse() {
  if (!configuration.isResourceLoaded(resource)) {
    // 1.这个方法执行后,configuration 就得到了sql 语句
    configurationElement(parser.evalNode("/mapper"));
    // 2. 标记 xml文件
      configuration.addLoadedResource(resource);
    bindMapperForNamespace();
  }

  parsePendingResultMaps();
  parsePendingCacheRefs();
  parsePendingStatements();
}
  • 解析MapperXML节点: configurationElement(parser.evalNode(“/mapper”)); (细节自己看)
  • before:
    sql-before
  • after:
    sql-after
解析Mapper.XML文件

configurationElement(parser.evalNode(“/mapper”));

XMLMapperBuilder中, 此方法只是处理XXXMapper.XMl文件

private void configurationElement(XNode context) {
    try {
        //1. 获取并设置namespace, 尝试缓存
      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"));
      // 2 .处理一些配置信息 标签
        cacheElement(context.evalNode("cache"));
        //3. 处理方法参数 标签
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      //  4. 处理结果集 标签
        resultMapElements(context.evalNodes("/mapper/resultMap"));
          // 处理<sql> 标签
        sqlElement(context.evalNodes("/mapper/sql"));
        // 6.构建 statement
      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);
    }
  }
解析所有SQL命令,并构建statement:

ldStatementFromContext(context.evalNodes(“select|insert|update|delete”));

遍历节点,通过XMlStatementBuilder解析SQL

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);
    }
  }
}
解析单条sql命令

void parseStatementNode()

真正干活的人

构建statement,并加入mappedStatement

XMLStatementBuilder.java

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

   if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
     return;
   }
//2. 获取sql 类型 select, upate, insert,delate
    // 获取一些基本参数
   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
    // 3 处理include 标签 ,如: <sql id="my">id ,name <sql>
    // <select >select <include refid="my"> from Blog</select>
   XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
   includeParser.applyIncludes(context.getNode());
// 4. 处理方法参数
   String parameterType = context.getStringAttribute("parameterType");
   Class<?> parameterTypeClass = resolveClass(parameterType);
// 5. 处理 long ,格式转化
   String lang = context.getStringAttribute("lang");
   LanguageDriver langDriver = getLanguageDriver(lang);
// todo  处理映射关系
   // Parse selectKey after includes and remove them.
   processSelectKeyNodes(id, parameterTypeClass, langDriver);
// 6. 解析sql
   // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
   // 6.1 设置主键
    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;
   }
// 6.2 获取SQL语句
   SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    //6.3 获取statementType , prepared, statement,callable
   StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    // 6.4 获取一些基本参数, 建议参考官网https://MyBatis.org/MyBatis-3/zh/sqlmap-xml.html
   Integer fetchSize = context.getIntAttribute("fetchSize");
   Integer timeout = context.getIntAttribute("timeout");
    // 6.5 处理参数集合
   String parameterMap = context.getStringAttribute("parameterMap");
    // 6.6 获取结果集类型
   String resultType = context.getStringAttribute("resultType");
   Class<?> resultTypeClass = resolveClass(resultType);
   String resultMap = context.getStringAttribute("resultMap");
   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");
//7. 加入MappedStatement
   builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
       fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
       resultSetTypeEnum, flushCache, useCache, resultOrdered,
       keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
 }

单说, SqlSource的生成

  • 获取SQL语句

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

    XMLLanguageDriver

    @Override
    public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
        //  1.初始化, 添加字段,以及添加方法参数类型
      XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
      return builder.parseScriptNode();
    }
  • 正式处理SQL语句 (一个SQL 一个方法)

XMLScriptBuilder.java

public SqlSource parseScriptNode() {
  MixedSqlNode rootSqlNode = parseDynamicTags(context);
  SqlSource sqlSource;
  if (isDynamic) {
    sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
  } else {
    sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
  }
  return sqlSource;
}
获得最初的sql命令
  • 以静态创建为例

  • sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);

    getSql() 方法后 获得了未去掉占位符的sql 语句

    如: select * from Blog where id = #{id}

 public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
     //  获取到最初的SQL getSql(configuration,rootSqlNode);
   this(configuration, getSql(configuration, rootSqlNode), parameterType);
 }
// 处理SQL参数
 public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
   SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
   Class<?> clazz = parameterType == null ? Object.class : parameterType;
     // 
   sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
 }
处理SQL的参数

打工人,打工魂,

SQLSourceBuilder.java


public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    String sql;
    if (configuration.isShrinkWhitespacesInSql()) {
      sql = parser.parse(removeExtraWhitespaces(originalSql));
    } else {
      sql = parser.parse(originalSql);// 代码替换, #{} -> ? 并生成SQL字符串返回
    }
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());//绑定sql字符串, 参数map,
  }

注解解析,获取SQL:

parseStatement(method)

  • 通过注解的方式进行解析, 获得sql,statement ,( 第一次查看的时候,可以选择性略过,需要投入的时间,经历来细看)
void parseStatement(Method method) {
    // 1, 获取参数类型信息
   final Class<?> parameterTypeClass = getParameterType(method);
   // 2. 获取 language Driver
    final LanguageDriver languageDriver = getLanguageDriver(method);
// 3, 通过注解获得 statement
   getAnnotationWrapper(method, true, statementAnnotationTypes).ifPresent(statementAnnotation -> {
       // 3.1 获取 SQL语句 以及,方法参数 名称,类型,.... 
       // SQL: select * from  blog where  name = ? 
 		// paramter: name , string ,varchar 
     final SqlSource sqlSource = buildSqlSource(statementAnnotation.getAnnotation(), parameterTypeClass, languageDriver, method);
       // 3,2 获取 sql命令的类型 select, update,delete ,insert
     final SqlCommandType sqlCommandType = statementAnnotation.getSqlCommandType();
     	// 3.3. ?? todo  返回为空 猜测: resultmap信息??
       final Options options = getAnnotationWrapper(method, false, Options.class).map(x -> (Options)x.getAnnotation()).orElse(null);
       //io.github.whywhathow.MyBatissourcelearn.Mapper.BlogMapper.selectBlogByName
     final String mappedStatementId = type.getName() + "." + method.getName();
	// guess :主键生成器 true
     final KeyGenerator keyGenerator;
     String keyProperty = null;
     String keyColumn = null;
       // 3.4 判断是否 插入,或者更新操作 ,先略过
     if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
       // first check for SelectKey annotation - that overrides everything else
       SelectKey selectKey = getAnnotationWrapper(method, false, SelectKey.class).map(x -> (SelectKey)x.getAnnotation()).orElse(null);
       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 {
         // 3.4.? 不设置主键生成器,  就是一个空方法
       keyGenerator = NoKeyGenerator.INSTANCE;
     }
	// 3.5 
     Integer fetchSize = null;// 返回结果集的行数
     Integer timeout = null;
       // 3.6 statement 类型:prepared->prepareStatement(预加载), statement->statement(string解析) callable ??? todo 
     StatementType statementType = StatementType.PREPARED;
       // 3.7 获取默认结果集类型
     ResultSetType resultSetType = configuration.getDefaultResultSetType();
     	// 3.8 判断是否是查找操作
       boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
       // 3.9 初始化刷新缓存默认值
       boolean flushCache = !isSelect;// 是否刷新缓存, true ,每一次刷新都会清除缓存,
       // 3.10 初始化 使用缓存默认值 
     boolean useCache = isSelect;// 是否使用缓存, 默认true
       //3.11 设置 flushcache ,usecache
     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();
       }
     }
	// 3.12 设置 resultmap 结果集
     String resultMapId = null;
     if (isSelect) {
       ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
       if (resultMapAnnotation != null) {
         resultMapId = String.join(",", resultMapAnnotation.value());
       } else {
         resultMapId = generateResultMapName(method);
       }
     }
	// 3.13 加入MapppedStatement
     assistant.addMappedStatement(
         mappedStatementId,
         sqlSource,
         statementType,
         sqlCommandType,
         fetchSize,
         timeout,
         // ParameterMapID
         null,
         parameterTypeClass,
         resultMapId,
         getReturnType(method),
         resultSetType,
         flushCache, // 是否刷新缓存
         useCache,// SQL语句是否使用缓存
         // TODO gcode issue #577
         false,
         keyGenerator,
         keyProperty,
         keyColumn,
         statementAnnotation.getDatabaseId(),
         languageDriver,
         // ResultSets
         options != null ? nullOrEmpty(options.resultSets()) : null);
   });
 }

总结:

addMapper(BlogMapper.class) 过程解析:

  1. 通过xml方式解析 : loadXMLResource()

     1.  解析XML文件 
          1.  解析 <mapper/> 标签
               1.  按照 **SQL->statement -> resultSet **顺序生成mappedstatement
  2. 通过 Annotation 方式解析

     1. 遍历每一个方法 ,通过解析注解的形式生成mappedstatement

参考:

  • https://MyBatis.org/MyBatis-3/zh/getting-started.html