美文网首页
Mybatis源码研读(一)—— XML解析

Mybatis源码研读(一)—— XML解析

作者: 04040d1599e6 | 来源:发表于2020-05-10 23:02 被阅读0次

走进Mybatis

上文中简单的介绍过了Mybatis的使用。本篇文章将介绍Mybatis如何解析XML

组件:

  • SqlSessionFactory
    用于生成SQLSession 的工厂类

  • SqlSession
    Mybatis 的核心API,表示和数据交互的会话,用于处理所有的增删改查功能。

  • Configuration
    Configuration可以说是一个仓库类了。所有的配置项都集中在这个类里面。在SqlSessionFactoryBuilder构建SqlSessionFactory的时候,会创建XMLConfigBuilder,然后会解析到Configuration类,再通过这个Configuration类来构建SqlSessionFactory。所有所有的XML文件信息到最后都是通过Configuration来承载,供SQLSessionFactory创建SQLSession。

public class Configuration {
  protected Environment environment;

  protected boolean safeRowBoundsEnabled;
  protected boolean safeResultHandlerEnabled = true;
  protected boolean mapUnderscoreToCamelCase;
  protected boolean aggressiveLazyLoading;
  protected boolean multipleResultSetsEnabled = true;
  protected boolean useGeneratedKeys;
  protected boolean useColumnLabel = true;
  protected boolean cacheEnabled = true;
  protected boolean callSettersOnNulls;
  protected boolean useActualParamName = true;
  protected boolean returnInstanceForEmptyRow;
  //日志前缀
  protected String logPrefix;
  // 日志接口
  protected Class <? extends Log> logImpl;
  protected Class <? extends VFS> vfsImpl;
  //
  protected LocalCacheScope localCacheScope = LocalCacheScope.SESSION;
  //数据库类型
  protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
 //
  protected Set<String> lazyLoadTriggerMethods = new HashSet<>(Arrays.asList("equals", "clone", "hashCode", "toString"));
  protected Integer defaultStatementTimeout;
  protected Integer defaultFetchSize;
  protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
  protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
  protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;

  protected Properties variables = new Properties();
  protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
  protected ObjectFactory objectFactory = new DefaultObjectFactory();
  protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();

  protected boolean lazyLoadingEnabled = false;
  protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL

  protected String databaseId;
  /**
   * Configuration factory class.
   * Used to create Configuration for loading deserialized unread properties.
   *
   * @see <a href='https://code.google.com/p/mybatis/issues/detail?id=300'>Issue 300 (google code)</a>
   */
  protected Class<?> configurationFactory;

  protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
  protected final InterceptorChain interceptorChain = new InterceptorChain();
  protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry();
  protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
  protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();

  protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection")
      .conflictMessageProducer((savedValue, targetValue) ->
          ". please check " + savedValue.getResource() + " and " + targetValue.getResource());
  protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
  protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Result Maps collection");
  protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection");
  protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<>("Key Generators collection");

  protected final Set<String> loadedResources = new HashSet<>();
  protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers");

  protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<>();
  protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<>();
  protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<>();
  protected final Collection<MethodResolver> incompleteMethods = new LinkedList<>();

  /*
   * A map holds cache-ref relationship. The key is the namespace that
   * references a cache bound to another namespace and the value is the
   * namespace which the actual cache is bound to.
   */
  protected final Map<String, String> cacheRefMap = new HashMap<>();

解析XML的过程,可以理解为就是在填充Configuration的过程。

SqlSessionFactoryBuilder().build(inputStream);

一切的解析都是由build开始。最终会调用到 XMLConfigBuilder.parseConfiguration方法。

  private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(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);
    }
  }

在该方法中对每个节点进行单独的解析。

properties

解析Mapper.xml

在解析XML中最重要的是就是解析Mapper.xml了,也就是代码中的: mapperElement(root.evalNode("mappers"))
其最终会调用到:
XMLMapperBuilder.configurationElement

  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"));
      //解析SQL节点
      sqlElement(context.evalNodes("/mapper/sql"));
      //解析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);
    }
  }
SQL节点

SQL节点可以将将重复的sql提取出来,在使用到的地方使用include医用即可,最终达到SQL重用的目的。它的解析很简单,就是把内容放入sqlFragments容器。id为命名空间+节点ID

  private void sqlElement(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      String databaseId = context.getStringAttribute("databaseId");
      String id = context.getStringAttribute("id");
      id = builderAssistant.applyCurrentNamespace(id, false);
      if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
        sqlFragments.put(id, context);
      }
    }
  }
SQL语句

buildStatementFromContext(context.evalNodes("select|insert|update|delete"))
这一段是Mybatis的核心所在。他会把Mapper.xml里面的SQL语句进行解析。分为两种类型。存在动态标签的会被解析为动态SQL, 不存在动态标签的则被解析为静态SQL

动态标签指的是在SQL里面添加的 <if><choose><foreach><trim><set> 等的标签。
例如:

    <select id="selectByIds" resultType="com.xavier.mybatis.User">
        SELECT * FROM TB_USER
        where id in
        <foreach collection="ids" item="id" index="index" open="(" close=")" separator=",">
            #{id}
        </foreach>
    </select>

在解析SQL的时候,会根据SQL是否有动态标签来确定初始化的SqlSource的类型。动态的为DynamicSqlSource, 静态的为RawSqlSource .

  public SqlSource parseScriptNode() {
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource;
    if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }

我们来看一下parseDynamicTags方法:

    MixedSqlNode rootSqlNode = parseDynamicTags(context);

如其名,他是解析SQL语句的,并且返回MixedSqlNode
MixedSqlNode 的结构如下。它包含了一个SqlNode的List

public class MixedSqlNode implements SqlNode {
  private final List<SqlNode> contents;
}

接着看它解析过程:

  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));
      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 {
        //如果为静态属性,则添加 __StaticTextSqlNode__
          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);
  }

可以看到,这就是对子节点的处理。

  1. 判断节点的内容是不是纯文本。如果是纯文本就构建静态对象StaticTextSqlNode
  2. 如果是动态标签,则获取节点类型,然后交给对应的类型处理。

为每一个动态标签绑定不同的处理器在:
XMLScriptBuilder.initNodeHandlerMap

  private void initNodeHandlerMap() {
    nodeHandlerMap.put("trim", new TrimHandler());
    nodeHandlerMap.put("where", new WhereHandler());
    nodeHandlerMap.put("set", new SetHandler());
    nodeHandlerMap.put("foreach", new ForEachHandler());
    nodeHandlerMap.put("if", new IfHandler());
    nodeHandlerMap.put("choose", new ChooseHandler());
    nodeHandlerMap.put("when", new IfHandler());
    nodeHandlerMap.put("otherwise", new OtherwiseHandler());
    nodeHandlerMap.put("bind", new BindHandler());
  }

我们分析一下foreach的Handler:

private class ForEachHandler implements NodeHandler {
    public ForEachHandler() {
      // Prevent Synthetic Access
    }

    @Override
    public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {
      MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
      String collection = nodeToHandle.getStringAttribute("collection");
      String item = nodeToHandle.getStringAttribute("item");
      String index = nodeToHandle.getStringAttribute("index");
      String open = nodeToHandle.getStringAttribute("open");
      String close = nodeToHandle.getStringAttribute("close");
      String separator = nodeToHandle.getStringAttribute("separator");
      ForEachSqlNode forEachSqlNode = new ForEachSqlNode(configuration, mixedSqlNode, collection, index, item, open, close, separator);
      targetContents.add(forEachSqlNode);
    }
  }

ForEachHandler的处理方式,

  1. 递归调用parseDynamicTags(),继续处理自己的节点
  2. 获取自己定义的各种参数: collection, item , index, open ,close ……
  3. 构建ForEachSqlNode对象,然后把它加入到 targetContents

其他类型的处理器大致如此。

所以,我们最后解析获取到的MixedSqlNode对象就是一棵以树,其中的contents是这棵树的第一级子节点。

在初始化好了SqlSource之后,Mybatis会将SqlSource连同入参,出参,等等一系列参数封装为一个MappedStatement,然后注册到configuration这个大管家类中:

    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
        .resource(resource)
        .fetchSize(fetchSize)
        .timeout(timeout)
        .statementType(statementType)
        .keyGenerator(keyGenerator)
        .keyProperty(keyProperty)
        .keyColumn(keyColumn)
        .databaseId(databaseId)
        .lang(lang)
        .resultOrdered(resultOrdered)
        .resultSets(resultSets)
        .resultMaps(getStatementResultMaps(resultMap, resultType, id))
        .resultSetType(resultSetType)
        .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
        .useCache(valueOrDefault(useCache, isSelect))
        .cache(currentCache);

    ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    if (statementParameterMap != null) {
      statementBuilder.parameterMap(statementParameterMap);
    }

    MappedStatement statement = statementBuilder.build();
    configuration.addMappedStatement(statement);

在大管家中的mappedStatements 属性是一个StrictMap<MappedStatement> ,所以存入的时候,key 为唯一识别这个Statement的东西,在mybatis中是由 namespace+mappper中节点的id,在我们案例中就是:com.xavier.mybatis.UserMapper.selectUser

相关文章

网友评论

      本文标题:Mybatis源码研读(一)—— XML解析

      本文链接:https://www.haomeiwen.com/subject/ohabbqtx.html