Mybatis源码之mapper解析

config文件mapper的配置方式有如下几种


    
    

因为指定xml的解析方式更加直接,代码更加清晰,所以先以xml配置的方式调试源码,后面再说包解析和class解析。

一个简单的mapper.xml文件,内容如下


    

其中namespace不能为空,对应此mapper的Dao接口,子标签的id对应相应的方法

mapper文件的解析在初始化 configuration 的时候,XMLConfigBuilder.parseConfiguration() 方法内调用mapperElement() 方法完成。如下

private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
        // package
      if ("package".equals(child.getName())) {
        String mapperPackage = child.getStringAttribute("name");
        configuration.addMappers(mapperPackage);
      } else {
        String resource = child.getStringAttribute("resource");
        String url = child.getStringAttribute("url");
        String mapperClass = child.getStringAttribute("class");
        if (resource != null && url == null && mapperClass == null) {
         //resource
          ErrorContext.instance().resource(resource);
          InputStream inputStream = Resources.getResourceAsStream(resource);
          XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
          mapperParser.parse();
        } else if (resource == null && url != null && mapperClass == null) {
            // url
          ErrorContext.instance().resource(url);
          InputStream inputStream = Resources.getUrlAsStream(url);
          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.");
        }
      }
    }
  }
}

XMLMapperBuilder用来解析mapper,parse 方法

// XMLMapperBuilder#parse()
public void parse() {
  if (!configuration.isResourceLoaded(resource)) {
    configurationElement(parser.evalNode("/mapper"));
    configuration.addLoadedResource(resource);
    bindMapperForNamespace();
  }

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

资源未加载,解析 mapper 节点

configurationElement(parser.evalNode("/mapper"));

private void configurationElement(XNode context) {
  try {
      // 必填,对应的java接口
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.equals("")) {
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    builderAssistant.setCurrentNamespace(namespace);
      // 参照缓存
    cacheRefElement(context.evalNode("cache-ref"));
      // 自定义缓存,
    cacheElement(context.evalNode("cache"));
      // parameterMap 已废弃
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      // resultMap
    resultMapElements(context.evalNodes("/mapper/resultMap"));
      // 可被其他语句引用的可重用语句块
    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 解析完毕后放在 configuration 的 caches 集合中
  2. resultMap 元素是 MyBatis 中最重要最强大的元素。它可以让你从 90% 的 JDBC ResultSets 数据提取代码中解放出来, 并在一些情形下允许你做一些 JDBC 不支持的事情。 放在 configuration 的 resultMaps 集合中
  3. sql 解析完毕后放在 configuration 的 sqlFragments 集合中

下面重点解析Statement节点,解析Statement节点,保存到sqlFragments 中,解析方法为XMLStatementBuilder.parseStatementNode(),步骤如下

  1. 解析statementNode属性
    属性有id、fetchSize、timeout、parameterMap、parameterType、resultMap、statementType (默认PREPARED),默认非select查询flushCache为true、select查询useCache为true

  2. 解析include 子节点
    将子节点的include节点替换为sql语句,然后移除include节点

    includeParser.applyIncludes(context.getNode());
    
  3. 解析selectKey

    processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
  4. 解析sql,创建sqlSource

    // langDriver 默认为XMLLanguageDriver
    langDriver.createSqlSource(configuration, context, parameterTypeClass);
    

    createSqlSource方法调用XMLScriptBuilder.parseScriptNode()方法返回SqlSource对象,

    public SqlSource parseScriptNode() {
        // 解析动态节点
        MixedSqlNode rootSqlNode = parseDynamicTags(context);
        SqlSource sqlSource = null;
        if (isDynamic) {
            sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
        } else {
            sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
        }
        return sqlSource;
    }
    

    parseDynamicTags方法如下

    protected MixedSqlNode parseDynamicTags(XNode node) {
      List contents = new ArrayList();
      NodeList children = node.getNode().getChildNodes();
      for (int i = 0; i < children.getLength(); i++) {
        XNode child = node.newXNode(children.item(i));
        if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
          String data = child.getStringBody("");
          TextSqlNode textSqlNode = new TextSqlNode(data);
            // ${} 为动态节点
          if (textSqlNode.isDynamic()) {
            contents.add(textSqlNode);
            isDynamic = true;
          } else {
            contents.add(new StaticTextSqlNode(data));
          }
        } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
          String nodeName = child.getNode().getNodeName();
          NodeHandler handler = nodeHandlerMap.get(nodeName);
          if (handler == null) {
            throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
          }
          handler.handleNode(child, contents);
          isDynamic = true;
        }
      }
      return new MixedSqlNode(contents);
    }
    

    如果statement节点下不存在子节点,但是文本中包含${},那么认为是动态节点;如果statement节点下存在子节点,如trim,if,where,那么statement也动态节点。 最后返回new MixedSqlNode(contents)

    最后,如果包含动态节点返回DynamicSqlSource对象,否则返回RawSqlSource对象,SqlSource构造函数中会进行占位符替换成?

  5. 创建KeyGenerator

  6. 创建MappedStatement 并添加到configuration.mappedStatements中

configuration.addLoadedResource(resource);

添加已解析资源路径

bindMapperForNamespace();

根据namespace寻找相应的mapper类,添加已加载资源和mapper类

boundType = Resources.classForName(namespace);
configuration.addLoadedResource("namespace:" + namespace);
configuration.addMapper(boundType);

到此一个Mapper解析已经完毕,以上是以xml配置的方式解析mapper,在使用package方式配置mapper的时候,需要对mapper文件的名称及位置需要进行特别处理,就是包扫描的是根据dao接口所在文件夹配置的,默认会根据该文件下的接口,去寻找资源路径下 mapper.xml 文件,寻找的路径和文件名是和接口一致的。举个例子:

在 maven 项目中,如果我配置的是

该路径下有个BlogDao.java 接口,那我需要在resource文件夹下有com/zero/mybatis/dao/BlogDao.xml,正确配置上。如图

Mybatis源码之mapper解析_第1张图片
image

下面源码解释,还是XMLConfigBuilder.parseConfiguration() 方法

if (resource != null && url == null && mapperClass == null) {
  ErrorContext.instance().resource(resource);
  InputStream inputStream = Resources.getResourceAsStream(resource);
  XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
  mapperParser.parse();
}

方法调用会进入到MapperRegistry.addMapper方法,这个方法是根据配置包下的class文件,进行mapper注册,配置class也是使用此方法进行注册

// MapperRegistry.addMapper
public  void addMapper(Class type) {
    if (type.isInterface()) {
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
          // 代理工厂,用来生成dao的代理
        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.
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
          // 这里解析
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

MapperAnnotationBuilder.loadXmlResource()方法根据class寻找mapper文件

public void parse() {
    //...
    loadXmlResource();
    //...
}
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
    if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
        // 这里就是对应加载的xml mapper文件,可以看到是类全名替换成路径 + .xml
        String xmlResource = type.getName().replace('.', '/') + ".xml";
        InputStream inputStream = null;
        try {
            inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
        } catch (IOException e) {
            // ignore, resource is not required
        }
        if (inputStream != null) {
            XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
            xmlParser.parse();
        }
    }
}

xmlResource 可以看到是类全名替换成路径 + .xml,解析方式和xml配置的解析方式一致。到此Mapper解析完毕。

你可能感兴趣的:(Mybatis源码之mapper解析)