深入浅出Mybatis源码解析——映射文件加载流程

前言

在前两篇文章中,简单了说了下Mybatis全局配置文件解析加载流程和和全局配置文件的部分标签解析,但是还并没有涉及到核心的解析。所以本篇文章将开始核心解析的部分,那就是mappers标签的解析。可能有人很奇怪这为什么不在前两篇文章中拿出来说,因为mapper标签的解析在configuration标签同层标签解析中是一个比较特殊的部分,因此需要单独拿出来进行说明。

好吧,说了这么多废话,还是进入正题吧!

一、mapper标签解析

由于前两篇文章已经贴了mapper标签解析的入口代码了,这里就不在贴代码了,那我们来看看入口mapperElement方法中是怎么实现解析的:

  private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      // 获取<mappers>标签的子标签
      for (XNode child : parent.getChildren()) {
    	// <package>子标签
        if ("package".equals(child.getName())) {
          // 获取mapper接口和mapper映射文件对应的package包名
          String mapperPackage = child.getStringAttribute("name");
          // 将包下所有的mapper接口以及它的代理对象存储到一个Map集合中,key为mapper接口类型,value为代理对象工厂
          configuration.addMappers(mapperPackage);
        } else {// <mapper>子标签
          // 获取<mapper>子标签的resource属性
          String resource = child.getStringAttribute("resource");
          // 获取<mapper>子标签的url属性
          String url = child.getStringAttribute("url");
          // 获取<mapper>子标签的class属性
          String mapperClass = child.getStringAttribute("class");
          // 它们是互斥的
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            // 专门用来解析mapper映射文件
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            // 通过XMLMapperBuilder解析mapper映射文件
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            // 通过XMLMapperBuilder解析mapper映射文件
            mapperParser.parse();
          } else if (resource == null && url == null && mapperClass != null) {
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            // 将指定mapper接口以及它的代理对象存储到一个Map集合中,key为mapper接口类型,value为代理对象工厂
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }

看完这段代码,可能人要说了,这段代码的逻辑和上一篇文章的代码不是几乎差不多嘛。的确是差不多,但是里的代码是mapper映射文件的入口处,因此这就是它的特殊之处,还有这段代码中涉及到了XMLMapperBuilder对象,而这个对象的作用正是专门用于解析mapper映射文件的。

我们还是来说下这段代码的意思吧,首先通过mappers标签来获得它的字标签,然后判断它下面是否存在package标签(<package name="xx.xx.Mapper"/>),如果存在,则直接进入它的逻辑代码中,然后获取其name属性,获取到后再将其放到configuration对象中。否则则进入其它代码逻辑,当进入反之的代码后,我们可以看到它获取了resource、url、class这三个标签的属性,然后根据这三个属性值的有无来执行相关代码,其实我们会发现当resource或url不为null的时候,都是先通过获取流,然后在调用XMLMapperBuilder的构造方法来进行创建XMLMapperBuilder对象,只有当class不为null的时候,才会通过反射来获取mapperClass对象,然后将其放到configuration对象中。

二、XMLMapperBuilder对象的构造

前面我们说到了这个方法中有XMLMapperBuilder这个对象,还有通过这个对象来调用parse()解析方法,那我们就来看看这个对象的构造方法,代码如下:

  public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
    this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver()),
        configuration, resource, sqlFragments);
  }

这段代码很简单,但还是要进行相关说明,我们在这段代码中看到构造了这样几个对象XPathParser和XMLMapperEntityResolver,那么我们首先来看下XMLMapperEntityResolver这个类的代码:

/**
 * Offline entity resolver for the MyBatis DTDs
 * 
 */
public class XMLMapperEntityResolver implements EntityResolver {

  private static final String IBATIS_CONFIG_SYSTEM = "ibatis-3-config.dtd";
  private static final String IBATIS_MAPPER_SYSTEM = "ibatis-3-mapper.dtd";
  private static final String MYBATIS_CONFIG_SYSTEM = "mybatis-3-config.dtd";
  private static final String MYBATIS_MAPPER_SYSTEM = "mybatis-3-mapper.dtd";

  private static final String MYBATIS_CONFIG_DTD = "org/apache/ibatis/builder/xml/mybatis-3-config.dtd";
  private static final String MYBATIS_MAPPER_DTD = "org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd";


  @Override
  public InputSource resolveEntity(String publicId, String systemId) throws SAXException {
    try {
      if (systemId != null) {
        String lowerCaseSystemId = systemId.toLowerCase(Locale.ENGLISH);
        if (lowerCaseSystemId.contains(MYBATIS_CONFIG_SYSTEM) || lowerCaseSystemId.contains(IBATIS_CONFIG_SYSTEM)) {
          return getInputSource(MYBATIS_CONFIG_DTD, publicId, systemId);
        } else if (lowerCaseSystemId.contains(MYBATIS_MAPPER_SYSTEM) || lowerCaseSystemId.contains(IBATIS_MAPPER_SYSTEM)) {
          return getInputSource(MYBATIS_MAPPER_DTD, publicId, systemId);
        }
      }
      return null;
    } catch (Exception e) {
      throw new SAXException(e.toString());
    }
  }

  private InputSource getInputSource(String path, String publicId, String systemId) {
    InputSource source = null;
    if (path != null) {
      try {
        InputStream in = Resources.getResourceAsStream(path);
        source = new InputSource(in);
        source.setPublicId(publicId);
        source.setSystemId(systemId);
      } catch (IOException e) {
        // ignore, null is ok
      }
    }
    return source;
  }

}

这个类的功用其实MyBatis DTD离线实体解析器,具体代码也不复杂,这里就不多说了,有兴趣的朋友可以自己看看。那么我们来继续看下XPathParser类的代码吧!

三、XPathParser对象的创建流程

  public XPathParser(InputStream inputStream, boolean validation, Properties variables, EntityResolver entityResolver) {
    commonConstructor(validation, variables, entityResolver);
    // 解析XML文档为Document对象
    this.document = createDocument(new InputSource(inputStream));
  }

  private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) {
    this.validation = validation;
    this.entityResolver = entityResolver;
    this.variables = variables;
    XPathFactory factory = XPathFactory.newInstance();
    this.xpath = factory.newXPath();
  }

  private Document createDocument(InputSource inputSource) {
    // important: this must only be called AFTER common constructor
    try {
      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
      // 进行dtd或者Schema校验
      factory.setValidating(validation);

      factory.setNamespaceAware(false);
      // 设置忽略注释为true
      factory.setIgnoringComments(true);
      // 设置是否忽略元素内容中的空白
      factory.setIgnoringElementContentWhitespace(false);
      factory.setCoalescing(false);
      factory.setExpandEntityReferences(true);

      DocumentBuilder builder = factory.newDocumentBuilder();
      builder.setEntityResolver(entityResolver);
      builder.setErrorHandler(new ErrorHandler() {
        @Override
        public void error(SAXParseException exception) throws SAXException {
          throw exception;
        }

        @Override
        public void fatalError(SAXParseException exception) throws SAXException {
          throw exception;
        }

        @Override
        public void warning(SAXParseException exception) throws SAXException {
        }
      });
      // 通过dom解析,获取Document对象
      return builder.parse(inputSource);
    } catch (Exception e) {
      throw new BuilderException("Error creating document instance.  Cause: " + e, e);
    }
  }

上面一口气贴了三个方法的代码,大家不要慌,容我慢慢来细说。在XPathParser对象构建的时候,首先会调用commonConstructor方法,其实这个方法的本质就是个构造函数,在这个构造函数中最主要的就是通过XPathFactory获得了factory这个对象,然后通过factory获得了path。

在调完commonConstructor方法后,便调用了createDocument方法,从传入参数InputSource来看,我们一开始以为在这个方法中会通过流信息来做了什么,但细看代码会发现,前面大部分都和传入的参数无关,我们来看看commonConstructor方法代码的具体步骤:

  1. 首先通过DocumentBuilderFactory获得一个factory实例,然后对factory这个实例进行各种属性的设置。
  2. 当属性设置完成后,又通过factory实例穿件DocumentBuilder对象,在获得这个对象后又对这个对象进行属性设置。
  3. 最后DocumentBuilder这个对象通过传如的流进行解析。

这里我给大家展示下builder.parse(inputSource)方法做了什么,代码如下:

    public Document parse(InputSource is) throws SAXException, IOException {
        if (is == null) {
            throw new IllegalArgumentException(
                DOMMessageFormatter.formatMessage(DOMMessageFormatter.DOM_DOMAIN,
                "jaxp-null-input-source", null));
        }
        if (fSchemaValidator != null) {
            if (fSchemaValidationManager != null) {
                fSchemaValidationManager.reset();
                fUnparsedEntityHandler.reset();
            }
            resetSchemaValidator();
        }
        domParser.parse(is);
        Document doc = domParser.getDocument();
        domParser.dropDocumentReferences();
        return doc;
    }

    public void parse(InputSource inputSource)
        throws SAXException, IOException {

        // parse document
        try {
            XMLInputSource xmlInputSource =
                new XMLInputSource(inputSource.getPublicId(),
                                   inputSource.getSystemId(),
                                   null);
            xmlInputSource.setByteStream(inputSource.getByteStream());
            xmlInputSource.setCharacterStream(inputSource.getCharacterStream());
            xmlInputSource.setEncoding(inputSource.getEncoding());
            parse(xmlInputSource);
        }

        // 此处省略代码

    } 

这里最重要的就是调用domParser.parse(is)方法去解析,然后通过domParser去获取Document对象,再调用dropDocumentReferences方法销毁解析器对Document的引用。最后返回Document对象。

四、解析mapper映射文件

这里所要说的就是最后的XMLMapperBuilder.parse(),我们就直接看parse方法的代码吧!代码如下:


  public void parse() {
	// mapper映射文件是否已经加载过
    if (!configuration.isResourceLoaded(resource)) {
      // 从映射文件中的<mapper>根标签开始解析,直到完整的解析完毕
      configurationElement(parser.evalNode("/mapper"));
      // 标记已经解析
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }

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

代码看上去还是如此的熟悉,首先判断mapper映射文件是否已经加载过,没有则进入逻辑代码进行执行。先来看看 configurationElement(parser.evalNode("/mapper")),代码如下

  /**
   * 解析映射文件
   * @param context 映射文件根节点<mapper>对应的XNode
   */
  private void configurationElement(XNode context) {
    try {
      // 获取<mapper>标签的namespace值,也就是命名空间
      String namespace = context.getStringAttribute("namespace");
      // 命名空间不能为空
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      
      // 设置当前的命名空间为namespace的值
      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>子标签,也就是SQL片段
      sqlElement(context.evalNodes("/mapper/sql"));
      // 解析<select>\<insert>\<update>\<delete>子标签
      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);
    }
  }

这里基本上都是解析标签的方法,其他的标签解析还是相对简单,我们直接看buildStatementFromContext这个方法:

  private void buildStatementFromContext(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
      buildStatementFromContext(list, configuration.getDatabaseId());
    }
    // 构建MappedStatement
    buildStatementFromContext(list, null);
  }

  private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      // MappedStatement解析器
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
    	// 解析select等4个标签,创建MappedStatement对象
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }

上面代码中通过传入的节点list来构建MappedStatement对象,然后通过MappedStatement来解析相关的标签信息。这里XMLStatementBuilder对象的构造代码我们就不看了,因为代码很简单,我们还是来看看核心的parseStatementNode方法:

  /**
   * 解析<select>\<insert>\<update>\<delete>子标签
   */
  public void parseStatementNode() {
	// 获取statement的id属性(特别关键的值)
    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");
    // 别名处理,获取入参对应的Java类型
    Class<?> parameterTypeClass = resolveClass(parameterType);
    // 获取ResultMap
    String resultMap = context.getStringAttribute("resultMap");
    // 获取结果映射类型
    String resultType = context.getStringAttribute("resultType");
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);
    
    // 别名处理,获取返回值对应的Java类型
    Class<?> resultTypeClass = resolveClass(resultType);
    String resultSetType = context.getStringAttribute("resultSetType");
    
    // 设置默认StatementType为Prepared,该参数指定了后面的JDBC处理时,采用哪种Statement
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);

    String nodeName = context.getNode().getNodeName();
    // 解析SQL命令类型是什么?确定操作是CRUD中的哪一种
    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
    // <include>标签解析
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    // Parse selectKey after includes and remove them.
    // 解析<selectKey>标签
    processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    // 创建SqlSource,解析SQL,封装SQL语句(未参数绑定)和入参信息
    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;
    }

    // 通过构建者助手,创建MappedStatement对象
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

这段代码看上去很复杂,其实重要的几个点无非是createSqlSource(创建SqlSource,解析SQL,封装SQL语句(未参数绑定)和入参信息)和addMappedStatement(通过构建者助手,创建MappedStatement对象)。

由于篇幅的原因,今天就先到这里了,夜也已深,童鞋们也都要早点休息。关于SQL的解析和addMappedStatement方法的调用我们后面将再写一篇文章来进行解析。最后我们还是来个简单的总结。

总结

相关类和接口:

  • XMLConfigBuilder
  • XMLMapperBuilder
  • XPathParser
  • MapperBuilderAssistant (下篇文章进行解析)
  • XMLStatementBuilder
  • MappedStatement
 
流程分析:
 
XMLConfigBuilder#mapperElement:解析全局配置文件中的<mappers>标签
  • XMLMapperBuilder#构造方法:专门用来解析映射文件的
    • XPathParser#构造方法:
      • XPathParser#createDocument():创建Mapper映射文件对应的Document对象
      • MapperBuilderAssistant#构造方法:用于构建MappedStatement对象的 (下篇文章进行解析)
  • XMLMapperBuilder#parse()
    • XMLMapperBuilder#configurationElement:专门用来解析mapper映射文件
      • XMLMapperBuilder#buildStatementFromContext:用来创建MappedStatement对象的
        • XMLMapperBuilder#buildStatementFromContext
          • XMLStatementBuilder#构造方法:专门用来解析MappedStatement
          • XMLStatementBuilder#parseStatementNode:
            • MapperBuilderAssistant#addMappedStatement:创建MappedStatement对象(下篇文章进行解析)
              • MappedStatement.Builder#构造方法(下篇文章进行解析)
              • MappedStatement#build方法:创建MappedStatement对象,并存储到Configuration对象中(下篇文章进行解析)
 
发布了41 篇原创文章 · 获赞 8 · 访问量 4253

猜你喜欢

转载自blog.csdn.net/zfy163520/article/details/103122582