mybatis 读取mapper文件 (mybatis通过xml运行mapper)

首先要解析Mapper.xml文件需要,在configuration中配置mapper文件的路径。配置的路径可以是resouce、url或者是class、package 。配置为 resouce、url 以流资源的方式解析mapper文件,class、package 另一种方式,通过MapperProxy代理来完成。

XmlConfigBuilder 类中mapperElement方法就是以不同的方式解析:

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        //扫描包下面的类
        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);
            try(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);
            try(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.");
          }
        }
      }
    }
  }

Mybatis 以流资源方式解析mapper.xml文件

通俗一点的意思就是:直接获取 mapper.xml文件进行解析。

XmlMapperBuilder 中的 parse 方法实现解析:

第一步解析Mapper标签:

public void parse() {
  //判断资源是否以及被加载过了
    if (!configuration.isResourceLoaded(resource)) {
      // 解析Mapper 标签
      configurationElement(parser.evalNode("/mapper"));
      //将资源路径加载到configuration的成员中 ,防止重复加载mapper 
      //resource 全路径名
      configuration.addLoadedResource(resource);
      // 将mapper 绑定到 命名空间绑定
      bindMapperForNamespace();
    }
	//resultMap 、CacheRef、statement  绑定并且解析
    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }

第二步解析Mapper标签中的子标签:

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");
      }
      //MapperBuilderAssistant对象设置命名空间,
      //bindMapperForNamespace()方法中会用到
      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"));
      // curd操作
      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);
    }
  }

第三部 查看Mapper子标签主要生成的对象:

select|insert|update|delete 标签 生成 XMLStatementBuilder对象进行封装;

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) {
      //为每个 crud 标签创建一个 XMLStatementBuilder对象
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }

XMLStatementBuilder对象调用parseStatementNode()方法:

public void parseStatementNode() {
  //获取节点 key 
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }

  //节点名称
    String nodeName = context.getNode().getNodeName();
  // SqlCommandType 数据类型是 UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    //是否是查询
   boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
  //缓存刷新策略 flushCache 设置值 以设置的值为依据。
  //flushCache 没有设置值 ,查询返回FALSE,非查询true
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
  //是否使用缓存 useCache 设置值 以设置的值为准
  //useCache没有设置值,查询返回TRUE,非查询返回FALSE
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
  //设置仅针对嵌套结果 select 语句:如果为 true,将会假设包含了嵌套结果集或是分组,
  //当返回一个主结果行时,就不会产生对前面结果集的引用。
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // Include Fragments before parsing
  // 分析是否有 SQL片段引用
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

  //入参数据类型
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);
   //指定特定语言, 可以使用 Apache Velocity 作为动态语言,
  //更多细节请参考 MyBatis-Velocity 项目。
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    // Parse selectKey after includes and remove them.
  // 引用sql片段 解析后 解析 selectKey ,然后删除这两个
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    KeyGenerator keyGenerator;
  // id + !selectKey 
    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;
    }
		// 创建SqlSource 
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    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");

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

langDriver.createSqlSource(configuration, context, parameterTypeClass)方法创建SqlSocre。createSqlSource方法生成XMLScriptBuilder对象。

mybatis根据xml生成mapper,mybatis通过xml运行mapper

XMLScriptBuilder调用parseScriptNode方法生成SQL。

 public SqlSource parseScriptNode() {
   //该方法中包含是否是动态SQL的判断
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    if (isDynamic) {
      //动态SQL
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      //静态SQL
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }

解析标签并且判断SQL类型(动态|静态)。

 protected MixedSqlNode parseDynamicTags(XNode node) {
    List<SqlNode> contents = new ArrayList<>();
    NodeList children = node.getNode().getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      XNode child = node.newXNode(children.item(i));
      // CDATA  TEXT 
      if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
        String data = child.getStringBody("");
        TextSqlNode textSqlNode = new TextSqlNode(data);
        // 判断 SQL语句中是否有  ${}  GenericTokenParser.createParser
        if (textSqlNode.isDynamic()) {
          //动态SQL
          contents.add(textSqlNode);
          isDynamic = true;
        } else {
          //静态SQL
          contents.add(new StaticTextSqlNode(data));
        }
      } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
        //当前是元素节点
        String nodeName = child.getNode().getNodeName();
        // nodeHandlerMap集合中查看是否有对应节点,
        //XMLScriptBuilder构造函数初始时加进集合的
        NodeHandler handler = nodeHandlerMap.get(nodeName);
        if (handler == null) {
          throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
        }
        //node节点处理器,会递归调用,直到没有内层标签
        handler.handleNode(child, contents);
        //包含 trim|if|choose|set|where|foreach|when|otherwise|bind 都是动态标签
        isDynamic = true;
      }
    }
    return new MixedSqlNode(contents);
  }

小结:

动态SQL:DynamicSqlSource 只要xml编写的SQL语句中包含 ${}形式的入参,trim|if|where|foreach|set|when|otherwise|bind|choose

静态SQL: RawSqlSource 静态SQL,SQL结构不会变化,只是入参会不同而已。

MixedSqlNode:private final List<SqlNode> contents 本质,就是将xml中的SQL语句根 据标签进行拆分,组装成SqlNode的集合。

SqlSource: 就是 DynamicSqlSource 或者 RawSqlSource其中的一种,而这两个SqlSource的成员中带着 mixedSqlNode对象。本质上SqlSource也是一个SqlNode集合。

Mybatis以Mapper接口方式解析xml文件

就给我一个接口,还要我解析XML文件。xml文件在哪里呀?一定有规则否则,如何加载的到哪?

使用mapper接口进行注册:

XMLConfigBuilder中mapperElement方法调用configuration.addMapper方法。

mybatis根据xml生成mapper,mybatis通过xml运行mapper

给mapperRegistry成员属性中添加值。

mybatis根据xml生成mapper,mybatis通过xml运行mapper

MapperRegister中使用addMapper方法,给成员属性knowMapppers添加一个数据,MapperProxyFactory的接口对象。

public <T> void addMapper(Class<T> type) {
  if (type.isInterface()) {
    if (hasMapper(type)) {
      throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
    }
    boolean loadCompleted = false;
    try {
      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 调用 Parse方法,方法中有一个loadXmlResource()。

mybatis根据xml生成mapper,mybatis通过xml运行mapper

public void parse() {
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {
      loadXmlResource();
      configuration.addLoadedResource(resource);
      assistant.setCurrentNamespace(type.getName());
      parseCache();
      parseCacheRef();
      for (Method method : type.getMethods()) {
        if (!canHaveStatement(method)) {
          continue;
        }
        if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
            && method.getAnnotation(ResultMap.class) == null) {
          //解析resultmap 
          parseResultMap(method);
        }
        try {
          //解析一个方法生成对应的MapperedStatement对象
          parseStatement(method);
        } catch (IncompleteElementException e) {
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
    }
  //解析方法 (挂起的)
    parsePendingMethods();
  }

loadXmlResource方法中,把接口 全限定名 替换成文件路径,以及xml结尾。以此为路径查找mapper.xml文件资源。

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())) {
      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());
        xmlParser.parse();
      }
    }
  }

小结:

以文件包或者接口的形式,首先给接口生成一个MapperProxyFactory对象,其次再根据接口的全限定名更改为路径再添加xml,以此来查找mapper.xml文件资源。

总之一句话,用接口全限定名找mapper配置文件。