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

前言

接上一篇博客,解析核心配置文件的流程还剩两块。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<ResultMap> 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源码中文注释

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值