mybaits-从XML到Java对象

71 阅读6分钟

从XML到Java对象

涉及的具体的解析可能不太详尽,但是可以自行debug进行回忆,这块代码不是很难!

首先在最初学习MyBatis的时候,我们需要配置一个mybatis-config.xml和若干XXXMapper.xml文件的,这两种形式的文件就构建出了MyBatis中最基础和重要的数据来源。那么现在就有问题了:

  1. 文件是存储在硬盘上的数据源,而程序是运行在内存中的,那么如何将这些文件读取到内存中。
  2. 我们使用Java语言进行程序的开发,在Java中有一个很著名的话就是“万物皆对象”,那么MyBatis是如何将些数据封装Java对象以便在JVM中使用的呢!

围绕这两点我们来讲一下,MyBatis是如何做的。

在使用MyBatis时需要一个SqlSession来获取XXXMapper,或者直接执行SQL操作,但是不管怎么样都得搞个SqlSession出来,而这个SqlSession是由SqlSessionFactory来创建的,所以在每次使用的使用总会有一下几行代码:

				 1. InputStream resourceAsStream =  OriginMybatisTest.class.getClassLoader().getResourceAsStream("mybatis-config.xml");
         2. SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsStream);
         3. SqlSession sqlSession = build.openSession();

其实上面第一个问题的解决很简单,jdk本身乃至MyBatis框架都提供了快速读取文件为字节流或者字符流的方式,上面的代码就是通过类加载器读取一个mybatis-config.xml到字节流中。

当然MyBatis本身也提供了工具类:

InputStream resourseAsStream = Resources.getResourceAsStream("mybatis-config.xml");

解析和封装

那么最关键的就是第二个问题了, 即MyBatis是如何将XML进行解析并且封装中JVM中的一个个对象以供使用的。

XML的解析

一般对于XML的解析有三种方式:DOM、SAX、XPath。

XPath比前两者出现的时间要晚,但是使用起来却更加的简单,而MyBatis采用的也是这种方式,并且MyBatis还对XPath进行了一次封装,因此如果我们开发中有需要可以也可以去使用。

下面看一个XPath的一个简单Demo:

		XPathParser parser = new XPathParser(Resources.getResourceAsReader(OriginMybatisTest.class.getClassLoader(),"mybatis-config.xml"));
		获取/configuration标签下的所有子标签
    XNode configuration = parser.evalNode("configuration");
    for (XNode child : configuration.getChildren()) {
          System.out.println(child);
    }

将每个标签解析成一个XNode对象,依靠这个对象当前节点以及其子节点进行解析并且都会封装成XNode对象。MyBatis对XPath的封装都在org.apache.ibatis.parsing

MyBatis解析mybatis-config.xml

而MyBatis对于mybatis-config.xml的解析发生在:

new SqlSessionFactoryBuilder().build(resourceAsStream);

build方法的入参就是之前获取的xml文件的字节流对象:

图片转存失败,建议将图片保存下来直接上传

对于上图中的第①点很容易解释,看一下它的构造方法就清楚了:

public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
		***1**
    this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
  }

  private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
    **super(new Configuration());**
    ErrorContext.instance().resource("SQL Mapper Configuration");
    this.configuration.setVariables(props);
    this.parsed = false;
    this.environment = environment;
    this.parser = parser;
  }

在*1处,创建了一个XPathParser对象,并且还把inputStream,也就是xml文件的字节流传了进去,这就表明后续就可以同个这个parse文件的解析。并且在这里我们还可以看到一个非常重要的一个对象的初始化,就是Configuration!

重点我们需要关注图中的第二点:

public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
		// 可以看到这里就可以解析根节点并且获取根节点的XNode对象,而“/configuration” config文件中的跟标签了
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }
private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

在parseConfiguration(root)标签中我们就可以很清晰的看到各种等待被解析的标签,这些名称都是在mybatis-config.xml文件中存在的标签。看了一下大部分的标签解析都不难,到时候想看了可以自行去debug,这些解析都发生在XMLConfigBuilder 中。

但是呢还是有必要看一下,对于mapper文件的解析的! 我将对下面的代码进行一下修剪,在else中下面有三个判断条件,大部分情况下我们只会写resource属性,所以下面的两个关于url和class属性的逻辑就先删掉了

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) {
            ErrorContext.instance().resource(resource);
						***1**
            InputStream inputStream = Resources.getResourceAsStream(resource);
						***2**
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
						***3**
            mapperParser.parse();
          }’
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }

在对于mappers标签解析的时候是要获取该标签下的所有**子标签,**并且进行一个遍历,因此就有了大致下面的一个解析流程

以第二种情况,mapper标签中不是package属性进行演示,对于mapper文件的解析和config的解析大同小异,基本上都是:

  1. 获取mapper文件的字节流
  2. 通过字节流构建一个XMLMapperBuilder,内部同样封装了XPathParse,并且据此进行标签的解析
  3. 解析mapper文件中的各个标签信息,封装进MappedStatement对象,并且将该对象放入到Configuration中

前两步都比较简单主要来分析一下第三步, mapperParser.parse()

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
			**真正的解析在这里,去获取XXXMapper.xml中的所有标签**
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingCacheRefs();
    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);
      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);
    }
  }

这里我们就可以看到一些熟悉的字符串了,其中最终要的肯定就是上面标的那一行了。如果再看一下这个代码的话依旧会看到很多熟悉的属性

public void parseStatementNode() {
    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);

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

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

    // Parse selectKey after includes and remove them.
    processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    String resultSets = context.getStringAttribute("resultSets");
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    KeyGenerator keyGenerator;
    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;
    }

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

至此在属性解析的过程中,我们会发现MyBatis的一个手法,就是不管是哪一种属性标签都会为其创建一个XXXBuilderAssistant对象,在创建的时候会将configuration传入,在对应解析方法的最用会依靠这个builderAssistant将解析并且封装好的对象,添加进configuration中去。

比如解析mapper标签的过程中使用的是MapperBuilderAssistant ,然后解析出来的子标签对象都是通过assistant添加进的MappedStatement或者添加进configuration中。

在整个对config.xml解析中,在完成所有标签解析之后,整个大的这个解析也就结束了,最终就会返回一个SqlSessionFactory 并且在后面生产SqlSession进行SQL的使用。而SqlSessionFactory 内部就封装了Configuration进一步封装了MappedStatement