Mybatis源码阅读(一):Mybatis初始化1.3 —— 解析sql片段和sql节点

*************************************优雅的分割线 **********************************

分享一波:程序员赚外快-必看的巅峰干货

如果以上内容对你觉得有用,并想获取更多的赚钱方式和免费的技术教程

请关注微信公众号:HB荷包
在这里插入图片描述
一个能让你学习技术和赚钱方法的公众号,持续更新

前言

接上一篇博客,解析核心配置文件的流程还剩两块。Mybatis初始化1.2 —— 解析别名、插件、对象工厂、反射工具箱、环境

本想着只是两个模块,随便写写就完事,没想到内容还不少,加上最近几天事情比较多,就没怎么更新,几天抽空编写剩下两块代码。
解析sql片段

sql节点配置在Mapper.xml文件中,用于配置一些常用的sql片段。

/**
 * 解析sql节点。
 * sql节点用于定义一些常用的sql片段
 * @param list
 */
private void sqlElement(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
        sqlElement(list, configuration.getDatabaseId());
    }
    sqlElement(list, null);
}

/**
 * 解析sql节点
 * @param list sql节点集合
 * @param requiredDatabaseId 当前配置的databaseId
 */
private void sqlElement(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
        // 获取databaseId和id属性
        String databaseId = context.getStringAttribute("databaseId");
        // 这里的id指定的是命名空间
        String id = context.getStringAttribute("id");
        // 启用当前的命名空间
        id = builderAssistant.applyCurrentNamespace(id, false);
        if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
            // 如果该节点指定的databaseId是当前配置中的,就启用该节点的sql片段
            sqlFragments.put(id, context);
        }
    }
}

这里面,SQLFragments用于存放sql片段。在存放sql片段之前,会先调用databaseIdMatchesCurrent方法去校验该片段的databaseId是否为当前启用的databaseId

/**
 * 判断databaseId是否是当前启用的
 * @param id 命名空间id
 * @param databaseId 待匹配的databaseId
 * @param requiredDatabaseId 当前启用的databaseId
 * @return
 */
private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {
    if (requiredDatabaseId != null) {
        return requiredDatabaseId.equals(databaseId);
    }
    if (databaseId != null) {
        return false;
    }
    if (!this.sqlFragments.containsKey(id)) {
        return true;
    }
    // skip this fragment if there is a previous one with a not null databaseId
    XNode context = this.sqlFragments.get(id);
    return context.getStringAttribute("databaseId") == null;
}

解析sql片段的步骤就这么简单,下面是解析sql节点的代码。
解析sql节点

在XxxMapper.xml中存在诸多的sql节点,大体分为select、insert、delete、update节点(此外还有selectKey节点等,后面会进行介绍)。每一个sql节点最终会被解析成MappedStatement。

/**

  • 表示映射文件中的sql节点

  • select、update、insert、delete节点

  • 该节点中包含了id、返回值、sql等属性

  • @author Clinton Begin
    */
    public final class MappedStatement {

    /**

    • 包含命名空间的节点id
      /
      private String resource;
      private Configuration configuration;
      /
      *
    • 节点id
      /
      private String id;
      private Integer fetchSize;
      private Integer timeout;
      /
      *
    • STATEMENT 表示简单的sql,不包含动态的
    • PREPARED 表示预编译sql,包含#{}
    • CALLABLE 调用存储过程
      */
      private StatementType statementType;
      private ResultSetType resultSetType;

    /**

    • 节点或者注解中编写的sql
      /
      private SqlSource sqlSource;
      private Cache cache;
      private ParameterMap parameterMap;
      private List resultMaps;
      private boolean flushCacheRequired;
      private boolean useCache;
      private boolean resultOrdered;
      /
      *
    • sql的类型。select、update、insert、delete
      */
      private SqlCommandType sqlCommandType;
      private KeyGenerator keyGenerator;
      private String[] keyProperties;
      private String[] keyColumns;
      private boolean hasNestedResultMaps;
      private String databaseId;
      private Log statementLog;
      private LanguageDriver lang;
      private String[] resultSets;
      }

处理sql节点

/**
 * 处理sql节点
 * 这里的Statement单词后面会经常遇到
 * 一个MappedStatement表示一条sql语句
 * @param list
 */
private void buildStatementFromContext(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
        buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
}

/**
 * 启用当前databaseId的sql语句节点
 * @param list
 * @param requiredDatabaseId
 */
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
        final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
        try {
            // 解析sql节点
            statementParser.parseStatementNode();
        } catch (IncompleteElementException e) {
            configuration.addIncompleteStatement(statementParser);
        }
    }
}

在parseStatementNode方法中,只会启用当前databaseId的sql节点(如果没配置就全部启用)

/**
 * 解析sql节点
 */
public void parseStatementNode() {
    // 当前节点id
    String id = context.getStringAttribute("id");
    // 获取数据库id
    String databaseId = context.getStringAttribute("databaseId");
    // 启用的数据库和sql节点配置的不同
    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
        return;
    }
    // 获取当前节点的名称
    String nodeName = context.getNode().getNodeName();
    // 获取到sql的类型。select|update|delete|insert
    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和selectKey节点
    ......

}

在该方法中,会依次处理include节点、selectKey节点、最后获取到当前sql节点的各个属性,去创建MappedStatement对象,并添加到Configuration中。

/**
 * 解析sql节点
 */
public void parseStatementNode() {
    // 在上面已经进行了注释
    ......
    // 解析sql前先处理include节点。
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    // 获取parameterType属性
    String parameterType = context.getStringAttribute("parameterType");
    // 直接拿到parameterType对应的Class
    Class<?> parameterTypeClass = resolveClass(parameterType);
    // 获取到lang属性
    String lang = context.getStringAttribute("lang");
    // 获取对应的动态sql语言驱动器。
    LanguageDriver langDriver = getLanguageDriver(lang);

    // 解析selectKey节点
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

}

解析parameterType和lang属性比较简单,这里只看解析include和selectKey
解析include节点

/**
 * 启用include节点
 *
 * @param source
 */
public void applyIncludes(Node source) {
    Properties variablesContext = new Properties();
    Properties configurationVariables = configuration.getVariables();
    Optional.ofNullable(configurationVariables).ifPresent(variablesContext::putAll);
    applyIncludes(source, variablesContext, false);
}

在applyIncludes方法中,会调用它的重载方法,递归去处理所有的include节点。include节点中,可能会存在${}占位符,在这步,也会将该占位符给替换成实际意义的字符串。接着,include节点会被处理成sql节点,并将sql节点中的sql语句取出放到节点之前,最后删除sql节点。最终select等节点会被解析成带有动态sql的节点。

/**
 * 递归去处理所有的include节点.
 *
 * @param source           include节点
 * @param variablesContext 当前所有的配置
 */
private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
    if (source.getNodeName().equals("include")) {
        // 获取到refid并从配置中拿到sql片段
        Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
        // 解析include节点下的Properties节点,并替换value对应的占位符,将name和value键值对形式存放到variableContext
        Properties toIncludeContext = getVariablesContext(source, variablesContext);
        // 递归处理,在sql节点中可能会使用到include节点
        applyIncludes(toInclude, toIncludeContext, true);
        if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
            toInclude = source.getOwnerDocument().importNode(toInclude, true);
        }
        // 将include节点替换成sql节点
        source.getParentNode().replaceChild(toInclude, source);
        while (toInclude.hasChildNodes()) {
            // 如果还有子节点,就添加到sql节点前面
            // 在上面的代码中,sql节点已经不可能再有子节点了
            // 这里的子节点就是文本节点(具体的sql语句)
            toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
        }
        // 删除sql节点
        toInclude.getParentNode().removeChild(toInclude);
    } else if (source.getNodeType() == Node.ELEMENT_NODE) {
        if (included && !variablesContext.isEmpty()) {
            NamedNodeMap attributes = source.getAttributes();
            for (int i = 0; i < attributes.getLength(); i++) {
                Node attr = attributes.item(i);
                attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
            }
        }
        // 获取所有的子节点
        NodeList children = source.getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
            // 解析include节点
            applyIncludes(children.item(i), variablesContext, included);
        }
    } else if (included && (source.getNodeType() == Node.TEXT_NODE || source.getNodeType() == Node.CDATA_SECTION_NODE)
            && !variablesContext.isEmpty()) {
        // 使用之前解析到的Properties对象替换对应的占位符
        source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
    }
}

第一行代码的含义是根据include节点的refid属性去获取到对应的sql片段,代码比较简单

/**
 * 根据refid查找sql片段
 * @param refid
 * @param variables
 * @return
 */
private Node findSqlFragment(String refid, Properties variables) {
    // 替换占位符
    refid = PropertyParser.parse(refid, variables);
    // 将refid前面拼接命名空间
    refid = builderAssistant.applyCurrentNamespace(refid, true);
    try {
        // 从Configuration中查找对应的sql片段
        XNode nodeToInclude = configuration.getSqlFragments().get(refid);
        return nodeToInclude.getNode().cloneNode(true);
    } catch (IllegalArgumentException e) {
        throw new IncompleteElementException("Could not find SQL statement to include with refid '" + refid + "'", e);
    }
}

到这里,include节点就会被替换成有实际意义的sql语句。
解析selectKey节点

当数据表中主键设计为自增,可能会存在业务需要在插入后获取到主键,这时候就需要使用selectKey节点。processSelectKeyNodes方法用于解析selectKey节点。该方法会先获取到该sql节点所有的selectKey节点,遍历去解析,解析完毕后删除selectKey节点。

/**
 * 解析selectKey节点
 * selectKey节点可以解决insert时主键自增问题
 * 如果需要在插入数据后获取到主键,就需要使用selectKey节点
 *
 * @param id                 sql节点的id
 * @param parameterTypeClass 参数类型
 * @param langDriver         动态sql语言驱动器
 */
private void processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver) {
    // 获取全部的selectKey节点
    List<XNode> selectKeyNodes = context.evalNodes("selectKey");
    if (configuration.getDatabaseId() != null) {
        parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());
    }
    parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);
    removeSelectKeyNodes(selectKeyNodes);
}

删除selectKey节点的代码比较简单,这里就不贴了,重点看parseSelectKeyNodes方法。

该方法负责遍历获取到的所有selectKey节点,只启用当前databaseId对应的节点(这里的逻辑和sql片段那里一样,如果开发者没有配置databaseId,就全部启用)

/**
 * 解析selectKey节点
 *
 * @param parentId             父节点id(指sql节点的id)
 * @param list                 所有的selectKey节点
 * @param parameterTypeClass   参数类型
 * @param langDriver           动态sql驱动
 * @param skRequiredDatabaseId 数据源id
 */
private void parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId) {
    // 遍历selectKey节点
    for (XNode nodeToHandle : list) {
        // 拼接id 修改为形如 findById!selectKey形式
        String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
        // 获得当前节点的databaseId属性
        String databaseId = nodeToHandle.getStringAttribute("databaseId");
        // 只解析databaseId是当前启用databaseId的节点
        if (databaseIdMatchesCurrent(id, databaseId, skRequiredDatabaseId)) {
            parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, langDriver, databaseId);
        }
    }
}

在for循环中,会逐个调用parseSelectKeyNode方法去解析selectKey节点。代码看似复杂其实很简单,最终selectKey节点也会被解析成MappedStatement对象

/**
 * 解析selectKey节点
 *
 * @param id                 节点id
 * @param nodeToHandle       selectKey节点
 * @param parameterTypeClass 参数类型
 * @param langDriver         动态sql驱动
 * @param databaseId         数据库id
 */
private void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId) {
    // 获取 resultType 属性
    String resultType = nodeToHandle.getStringAttribute("resultType");
    // 解析返回值类型
    Class<?> resultTypeClass = resolveClass(resultType);
    // 解析statementType(sql类型,简单sql、动态sql、存储过程)
    StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    // 获取keyProperty和keyColumn属性
    String keyProperty = nodeToHandle.getStringAttribute("keyProperty");
    String keyColumn = nodeToHandle.getStringAttribute("keyColumn");
    // 是在之前还是之后去获取主键
    boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));

    // 设置MappedStatement对象需要的一系列属性默认值
    boolean useCache = false;
    boolean resultOrdered = false;
    KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
    Integer fetchSize = null;
    Integer timeout = null;
    boolean flushCache = false;
    String parameterMap = null;
    String resultMap = null;
    ResultSetType resultSetTypeEnum = null;

    // 生成sqlSource
    SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);
    // selectKey节点只能配置select语句
    SqlCommandType sqlCommandType = SqlCommandType.SELECT;

    // 用这么一大坨东西去创建MappedStatement对象并添加到Configuration中
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
            fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
            resultSetTypeEnum, flushCache, useCache, resultOrdered,
            keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);

    // 启用当前命名空间(给id前面加上命名空间)
    id = builderAssistant.applyCurrentNamespace(id, false);
    // 从Configuration中拿到上面的MappedStatement
    MappedStatement keyStatement = configuration.getMappedStatement(id, false);
    configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
}

至此,selectKey节点已经被解析完毕并删除掉了,其余代码就是负责解析其他属性并将该sql节点创建为MappedStatement对象。

    KeyGenerator keyGenerator;
    // 拼接id。形如findById!selectKey
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    // 给这个id前面追加当前的命名空间
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
        keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
        // 优先取配置的useGeneratorKeys。如果为空就判断当前配置是否允许jdbc自动生成主键,并且当前是insert语句
        // 判断如果为真就创建Jdbc3KeyGenerator,如果为假就创建NoKeyGenerator
        keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
                configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
                ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }

    // 获取当前sql节点的一堆属性,去创建MappedStatement。
    // 这里创建的MappedStatement就代表一个sql节点
    // 也是后面编写mybatis拦截器时可以拦截的一处
    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);

结语

在看本博客时,可能会觉得比较吃力,这里建议结合代码去阅读。事实上这三篇博客的阅读和编写的过程中,对应的mybatis代码都比较容易,结合代码阅读起来并没有多大难度。最后贴一下我的码云地址(别问为什么是github,卡的一批)

mybatis源码中文注释

*************************************优雅的分割线 **********************************

分享一波:程序员赚外快-必看的巅峰干货

如果以上内容对你觉得有用,并想获取更多的赚钱方式和免费的技术教程

请关注微信公众号:HB荷包
在这里插入图片描述
一个能让你学习技术和赚钱方法的公众号,持续更新

posted @ 2020-12-19 16:51  游在空中的鱼  阅读(114)  评论(0编辑  收藏  举报