Mybatis源码分析——Mapper映射的解析过程

这篇文章具体来看看mapper.xml的解析过程

mappers配置方式

mappers 标签下有许多 mapper 标签,每一个 mapper 标签中配置的都是一个独立的映射配置文件的路径,配置方式有以下几种。

接口信息进行配置


    
    
    

注意:这种方式必须保证接口名(例如UserMapper)和xml名(UserMapper.xml)相同,还必须在同一个包中。因为是通过获取mapper中的class属性,拼接上.xml来读取UserMapper.xml,如果xml文件名不同或者不在同一个包中是无法读取到xml的。

相对路径进行配置


    
    
    

注意:这种方式不用保证同接口同包同名。但是要保证xml中的namespase和对应的接口名相同。

绝对路径进行配置


    
    
    

接口所在包进行配置


    

这种方式和第一种方式要求一致,保证接口名(例如UserMapper)和xml名(UserMapper.xml)相同,还必须在同一个包中。

注意:以上所有的配置都要保证xml中的namespase和对应的接口名相同。
我们以packae属性为例详细分析一下:

mappers解析入口方法

接上一篇文章最后部分,我们来看看mapperElement方法:

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            //包扫描的形式
            if ("package".equals(child.getName())) {
                // 获取  节点中的 name 属性
                String mapperPackage = child.getStringAttribute("name");
                // 从指定包中查找 所有的 mapper 接口,并根据 mapper 接口解析映射配置
                configuration.addMappers(mapperPackage);
            } else {
                //读取中的mapper/userDao-mapping.xml,即resource = "mapper/userDao-mapping.xml"
                String resource = child.getStringAttribute("resource");
                //读取mapper节点的url属性
                String url = child.getStringAttribute("url");
                //读取mapper节点的class属性
                String mapperClass = child.getStringAttribute("class");
                
                // resource 不为空,且其他两者为空,则从指定路径中加载配置
                if (resource != null && url == null && mapperClass == null) {
                    //根据rusource加载mapper文件
                    ErrorContext.instance().resource(resource);
                    //读取文件字节流
                    InputStream inputStream = Resources.getResourceAsStream(resource);
                    //实例化mapper解析器
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                    // 解析映射文件
                    mapperParser.parse();
                    
                // url 不为空,且其他两者为空,则通过 url 加载配置
                } else if (resource == null && url != null && mapperClass == null) {
                    //从网络url资源加载mapper文件
                    ErrorContext.instance().resource(url);
                    InputStream inputStream = Resources.getUrlAsStream(url);
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
                    
                    // 解析映射文件
                    mapperParser.parse();
                    
                // mapperClass 不为空,且其他两者为空,则通过 mapperClass 解析映射配置   
                } else if (resource == null && url == null && mapperClass != null) {
                    //使用mapperClass加载文件
                    Class mapperInterface = Resources.classForName(mapperClass);
                    configuration.addMapper(mapperInterface);
                } else {
                    //resource,url,mapperClass三种配置方法只能使用其中的一种,否则就报错
                    throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
                }
            }
        }
    }
}

在 MyBatis 中,共有四种加载映射文件或信息的方式。

  • 1、从文件系统中加载映射文件。
  • 2、通过 URL 的方式加载和解析映射文件。
  • 3、通过 mapper 接口加载映射信息,映射信息可以配置在注解中,也可以配置在映射文件中。
  • 4、最后一种是通过包扫描的方式获取到某个包下的所有类,并使用第三种方式为每个类解析映射信息。

我们先看下以packae扫描的形式,看下configuration.addMappers(mapperPackage)方法

public void addMappers(String packageName) {
    mapperRegistry.addMappers(packageName);
}

我们看一下MapperRegistry的addMappers方法:

public void addMappers(String packageName) {
    //传入包名和Object.class类型
    addMappers(packageName, Object.class);
}


public void addMappers(String packageName, Class superType) {
    ResolverUtil> resolverUtil = new ResolverUtil>();
    
    /*
     * 查找包下的父类为 Object.class 的类。
     * 查找完成后,查找结果将会被缓存到resolverUtil的内部集合中。上一篇文章我们已经看过这部分的源码,不再累述了
     */ 
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    // 获取查找结果
    Set>> mapperSet = resolverUtil.getClasses();
    for (Class mapperClass : mapperSet) {
        //这个方法后面重点讲
        addMapper(mapperClass);
    }
}

其实就是通过 VFS(虚拟文件系统)获取指定包下的所有文件的Class,也就是所有的Mapper接口,然后遍历每个Mapper接口进行解析,接下来就和第一种配置方式(接口信息进行配置)一样的流程了,接下来我们来看看 基于 XML 的映射文件的解析过程,可以看到先创建一个XMLMapperBuilder,再调用其parse()方法,跟进mapperParser.parse():

public void parse() {
    // 检测映射文件是否已经被解析过
    if (!configuration.isResourceLoaded(resource)) {
        // 解析 mapper 节点
        configurationElement(parser.evalNode("/mapper"));
        // 添加资源路径到“已解析资源集合”中
        configuration.addLoadedResource(resource);
        // 通过命名空间绑定 Mapper 接口
        bindMapperForNamespace();
    }

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

该方法重点关注第5行和第9行的逻辑,也就是configurationElement和bindMapperForNamespace方法。

解析映射文件

在 MyBatis 映射文件中,可以配置多种节点。比如 以及 select * from WHERE id = #{id}

接着来看看configurationElement解析mapper.xml中的内容。


public class XMLMapperBuilder extends BaseBuilder {

    private final XPathParser parser;
    private final MapperBuilderAssistant builderAssistant;
  
    private void configurationElement(XNode context) {
        try {
            // 获取 mapper 命名空间,如 mapper.UserMapper
            String namespace = context.getStringAttribute("namespace");
            if (namespace == null || namespace.equals("")) {
                throw new BuilderException("Mapper's namespace cannot be empty");
            }
            // 设置命名空间到 builderAssistant 中
            builderAssistant.setCurrentNamespace(namespace);
            
            // 解析  节点
            cacheRefElement(context.evalNode("cache-ref"));
            
            // 解析  节点
            cacheElement(context.evalNode("cache"));
            
            // 已废弃配置,这里不做分析
            parameterMapElement(context.evalNodes("/mapper/parameterMap"));
            
            // 解析  节点
            resultMapElements(context.evalNodes("/mapper/resultMap"));
            
            // 解析  节点
            sqlElement(context.evalNodes("/mapper/sql"));
            
            // 解析 
    SELECT * FROM  WHERE id = #{id}

下面分析一下 sql 节点的解析过程, 如下:
sqlElement(context.evalNodes("/mapper/sql"))

public class XMLMapperBuilder extends BaseBuilder {

    private void sqlElement(List list) throws Exception {
        if (configuration.getDatabaseId() != null) {
            // 调用 sqlElement 解析  节点
            sqlElement(list, configuration.getDatabaseId());
        }
        
        // 再次调用 sqlElement,不同的是,这次调用,该方法的第二个参数为 null
        sqlElement(list, null);
    }
    
    private void sqlElement(List list, String requiredDatabaseId) throws Exception {
        for (XNode context : list) {
            // 获取 id 和 databaseId 属性
            String databaseId = context.getStringAttribute("databaseId");
            String id = context.getStringAttribute("id");
            
            // id = currentNamespace + "." + id
            id = builderAssistant.applyCurrentNamespace(id, false);
            
            // 检测当前 databaseId 和 requiredDatabaseId 是否一致
            if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
                // 将  键值对缓存到XMLMapperBuilder对象的 sqlFragments 属性中,以供后面的sql语句使用
                sqlFragments.put(id, context);
            }
        }
    }
}
解析select|insert|update|delete节点

节点名称为 select String nodeName = context.getNode().getNodeName(); // 根据节点名称解析 SqlCommandType 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 // 解析 节点 XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant); includeParser.applyIncludes(context.getNode()); // Parse selectKey after includes and remove them. processSelectKeyNodes(id, parameterTypeClass, langDriver); // Parse the SQL (pre: and were parsed and removed) // 解析 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 对象,并将该对象存储到 Configuration 的 mappedStatements 集合中 */ builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets); } }

我们主要来分析下面几个重要的方法:

  • 1、解析 节点
  • 2、解析 SQL,获取 SqlSource
  • 3、构建 MappedStatement 实例
解析 节点

先来看一个include的例子


    
        user
    

    

节点的解析逻辑封装在 applyIncludes 中,该方法的代码如下:
includeParser.applyIncludes(context.getNode())

public class XMLIncludeTransformer {

    public void applyIncludes(Node source) {
        Properties variablesContext = new Properties();
        Properties configurationVariables = configuration.getVariables();
        if (configurationVariables != null) {
            // 将 configurationVariables 中的数据添加到 variablesContext 中
            variablesContext.putAll(configurationVariables);
        }
        // 调用重载方法处理  节点
        applyIncludes(source, variablesContext, false);
    }
}

继续看 applyIncludes 方法

public class XMLIncludeTransformer {

    private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
        // 第一个条件分支
        if (source.getNodeName().equals("include")) {
            //获取  节点。
            Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
            Properties toIncludeContext = getVariablesContext(source, variablesContext);
            applyIncludes(toInclude, toIncludeContext, true);
            if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
                toInclude = source.getOwnerDocument().importNode(toInclude, true);
            }
            
            // 将  节点,节点类型:ELEMENT_NODE,此时会进入第二个分支,获取到获取  节点中的当前节点替换成  节点,然后调用toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);,将  中的内容插入到  节点之前,也就是将user插入到  节点之前,现在不需要  节点了,最后将该节点从 dom 中移除。

创建SqlSource

创建SqlSource在createSqlSource方法中。
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass)

public class XMLLanguageDriver implements LanguageDriver {

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

public class XMLScriptBuilder extends BaseBuilder {

    private final XNode context;
    private boolean isDynamic;
    private final Class parameterType;
    
    // -☆- XMLScriptBuilder
    public SqlSource parseScriptNode() {
        // 解析 SQL 语句节点
        MixedSqlNode rootSqlNode = parseDynamicTags(context);
        SqlSource sqlSource = null;
        // 根据 isDynamic 状态创建不同的 SqlSource
        if (isDynamic) {
            sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
        } else {
            sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
        }
        return sqlSource;
    }
}

继续跟进parseDynamicTags

public class XMLScriptBuilder extends BaseBuilder {

    private final XNode context;
    private boolean isDynamic;
    private final Class parameterType;
    
    /** 
     * 该方法用于初始化 nodeHandlerMap 集合,该集合后面会用到
     */
    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());
    }
    
    protected MixedSqlNode parseDynamicTags(XNode node) {
        List contents = new ArrayList();
        NodeList children = node.getNode().getChildNodes();
        // 遍历子节点
        for (int i = 0; i < children.getLength(); i++) {
            XNode child = node.newXNode(children.item(i));
            //如果节点是TEXT_NODE类型
            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
                    isDynamic = true;
                } else {
                    // 创建 StaticTextSqlNode
                    contents.add(new StaticTextSqlNode(data));
                }
                
            // child 节点是 ELEMENT_NODE 类型,比如  等
            } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628
                // 获取节点名称,比如 if、where、trim 等
                String nodeName = child.getNode().getNodeName();
                // 根据节点名称获取 NodeHandler,也就是上面注册的nodeHandlerMap
                NodeHandler handler = nodeHandlerMap.get(nodeName);
                if (handler == null) {
                    throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
                }
                // 处理 child 节点,生成相应的 SqlNode
                handler.handleNode(child, contents);
                // 设置 isDynamic 为 true
                isDynamic = true;
            }
        }
        return new MixedSqlNode(contents);
    }
}

对于if、trim、where等这些动态节点,是通过对应的handler来解析的,如下

handler.handleNode(child, contents);

该代码用于处理动态 SQL 节点,并生成相应的 SqlNode。下面来简单分析一下 WhereHandler 的代码。

public class XMLScriptBuilder extends BaseBuilder {

    private interface NodeHandler {
        void handleNode(XNode nodeToHandle, List targetContents);
    }
    
    private class WhereHandler implements NodeHandler {
        public WhereHandler() {
            // Prevent Synthetic Access
        }

        @Override
        public void handleNode(XNode nodeToHandle, List targetContents) {
            // 调用 parseDynamicTags 解析  节点
            MixedSqlNode mixedSqlNode = parseDynamicTags(nodeToHandle);
            // 创建 WhereSqlNode
            WhereSqlNode where = new WhereSqlNode(configuration, mixedSqlNode);
            // 添加到 targetContents
            targetContents.add(where);
        }
    }
}

我们已经将 XML 配置解析了 SqlSource,下面我们看看MappedStatement的构建。

构建MappedStatement

SQL 语句节点可以定义很多属性,这些属性和属性值最终存储在 MappedStatement 中。

public class MapperBuilderAssistant extends BaseBuilder {

    public MappedStatement addMappedStatement(
        String id,
        SqlSource sqlSource,
        StatementType statementType,
        SqlCommandType sqlCommandType,
        Integer fetchSize,
        Integer timeout,
        String parameterMap,
        Class parameterType,
        String resultMap,
        Class resultType,
        ResultSetType resultSetType,
        boolean flushCache,
        boolean useCache,
        boolean resultOrdered,
        KeyGenerator keyGenerator,
        String keyProperty,
        String keyColumn,
        String databaseId,
        LanguageDriver lang,
        String resultSets) {

        if (unresolvedCacheRef) {
            throw new IncompleteElementException("Cache-ref not yet resolved");
        }
        
        // 拼接上命名空间,如