【Mybatis源码解析】深入了解<select>等标签的实现流程(一)

前言

以前只知道select标签的个别属性用法,并不了解其实现流程与原理,正巧,最近在学习Cache一二级缓存,看到与select标签相关的属性,就借此机会把这篇文章写了出来。

此外,之所以以select为视角,是因为其他几个标签的源码流程与select大致相当,而且select在实际开发中使用更为频繁。

本文思路:

1. 先了解select标签各个属性作用

2. 从mapper.xml中加载select标签的过程

3. 加载sql操作标签的重要类---MappedStatement

4. 以select为例,解析select在执行时的流程,以及哪些属性将会影响执行中的select查询

5. 什么情况下会创建新的SqlSession,以及何时共用同一SqlSession。

6. SqlSession执行select操作的事务提交操作

涉及注解:

@Select

@Results

@Result

@CacheNamespace

@TypeDiscriminator


select标签属性

属性作用

Mybatis官网中展示了如下属性

语句需要经过什么样的流程呢?

从总体上看,大约有两个步骤:

1. 创建MappedStatement

2. 创建会话(SqlSession)并执行SQL。

下面我会主要围绕这两个大步骤来学习Mybatis框架。


MappedStatement的生成过程

XML文件生成MappedStatement流程

在mybatis代码中,每个xml操作数据库的标签( select id, class_name from t_class

每一个xml操作标签通过一系列操作后转为与之对应的MappedStatement类,select标签中的每个属性都被赋值给了MappedStatement类中的属性。

public final class MappedStatement {
  private String resource;
  private Configuration configuration; // 全局配置环境
  private String id;
  private Integer fetchSize;
  private Integer timeout;
  private StatementType statementType; // 根据sql类型选择不同的sql处理方式
  private ResultSetType resultSetType;
  private SqlSource sqlSource; // 处理后的sql语句
  private Cache cache;
  private ParameterMap parameterMap;
  private List resultMaps;
  private boolean flushCacheRequired;
  private boolean useCache;
  private boolean resultOrdered;
  private SqlCommandType sqlCommandType;
  private KeyGenerator keyGenerator;
  private String[] keyProperties;
  private String[] keyColumns;
  private boolean hasNestedResultMaps;
  private String databaseId;
  private Log statementLog;
  private LanguageDriver lang;
  private String[] resultSets;
}

MappedStatement类由内部类Builder使用建造者模式构建,Builder的构造函数中展示了一些属性的默认设置以及初始化情况。

    public Builder(Configuration configuration, String id, SqlSource sqlSource, SqlCommandType sqlCommandType) {
      mappedStatement.configuration = configuration;
      mappedStatement.id = id;
      mappedStatement.sqlSource = sqlSource;
      mappedStatement.statementType = StatementType.PREPARED;
      mappedStatement.resultSetType = ResultSetType.DEFAULT;
      mappedStatement.parameterMap = new ParameterMap.Builder(configuration, "defaultParameterMap", null, new ArrayList<>()).build();
      mappedStatement.resultMaps = new ArrayList<>();
      mappedStatement.sqlCommandType = sqlCommandType;
      mappedStatement.keyGenerator = configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
      String logId = id;
      if (configuration.getLogPrefix() != null) {
        logId = configuration.getLogPrefix() + id;
      }
      mappedStatement.statementLog = LogFactory.getLog(logId);
      mappedStatement.lang = configuration.getDefaultScriptingLanguageInstance();
    }

Builder构造函数实际传入的参数:

【Mybatis源码解析】深入了解<select>等标签的实现流程(一)_第2张图片

XML转换流程源码

按照上面的流程图,进行源码学习、分析

SqlSessionFactoryBean#buildSqlSessionFactory

这个类位于mybatis与spring集成的包中。

方法的主要功能是扫描指定位置的所有mapper.xml文件,并通过调用XMLMapperBuilder类处理这些xml文件。

for (Resource mapperLocation : this.mapperLocations) {
  if (mapperLocation == null) {
    continue;
  }
  try {
    XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
        targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
    xmlMapperBuilder.parse();
  } catch (Exception e) {
    throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
  } finally {
    ErrorContext.instance().reset();
  }
  LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'");
}

XMLMapperBuilder#parse

除了解析xml文件内容外,还会通过bindMapperForNamespace()方法对对应的Mapper.java接口进行扫描。

这一步有防重复扫描验证,每次扫描完文件后,都会将其存放在全局配置中的已加载资源中。

public void parse() {
  if (!configuration.isResourceLoaded(resource)) { // 避免二次扫描
    configurationElement(parser.evalNode("/mapper"));
    configuration.addLoadedResource(resource); //添加至已扫描列表
    bindMapperForNamespace(); // 将Mapper.java接口的注解等内容绑定至同一命名空间
  }

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

bindMapperForNamespace方法最终调用MapperAnnotationBuilder#parse,其扫描过程与xml方式的一致,它会通过Method的方法获取注解、传入参数、返回类型等数据,从而获得来预编译SQL语句、参数配置、状态信息、缓存等信息。

public void parse() {
  String resource = type.toString();
  if (!configuration.isResourceLoaded(resource)) {
    loadXmlResource();
    configuration.addLoadedResource(resource);
    assistant.setCurrentNamespace(type.getName());
    parseCache(); // @CacheNamespace
    parseCacheRef(); // @CacheNamespaceRef
    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 {
        parseStatement(method); // 生成MappedStatement
      } catch (IncompleteElementException e) {
        configuration.addIncompleteMethod(new MethodResolver(this, method));
      }
    }
  }
  parsePendingMethods();
}

例如parseResultMap(method)方法,平时我们写的@Results、@Result、@TypeDiscriminator等注解就是通过下面的parseResultMap(method)方法解析的。这里就不一一介绍了。

private String parseResultMap(Method method) {
  Class returnType = getReturnType(method);
  Arg[] args = method.getAnnotationsByType(Arg.class);
  Result[] results = method.getAnnotationsByType(Result.class);
  TypeDiscriminator typeDiscriminator = method.getAnnotation(TypeDiscriminator.class);
  String resultMapId = generateResultMapName(method);
  applyResultMap(resultMapId, returnType, args, results, typeDiscriminator); // 生成指定的映射类以及与每个sql字段对应的类的属性
  return resultMapId;
}

XMLMapperBuilder#configurationElement

configurationElement()方法是解析xml文件各个标签元素的主要方法。正如代码中context.evalNode所展示的,它的作用就是提取对应的标签元素,并通过对应方法进行处理。

private void configurationElement(XNode context) {
  try {
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.isEmpty()) {
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    builderAssistant.setCurrentNamespace(namespace);
    cacheRefElement(context.evalNode("cache-ref")); // 解析二级缓存关联的命名空间
    cacheElement(context.evalNode("cache")); // 解析是否打开二级缓存,打开时解析二级缓存的属性
    parameterMapElement(context.evalNodes("/mapper/parameterMap")); // 解析
    resultMapElements(context.evalNodes("/mapper/resultMap"));  // 解析标签
    sqlElement(context.evalNodes("/mapper/sql")); // 解析标签
    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);
  }
}

这里要提一点,大部分生成好的数据都会存放在Configuration这个全局环境中。少部分会用于生成MappedStatement的中间数据,例如生成的sqlFragments就会用于后面的标签解析。

标签-属性对应关系
标签 作用 存放类 是否被final修饰 属性名
cache-ref 二级缓存关联命名空间 Configuration true cacheRefMap
cache 二级缓存 Configuration true caches
cache 二级缓存 MapperBuilderAssistant false currentCache
parameterMap 入参 Configuration true parameterMaps
resultMap 返回结果 Configuration true resultMaps
sql Configuration true sqlFragments
sql XMLMapperBuilder true sqlFragments
select|insert|update|delete sql语句的配置信息 Configuration true mappedStatements

XMLMapperBuilder#buildStatementFromContext

遍历该命名空间中的每一个SQL标签(select|insert|update|delete)。

private void buildStatementFromContext(List list) {
  if (configuration.getDatabaseId() != null) {
    buildStatementFromContext(list, configuration.getDatabaseId());
  }
  buildStatementFromContext(list, null);
}

private void buildStatementFromContext(List 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);
    }
  }
}

其中每一个XNode context的数据如下:

【Mybatis源码解析】深入了解<select>等标签的实现流程(一)_第3张图片

XMLStatementBuilder#parseStatementNode

这个方法是最终解析标签的普通属性

一些不需要处理直接获取的属性,下面代码可以看到flushCache、useCache、parameterType等属性的解析过程。

  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);
  ………………

这些属性是通过key value的方式获取的,通过预先设定的类型转换为需要值,当值为空时,设置默认值。 

譬如context.getBooleanAttribute("flushCache", !isSelect),通过这个方法获取Boolean类型的flushCache值,如果该属性没有在的Node结构是怎么样的:

【Mybatis源码解析】深入了解<select>等标签的实现流程(一)_第6张图片

指明下面的node是一个"select"类型的标签元素。DeferredElementImpl代表当前节点是一个XML文档的元素节点,其下面的所有节点的ownerNode都指向它本身。

firstChild是这个标签元素的第一个节点,它是一个DeferredTextImpl类型的节点(文本节点保存元素或属性的非标记、非实体内容)。

第二节点是一个"include"类型的标签元素,同样是DeferredElementImpl。

…………

【Mybatis源码解析】深入了解<select>等标签的实现流程(一)_第7张图片

3. parameterType、resultType属性的获取

有关别名转换为真实类型或@Alias注解的内容可以参考这一篇文章:【Mybatis源码解析】 parameterType通过别名或缺省方式找到真实类型

  String parameterType = context.getStringAttribute("parameterType");
  Class parameterTypeClass = resolveClass(parameterType);
  …………
  String parameterMap = context.getStringAttribute("parameterMap");
  String resultType = context.getStringAttribute("resultType");
  Class resultTypeClass = resolveClass(resultType);
  String resultMap = context.getStringAttribute("resultMap");

4. SqlSource的生成

 xml中的动态sql语言是通过该段解析。这些动态的sql语言的解析过程,等日后有时间再看………

SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
public SqlSource parseScriptNode() {
  MixedSqlNode rootSqlNode = parseDynamicTags(context); // 将刚刚的DeferredXXXXImpl解析为MixedSqlNode
  SqlSource sqlSource;
  if (isDynamic) {
    sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
  } else {
    sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
  }
  return sqlSource;
}

直接看SqlSource的生成结果,与我们在数据库执行的SQL已经相差无几了:

【Mybatis源码解析】深入了解<select>等标签的实现流程(一)_第8张图片

 最终将把参数通过MappedStatement的建造者生成MappedStatement。

builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
      fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
      resultSetTypeEnum, flushCache, useCache, resultOrdered,
      keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);

MapperBuilderAssistan#addMappedStatement

到这里MappedStatement生成完毕。这一步的参数,都是通过上面的步骤获取的,需要提一点的是,如果有二级缓存,那么这里的currentCache是公用属于同一个命名空间的缓存。

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

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

  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);
  }

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

至此经过mapper接口、xml文件的解析之后,就转换为了Mybatis增删改查时需要的MappedStatement类了,下一步要研究的就是查询时如何创建Sqlsession以及执行时如何使用ms类。

待续…………

你可能感兴趣的:(java,Mybatis,java,mybatis)