mybatis源码阅读(三):mybatis初始化(下)mapper解析

MyBatis 的真正强大在于它的映射语句,也是它的魔力所在。由于它的异常强大,映射器的 XML 文件就显得相对简单。如果拿它跟具有相同功能的 JDBC 代码进行对比,你会立即发现省掉了将近 95% 的代码。MyBatis 就是针对 SQL 构建的,并且比普通的方法做的更好。

SQL 映射文件有很少的几个顶级元素(按照它们应该被定义的顺序):

  • cache – 给定命名空间的缓存配置。
  • cache-ref – 其他命名空间缓存配置的引用。
  • resultMap – 是最复杂也是最强大的元素,用来描述如何从数据库结果集中来加载对象。
  • parameterMap – 已废弃!老式风格的参数映射。内联参数是首选,这个元素可能在将来被移除,这里不会记录。
  • sql – 可被其他语句引用的可重用语句块。
  • insert – 映射插入语句
  • update – 映射更新语句
  • delete – 映射删除语句
  • select – 映射查询语句

对每个标签的属性以及作用,这里不做解释, 可以参考官方文档:http://www.mybatis.org/mybatis-3/zh/sqlmap-xml.html

上一篇文章介绍了mybatis配置文件解析mappers节点的源码中有如下语句,从这里得到mapper映射文件时通过XMLMapperBuilder解析的。

一、XMLMapperBuilder

//mapper映射文件都是通过XMLMapperBuilder解析
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
          mapperParser.parse();
//解析mapper文件
public void parse() {
  // 判断是否已经加载过改映射文件
  if (!configuration.isResourceLoaded(resource)) {
    // 处理mapper节点
    configurationElement(parser.evalNode("/mapper"));
    // 将resource添加到configuration的loadedResources集合中保存 它是HashSet<String>
    configuration.addLoadedResource(resource);
    //注册mapper接口
    bindMapperForNamespace();
  }
  // 处理解析失败的resultMap节点
  parsePendingResultMaps();
  // 处理解析失败的cache-ref节点
  parsePendingCacheRefs();
  // 处理解析失败的sql节点
  parsePendingStatements();
}
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);
    // 解析cache-ref节点
    cacheRefElement(context.evalNode("cache-ref"));
    // 解析cache节点
    cacheElement(context.evalNode("cache"));
    // 解析parameterMap节点,这个已经被废弃,不推荐使用
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    // 解析resultMap节点
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    // 解析sql节点
    sqlElement(context.evalNodes("/mapper/sql"));
    // 解析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);
  }
}

1.cache节点

        它通过调用CacheBuilder的相应方法完成cache的创建。每个cache内部都有一个唯一的ID,这个id的值就是namespace。创建好的cache对象存入configuration的cache缓存中(该缓存以cache的ID属性即namespace为key,这里再次体现了mybatis的namespace的强大用处)。

/**
 * cache- 配置本定命名空间的缓存。
 *         type- cache实现类,默认为PERPETUAL,可以使用自定义的cache实现类(别名或完整类名皆可)
 *         eviction- 回收算法,默认为LRU,可选的算法有:
 *             LRU– 最近最少使用的:移除最长时间不被使用的对象。
 *             FIFO– 先进先出:按对象进入缓存的顺序来移除它们。
 *             SOFT– 软引用:移除基于垃圾回收器状态和软引用规则的对象。
 *             WEAK– 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
 *         flushInterval- 刷新间隔,默认为1个小时,单位毫秒
 *         size- 缓存大小,默认大小1024,单位为引用数
 *         readOnly- 只读
 * @param context
 * @throws Exception
 */
private void cacheElement(XNode context) throws Exception {
  if (context != null) {
    String type = context.getStringAttribute("type", "PERPETUAL");
    Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
    String eviction = context.getStringAttribute("eviction", "LRU");
    Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
    Long flushInterval = context.getLongAttribute("flushInterval");
    Integer size = context.getIntAttribute("size");
    boolean readWrite = !context.getBooleanAttribute("readOnly", false);
    boolean blocking = context.getBooleanAttribute("blocking", false);
    Properties props = context.getChildrenAsProperties();
    builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
  }
}
public Cache useNewCache(Class<? extends Cache> typeClass,
    Class<? extends Cache> evictionClass,
    Long flushInterval,
    Integer size,
    boolean readWrite,
    boolean blocking,
    Properties props) {
  Cache cache = new CacheBuilder(currentNamespace)
      .implementation(valueOrDefault(typeClass, PerpetualCache.class))
      .addDecorator(valueOrDefault(evictionClass, LruCache.class))
      .clearInterval(flushInterval)
      .size(size)
      .readWrite(readWrite)
      .blocking(blocking)
      .properties(props)
      .build();
  configuration.addCache(cache);
  currentCache = cache;
  return cache;
}

2.cache-ref节点

        cacheRefElement方法负责解析cache-ref元素,它通过调用CacheRefResolver的相应方法完成cache的引用。创建好的cache-ref引用关系存入configuration的cacheRefMap缓存中。

/**
 *  cache-ref–从其他命名空间引用缓存配置。
 *         如果你不想定义自己的cache,可以使用cache-ref引用别的cache。
 *         因为每个cache都以namespace为id,
 *         所以cache-ref只需要配置一个namespace属性就可以了。
 *         需要注意的是,如果cache-ref和cache都配置了,以cache为准。
 * @param context
 */
private void cacheRefElement(XNode context) {
  if (context != null) {
    configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
    CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
    try {
      cacheRefResolver.resolveCacheRef();
    } catch (IncompleteElementException e) {
      configuration.addIncompleteCacheRef(cacheRefResolver);
    }
  }
}

3.resultMap节点

        resultMapElement方法负责解析resultMap元素,它通过调用ResultMapResolver的相应方法完成resultMap的解析。resultMap节点下除了discriminator子节点的其他子节点都会解析成对应的ResultMapping对象,而每个<resultMap>节点都会被解析成一个ResultMap对象,创建好的resultMap存入configuration的resultMaps缓存中(该缓存以namespace+resultMap的id为key,这里再次体现了mybatis的namespace的强大用处)。

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());
  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();
  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);
      }
      resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
    }
  }
  ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
  try {
    return resultMapResolver.resolve();
  } catch (IncompleteElementException  e) {
    configuration.addIncompleteResultMap(resultMapResolver);
    throw e;
  }
}

4.sql节点解析

sql节点用来定义可重用的sql语句片段, sqlElement方法负责解析sql元素。id属性用于区分不同的sql元素,在同一个mapper配置文件中可以配置多个sql元素。

private void sqlElement(List<XNode> list) throws Exception {
  if (configuration.getDatabaseId() != null) {
    sqlElement(list, configuration.getDatabaseId());
  }
  sqlElement(list, null);
}

private void sqlElement(List<XNode> list, String requiredDatabaseId) throws Exception {
  for (XNode context : list) {
    String databaseId = context.getStringAttribute("databaseId");
    String id = context.getStringAttribute("id");
    id = builderAssistant.applyCurrentNamespace(id, false);
    if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
      // 记录到sqlFragments中保存,其实 构造函数中可以看到该字段指向了configuration的sqlFragments集合中
      sqlFragments.put(id, context);
    }
  }
}

private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {
  if (requiredDatabaseId != null) {
    if (!requiredDatabaseId.equals(databaseId)) {
      return false;
    }
  } else {
    if (databaseId != null) {
      return false;
    }
    // skip this fragment if there is a previous one with a not null databaseId
    if (this.sqlFragments.containsKey(id)) {
      XNode context = this.sqlFragments.get(id);
      if (context.getStringAttribute("databaseId") != null) {
        return false;
      }
    }
  }
  return true;
}

二、XMLStatementBuilder

映射配置文件中还有一类比较重要的节点需要解析,其实就是select|insert|update|delete 节点,这些节点主要用于定义SQL语句,他们不在由XMLMapperBuilder进行解析,而是由XMLStatementBuilder负责进行解析,每个节点会被解析成MappedStatement对象并存入到configuration对象中去。在这个方法内有几个重要的步骤,理解他们对正确的配置statement元素很有帮助。

1.MappedStatement

MappedStatement包含了这些节点的很多属性,其中比较重要的如下:

private String resource;//节点中的id 包括命名空间
private SqlSource sqlSource;//SqlSource对象,对应一条SQL语句
private SqlCommandType sqlCommandType;//SQL的类型,insert,delete,select,update

解析过程代码如下:

public void parseStatementNode() {
  // 获取sql节点的id以及databaseId如果和当前不匹配不加载改节点,
  // 如果存在id相同且databaseId不为空的节点也不在加载改节点
  String id = context.getStringAttribute("id");
  String databaseId = context.getStringAttribute("databaseId");

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

  Class<?> resultTypeClass = resolveClass(resultType);
  String resultSetType = context.getStringAttribute("resultSetType");
  StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
  ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);

  // 根据节点的名称设置sqlCommandType的类型
  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);

  // 在解析SQL语句之前先处理include节点
  XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
  includeParser.applyIncludes(context.getNode());

  // 处理selectKey节点
  processSelectKeyNodes(id, parameterTypeClass, langDriver);
  
  // 完成节点的解析 该部分是核心
  SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
  // 获取resultSets keyProperty keyColumn三个属性
  String resultSets = context.getStringAttribute("resultSets");
  String keyProperty = context.getStringAttribute("keyProperty");
  String keyColumn = context.getStringAttribute("keyColumn");
  KeyGenerator keyGenerator;
  // 获取selectKey节点对应的selectKeyGenerator的id
  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;
  }

  // 通过MapperBuilderAssistant创建MappedStatement对象,
  // 并添加到configuration.mappedStatements集合中保存
  builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
      fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
      resultSetTypeEnum, flushCache, useCache, resultOrdered, 
      keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

2、解析include节点

在解析statement节点之前首先通过XMLIncludeTransformer解析include节点改过程会将include节点替换<sql>节点中定义的sql片段,并将其中的${xx}占位符换成真实的参数,

private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
  if (source.getNodeName().equals("include")) {  // ---(2)处理include节点
    // 查找refid属性指向的<sql>,返回的是深克隆的Node对象
    Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
    Properties toIncludeContext = getVariablesContext(source, variablesContext);
    //递归处理include节点
    applyIncludes(toInclude, toIncludeContext, true);
    if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
      toInclude = source.getOwnerDocument().importNode(toInclude, true);
    }
    // 将<include>节点替换<sql>节点
    source.getParentNode().replaceChild(toInclude, source);
    while (toInclude.hasChildNodes()) {
      // 将<sql>节点的子节点添加到<sql>节点的前面
      toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
    }
    // 替换后删除<sql>节点
    toInclude.getParentNode().removeChild(toInclude);
  } else if (source.getNodeType() == Node.ELEMENT_NODE) {// ---(1)
    if (included && !variablesContext.isEmpty()) {
      // replace variables in attribute values
      NamedNodeMap attributes = source.getAttributes();
      for (int i = 0; i < attributes.getLength(); i++) {// 遍历当前sql的子节点
        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
      && !variablesContext.isEmpty()) {// ---(3)
    // replace variables in text node 替换对应的占位符
    source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
  }
}

3、解析selectKey节点

在insert,update节点中可以定义selectKey节点来解决主键自增问题。

private void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId) {
  String resultType = nodeToHandle.getStringAttribute("resultType");
  Class<?> resultTypeClass = resolveClass(resultType);
  StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString()));
  String keyProperty = nodeToHandle.getStringAttribute("keyProperty");
  String keyColumn = nodeToHandle.getStringAttribute("keyColumn");
  boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));

  //defaults
  boolean useCache = false;
  boolean resultOrdered = false;
  KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
  Integer fetchSize = null;
  Integer timeout = null;
  boolean flushCache = false;
  String parameterMap = null;
  String resultMap = null;
  ResultSetType resultSetTypeEnum = null;

  // 生成SqlSource
  SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);
  // selectKey节点中只能配置select语句
  SqlCommandType sqlCommandType = SqlCommandType.SELECT;

  // 创建MappedStatement对象,并添加到configuration的mappedStatements集合中保存
  builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
      fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
      resultSetTypeEnum, flushCache, useCache, resultOrdered,
      keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);

  id = builderAssistant.applyCurrentNamespace(id, false);

  MappedStatement keyStatement = configuration.getMappedStatement(id, false);
  // 创建对应的KeyGenerator(主键自增策略),添加到configuration中
  configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
}

三、绑定Mapper接口

每个映射配置文件的命名空间可以绑定一个Mapper接口,并注册到MapperRegistry中。

// 绑定mapper接口
private void bindMapperForNamespace() {
  //获取映射文件的命名空间
  String namespace = builderAssistant.getCurrentNamespace();
  if (namespace != null) {
    Class<?> boundType = null;
    try {
      // 解析命名空间对应的类型 即dao
      boundType = Resources.classForName(namespace);
    } catch (ClassNotFoundException e) {
      //ignore, bound type is not required
    }
    if (boundType != null) {
      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);
        configuration.addMapper(boundType);
      }
    }
  }
}

猜你喜欢

转载自my.oschina.net/u/3737136/blog/1809796