mybatis源码分析(1)-初始化过程

背景

  • 按照项目的配置文件配置文件地址开始分析源码, 从SqlSessionFactoryBean开始分析。
  • 如果配置了mapperScanner,会对每个mapper逐一调用SqlSessionFactoryBean对应的方法。

本篇文章分析时序图

mybatis初始化过程.jpg

分析SqlSessionFactoryBean

  1. SqlSessionFactoryBean实现了InitializingBean,这个接口的afterPropertiesSet()会在初始化时被调用,所以直接看SqlSessionFactoryBean的afterPropertiesSet()方法。
public void afterPropertiesSet() throws Exception {
   //在配置文件里面设置的属性
    notNull(dataSource, "Property 'dataSource' is required");
   //这个是类的属性,sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();创建
    notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");

    this.sqlSessionFactory = buildSqlSessionFactory();
  }
  1. 调用自身的buildSqlSessionFactory()方法
protected SqlSessionFactory buildSqlSessionFactory() throws IOException {

    Configuration configuration;

    XMLConfigBuilder xmlConfigBuilder = null;
    if (this.configLocation != null) {
      /**
      * configLocation对应了mybatis的configuration属性
      * 以及底下的settings  typeAliases的属性设置
      * /
      xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
      configuration = xmlConfigBuilder.getConfiguration();
    } else {
      ......
    }
    //......省略一些属性的配置

    if (xmlConfigBuilder != null) {
      try {
        //重点步骤1 解析mybatis configuration配置的属性
        xmlConfigBuilder.parse();
       ......
      } catch (Exception ex) {
        throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex);
      } finally {
        ErrorContext.instance().reset();
      }
    }
    configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource));

    if (!isEmpty(this.mapperLocations)) {
      for (Resource mapperLocation : this.mapperLocations) {
        if (mapperLocation == null) {
          continue;
        }

        try {
          //重点步骤2 解析mapper.xml配置的属性
          XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
              configuration, mapperLocation.toString(), configuration.getSqlFragments());
          xmlMapperBuilder.parse();
        } catch (Exception e) {
          throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
        } finally {
          ErrorContext.instance().reset();
        }
        ......
      }
    } else {
       ......
    }
    //通过SqlSessionFactoryBuilder构建SqlSessionFactory
    return this.sqlSessionFactoryBuilder.build(configuration);
  }

XmlConfigBuilder

  • xmlConfigBuilder.parse()此方法是重点步骤1 解析mybatis configuration配置的属性 像什么settings,typeAliases, plugins, mappers,并设置到全局的configuration属性上去。
public Configuration parse() {
    //被解析过了就抛出异常
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    //解析configuration XPathParser已经做了些基础层的工作
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
 }
  • 继续往parseConfiguration()方法看,此方法是解析myabtis配置文件各个节点,代码贴出来,各个方法名称也比较容易知晓含义
 private void parseConfiguration(XNode root) {
    try {
      Properties settings = settingsAsPropertiess(root.evalNode("settings"));
      //issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      loadCustomVfs(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectionFactoryElement(root.evalNode("reflectionFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      /**
      * 本项目没有在mybatis里面配置mappers属性,而是在   
      *sqlSessionFactoryBean里面配置,所以这里不生效
      */
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

XMLMapperBuilder

  • xmlMapperBuilder.parse()方法是重点步骤3 这里负责解析映射配置文件
 public void parse() {
    //判断是否载入过resource
    if (!configuration.isResourceLoaded(resource)) {
      //主流程加载mapper
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      //注册mapper
      bindMapperForNamespace();
    }
    //处理configurationElement解析失败的属性
    parsePendingResultMaps();
    //处理configurationElement解析失败的属性
    parsePendingChacheRefs();
    //处理configurationElement解析失败的sql语句
    parsePendingStatements();
  }
  • 主流程加载mapper
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);
      //以下均是接下对应节点,比如这个是解析cacha-ref
      cacheRefElement(context.evalNode("cache-ref"));
      //这个是解析cache mybats二级缓存 
      cacheElement(context.evalNode("cache"));
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      sqlElement(context.evalNodes("/mapper/sql"));
      //解析select insert update delete重点关注 这个流程跟sql执行主流程相关
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
    }
  }
  • 比较重要的分析下,解析cache mybats二级缓存,最后把cache通过builderAssistant放到Configuration的cache属性,cache属性是Map, key是Cache对应的属性id,id记录了二级缓存开启的sql。 Map caches = new StrictMap("Caches collection"); StrictMap是内部类检测到重复key会抛出异常
private void cacheElement(XNode context) throws Exception {
    if (context != null) {
      //获取节点的 type属性,默认位是 PERPETUAL
      String type = context.getStringAttribute("type", "PERPETUAL");
      Class typeClass = typeAliasRegistry.resolveAlias(type);
      //获取节点的 eviction 属性,默认位是 LRU
      String eviction = context.getStringAttribute("eviction", "LRU");
      //解析 eviction 属,性指定的 Cache 装饰器类型
      Class 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();
      //通过 MapperBuilderAssistant 创建 Cache 对象,并添加到 Configuration . caches 集合中保存
      builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
  }
  • 还有一些resultMap的解析 这个后续再分析。
  • 解析sql代码这里需要重点关注,涉及sql执行的流程,buildStatementFromContext(context.evalNodes("select|insert|update|delete"))
private void buildStatementFromContext(List list, String requiredDatabaseId) {
    for (XNode context : list) {
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }

XMLStatementBuilder

  • XMLStatementBuilder使用的数据结构

MappedStatement

  • 后续执行sql的主流程会用到
//节点中的id属性 包括前缀
private String resource;
//SqlSource 对象,对应一条 SQL 语句
private SqlSource sqlSource;
// 枚举类型 UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
 private SqlCommandType sqlCommandType;

SqlSourc(构造方法由Configuration和SqlNode来的 )

public interface SqlSource {
  /** 
   * getBoundSql ()方法会根据映射文件或注解描述的 SQL 语句,
   * 以及传入的参敛,返回可执行的 SQL
   */
  BoundSql getBoundSql(Object parameterObject);
}
  • 重要的流程分析,涉及sql执行的主流程 parseStatementNode()方法
public void parseStatementNode() {
    /** 获取SQL节点的 id 以及 database Id 属性 ,若其 databaseid
     * 属性值与当前使用的数据库不匹配,则不加载该 SQL 节点;
     * 若存在相同 id 且databaseid不为空的 SQL 节点则不再加载该SQL节点(略)
    */
    /** 获取SQL节,点的多种属性例如,fetchSize,timeout,parameterType,
     * parameterMap ,resultMap等,具体实现比较简单,不在贴出来了。这些属性的具体含义
    * 在 MyBatis 官方文档中已经有比较详细的介绍了,这里不再赘述
    */
    //根据SQL节点的名称决定其 SqlCommandType
    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));

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

    // 处理selectKey节点
    processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
    //完成SQL节点的解析,该部分是parseStatementNode方法的核心后面单独分析
    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))
          ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
    }

    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }
  • 接下来是调用
    LanguageDriver.createSqlSource()方法创建对应 的 SqISource 对象 , 最后创建 Mapped Statement 对
    象,井添加到 Configuration .mappedStatements 集合中保存。

LanguageDriver接口

  • 通过LanguageDriver.createSqlSource方法创建SqlSource对象
SqlSource createSqlSource(Configuration configuration, XNode script, Class parameterType);

XMLLanguageDriver

//创建SqlSource
public SqlSource createSqlSource(Configuration configuration, XNode script, Class parameterType) {
    XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
    return builder.parseScriptNode();
  }

XMLScriptBuilder

  • 数据结构

SqlNode

public interface SqlNode {
  //最后通过传入的sql参数由动态sql解析出可被mysql执行的sql
  boolean apply(DynamicContext context);
}
  • MixedSqlNode, IfSqlNode, TextSqlNode, StaticTextSqlNode都是其实现类
  • xmlScriptBuilder.parseScriptNode()方法创建SqlSource, 此方法运用了组合模式,是组合模式加入元素的部分
 public SqlSource parseScriptNode() {
    /**首先判断当前的节点是不是有动态 SQL ,动态 SQL 会包括占位符
     *或是动态 SQL 的相关节点
     */
    List contents = parseDynamicTags(context);
    // SqlNode集合包装成一个MixedSqlNode 
    MixedSqlNode rootSqlNode = new MixedSqlNode(contents);
    SqlSource sqlSource = null;
    //根据是否是动态 SQL,创建相应的SqlSource对象
    if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }
  • xmlScriptBuilder.parseDynamicTags()方法, 解析出组合模式的初始化属性, 这里对应含有各自if trim等元素的动态sql树
List parseDynamicTags(XNode node) {
    List contents = new ArrayList();
    //获取子节点, 比如select 底下有if什么的一层一层的
    NodeList children = node.getNode().getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
      //创建 XNode ,该过程会将能解析掉的${}都解析掉
      XNode child = node.newXNode(children.item(i));
      //文本节点 TextSqlNode StaticTextSqlNode则直接添加
      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
        //如果是其他比如if trim这类节点一定是动态sql
        //通过Handler处理其他动态节点
        String nodeName = child.getNode().getNodeName();
        //初始化内部类NodeHandler
        NodeHandler handler = nodeHandlers(nodeName);
        if (handler == null) {
          throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
        }
        //处理动态 SQL ,并将解析得到 的 SqlNode 对象放入 contents 集合中保存
        handler.handleNode(child, contents);
        isDynamic = true;
      }
    }
    return contents;
  }

NodeHandler

  • nodeHandlers()
NodeHandler nodeHandlers(String nodeName) {
    Map map = new HashMap();
    map.put("trim", new TrimHandler());
    map.put("where", new WhereHandler());
    map.put("set", new SetHandler());
    map.put("foreach", new ForEachHandler());
    map.put("if", new IfHandler());
    map.put("choose", new ChooseHandler());
    map.put("when", new IfHandler());
    map.put("otherwise", new OtherwiseHandler());
    map.put("bind", new BindHandler());
    return map.get(nodeName);
  }

  • handleNode()将处理动态 SQL ,并将解析得到 的 SqlNode 对象放入 contents 集合中保存,使用IfHandlerNode举例
 public void handleNode(XNode nodeToHandle, List targetContents) {
      //递归调用parseDynamicTags,直到TextSqlNode为递归出口
      List contents = parseDynamicTags(nodeToHandle);
      MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
      String test = nodeToHandle.getStringAttribute("test");
      //创建 IfSqlNode其中也是多个SqlNode
      IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);
      targetContents.add(ifSqlNode);
    }
  • 目前核心流程已经走完
  1. XMLMapperBuilder.parse()中调用bindMapperForNamespace(); 完成了映射配置文件与对应 Mapper 接
    口的绑定
 private void bindMapperForNamespace() {
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
      Class boundType = null;
      try {
        //命名空间具体绑定的类
        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
          configuration.addMapper(boundType);
        }
      }
    }
  }

IncompleteElementException

  • XMLMapperBuilder.configurationElement()方法解析映射配置文件时,是按照从文件头到文件尾的顺序解析的,但是有时候在解析一个节点时, 会引用定义在该节点之后的、还未解析的节点,这就会导致解析失败井抛出IncompleteElementException 。
  • 根据抛出异常的节点不同, MyBatis 会创建不同 *Resolver 对象, 井添加到 Configuration的不同 incomplete*集合中.
    对异常部分感兴趣的可以参考mybatis技术内幕》第三章
  • 参考自《mybatis技术内幕》第三章
  • mybatis3中文文档

你可能感兴趣的:(mybatis源码分析(1)-初始化过程)