【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官网中展示了如下属性

<select
  id="selectPerson"
  parameterType="int"
  parameterMap="deprecated"
  resultType="hashmap"
  resultMap="personResultMap"
  flushCache="false"
  useCache="true"
  timeout="10"
  fetchSize="256"
  statementType="PREPARED"
  resultSetType="FORWARD_ONLY">

每个属性的作用(版本mybatis3)

AttributeDescription
id当前namespace中代表该条语句的唯一id。
parameterType将会传入这条语句的参数的类全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过类型处理器(TypeHandler)推断出具体传入语句的参数,默认值为未设置(unset)。
parameterMap弃用
resultType期望从这条语句中返回结果的类全限定名或别名。如果需要返回集合,那么这里的类型应该是集合的元素的类型,而不是集合本身的类型。resultType 和 resultMap 之间只能同时使用一个。
resultMap引用外部resultMap标签(结果映射)的id。
flushCache为true时将会在调用此语句时清空一级缓存(本地缓存)和二级缓存。默认false。
useCache为true时将查询语句的结果缓存在二级缓存中。默认true。
timeout设置数据库连接超时等待时间。默认未设置(由驱动driver程序控制超时时间)。
fetchSize这是一个给驱动的建议值,尝试让驱动程序每次批量返回的结果行数等于这个设置值。 默认值为未设置(unset)(依赖驱动)。
statementType执行SQL语句的类型。默认值:PREPARED。可选项:STATEMENT、PREPARED、CALLABLE。依据上述顺序MyBatis分别使用Statement(执行静态SQL语句)PreparedStatement(执行预编译SQL语句)CallableStatement(执行SQL存储过程)来进行语句操作
resultSetType可选项:FORWARD_ONLY、SCROLL_SENSITIVE、SCROLL_INSENSITIVE、DEFAULT(与未设置相同)。默认未设置。
databaseId如果配置了数据库厂商标识(databaseIdProvider),MyBatis 会加载所有不带 databaseId 或匹配当前 databaseId 的语句;如果带和不带的语句都有,则不带的会被忽略。
resultOrdered这个设置仅针对嵌套结果 select 语句:如果为 true,将会假设包含了嵌套结果集或是分组,当返回一个主结果行时,就不会产生对前面结果集的引用。 这就使得在获取嵌套结果集的时候不至于内存不够用。默认值:false
resultSets这个设置仅适用于多结果集的情况。它将列出语句执行后返回的结果集并赋予每个结果集一个名称,多个名称之间以逗号分隔。

上面就是select标签中所有的属性,在后面的加载流程中将只会涉及常用的属性。

那么在mybatis源码中,执行xml<select>语句需要经过什么样的流程呢?

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

1. 创建MappedStatement

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

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


MappedStatement的生成过程

XML文件生成MappedStatement流程

在mybatis代码中,每个xml操作数据库的标签(<select><insert><update><delete>),会生成对应的MappedStatement类。这个类对于Mybatis框架来说,非常的重要,也是理解整个框架的重要参考。

在Mybatis提交SQL语句到数据库时,MappedStatement类就包含了执行操作所需要的预编译SQL语句、参数配置、状态信息、缓存等信息。

生成MappedStatement流程图:

注意:这里的SqlSessionFactoryBean类,是Spring集成mybatis的包,用于Spring管理mybatis生成的bean,其他集成框架如mybatis-plus可以与这里有所不同。参考MyBatis-Spring

生成MappedStatement方式

Mybatis中有两种方法生成MappedStatement:

1. 注解方式(MapperAnnotationBuilder)

2. xml方式(XMLMapperBuilder)

前者通过扫描Mapper.java文件读取注解、参数等相关内容,后者扫描Mapper.xml文件读取xml标签的内容。这二者在生成时都会尝试解析对方的文件是否被扫描,并尝试对其进行扫描。

这里不会去深究这两种方式是如何读取的文件内容,只会从总体上了解他们的处理流程。由于时间不足,本文只讨论通过xml方式生成MappedStatement,只会在一些必要场景处涉及注解生成。

MappedStatement类

如下,是mapper.xml文件中的一个简单的select标签

    <select id="getPageList" resultMap="BaseResultMap">
        select id, class_name
        from t_class
    </select>

每一个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<ResultMap> 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构造函数实际传入的参数:

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")); // 解析<parameterMap>
    resultMapElements(context.evalNodes("/mapper/resultMap"));  // 解析<resultMap>标签
    sqlElement(context.evalNodes("/mapper/sql")); // 解析<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的中间数据,例如<sql>生成的sqlFragments就会用于后面的<include>标签解析。

标签-属性对应关系
标签作用存放类是否被final修饰属性名
cache-ref二级缓存关联命名空间ConfigurationtruecacheRefMap
cache二级缓存Configurationtruecaches
cache二级缓存MapperBuilderAssistantfalsecurrentCache
parameterMap入参ConfigurationtrueparameterMaps
resultMap返回结果ConfigurationtrueresultMaps
sqlConfigurationtruesqlFragments
sqlXMLMapperBuildertruesqlFragments
select|insert|update|deletesql语句的配置信息ConfigurationtruemappedStatements

XMLMapperBuilder#buildStatementFromContext

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

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

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

XMLStatementBuilder#parseStatementNode

这个方法是最终解析<select>标签属性并生成MappedStatement类的方法,且方法处理的东西比较多,所有我会根据类型来分类,然后逐一分析这些类型。

1. <select>标签的普通属性

一些不需要处理直接获取的属性,下面代码可以看到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值,如果该属性没有在<select>标签中设置,那么将默认值设置为“!isSelect”。

2. <include>标签处理

解析所有<include>标签,将其替换为refid属性所对应的<sql>标签。

  // Include Fragments before parsing
  XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
  includeParser.applyIncludes(context.getNode());

找到include标签的节点,并将其替换掉。

private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
  // 遍历include类型的元素节点
  if ("include".equals(source.getNodeName())) {
    Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext); // 获取include的refid属性值,并在sqlFragments中尝试获取该sql标签
    Properties toIncludeContext = getVariablesContext(source, variablesContext);
    applyIncludes(toInclude, toIncludeContext, true); // include中的include标签
    if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
      toInclude = source.getOwnerDocument().importNode(toInclude, true);
    }
    source.getParentNode().replaceChild(toInclude, source); // include标签替换为sql标签
    while (toInclude.hasChildNodes()) {
      toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
    }
    toInclude.getParentNode().removeChild(toInclude);
  } else if (source.getNodeType() == Node.ELEMENT_NODE) {
    // 如果当前节点是一个元素节点,继续向下遍历这个元素的每一个节点
    if (included && !variablesContext.isEmpty()) {
      // replace variables in attribute values
      NamedNodeMap attributes = source.getAttributes();
      for (int i = 0; i < attributes.getLength(); i++) {
        Node attr = attributes.item(i);
        attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
      }
    }
    NodeList children = source.getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      applyIncludes(children.item(i), variablesContext, included);
    }
  } else if (included && (source.getNodeType() == Node.TEXT_NODE || source.getNodeType() == Node.CDATA_SECTION_NODE)
      && !variablesContext.isEmpty()) {
    // replace variables in text node 替换Text类型节点中的变量
    source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
  }
}

这里可以简单的介绍一下Mybatis使用的Node以及实现类DeferredXXXXImpl。

Node接口是整个文档对象模型的主要数据类型。 它代表文档树中的单个节点,每一个节点都定义了各自的数据类型。

而由于每个节点的类型不同,也就有了处理各种节点类型的实现类。·

这些类中主要属性的作用:

/** 当前节点树的第一个节点 */
protected ChildNode firstChild = null;

/** 前一个节点 */
protected ChildNode previousSibling;

/** 后一个节点 */
protected ChildNode nextSibling;

/** 节点名称 */
protected String name;

/** 标签的属性Map */
protected AttributeMap attributes;

可以看看下面这一段<select>的Node结构是怎么样的:

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

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

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

…………

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已经相差无几了:

 最终将把参数通过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类。

待续…………

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值