正文

上一篇讲解到mapperElement方法用来解析mapper,我们这篇文章具体来看看mapper.xml的解析过程

mappers配置方式

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

接口信息进行配置

  1. <mappers>
  2. <mapper class="org.mybatis.mappers.UserMapper"/>
  3. <mapper class="org.mybatis.mappers.ProductMapper"/>
  4. <mapper class="org.mybatis.mappers.ManagerMapper"/>
  5. </mappers>

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

相对路径进行配置

<mappers>
    <mapper resource="org/mybatis/mappers/UserMapper.xml"/>
    <mapper resource="org/mybatis/mappers/ProductMapper.xml"/>
    <mapper resource="org/mybatis/mappers/ManagerMapper.xml"/>
</mappers>

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

绝对路径进行配置

<mappers>
    <mapper url="file:///var/mappers/UserMapper.xml"/>
    <mapper url="file:///var/mappers/ProductMapper.xml"/>
    <mapper url="file:///var/mappers/ManagerMapper.xml"/>
</mappers>

接口所在包进行配置

<mappers>
    <package name="org.mybatis.mappers"/>
</mappers>

这种方式和第一种方式要求一致,保证接口名(例如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())) {
                // 获取 <package> 节点中的 name 属性
                String mapperPackage = child.getStringAttribute("name");
                // 从指定包中查找 所有的 mapper 接口,并根据 mapper 接口解析映射配置
                configuration.addMappers(mapperPackage);
            } else {
                // 获取 resource/url/class 等属性
                String resource = child.getStringAttribute("resource");
                String url = child.getStringAttribute("url");
                String mapperClass = child.getStringAttribute("class");

                // resource 不为空,且其他两者为空,则从指定路径中加载配置
                if (resource != null && url == null && mapperClass == null) {
                    ErrorContext.instance().resource(resource);
                    InputStream inputStream = Resources.getResourceAsStream(resource);
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                    // 解析映射文件
                    mapperParser.parse();
                // url 不为空,且其他两者为空,则通过 url 加载配置
                } else if (resource == null && url != null && mapperClass == null) {
                    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) {
                    Class<?> mapperInterface = Resources.classForName(mapperClass);
                    configuration.addMapper(mapperInterface);
                } else {
                    throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
                }
            }
        }
    }
}

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

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

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

我们看一下MapperRegistry的addMappers方法:

 1 public void addMappers(String packageName) {
 2     //传入包名和Object.class类型
 3     this.addMappers(packageName, Object.class);
 4 }
 5 
 6 public void addMappers(String packageName, Class<?> superType) {
 7     ResolverUtil<Class<?>> resolverUtil = new ResolverUtil();
 8     /*
 9      * 查找包下的父类为 Object.class 的类。
10      * 查找完成后,查找结果将会被缓存到resolverUtil的内部集合中。上一篇文章我们已经看过这部分的源码,不再累述了
11      */ 
12     resolverUtil.find(new IsA(superType), packageName);
13     // 获取查找结果
14     Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
15     Iterator i$ = mapperSet.iterator();
16 
17     while(i$.hasNext()) {
18         Class<?> mapperClass = (Class)i$.next();
19         //我们具体看这个方法
20         this.addMapper(mapperClass);
21     }
22 
23 }

    public <T> void addMapper(Class<T> type) {
        // mapper必须是接口!才会添加
        if (type.isInterface()) {
            if (hasMapper(type)) {
                // 如果重复添加了,报错
                throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
            }
            boolean loadCompleted = false;
            try {
                knownMappers.put(type, new MapperProxyFactory<T>(type));
                // It's important that the type is added before the parser is
                // run
                // otherwise the binding may automatically be attempted by the
                // mapper parser. If the type is already known, it won't try.
                MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
                // 从这里先通过接口全类名,找到映射文件
                // 再解析映射文件
                parser.parse();
                loadCompleted = true;
            } finally {
                // 如果加载过程中出现异常需要再将这个mapper从mybatis中删除,这种方式比较丑陋吧,难道是不得已而为之?
                if (!loadCompleted) {
                    knownMappers.remove(type);
                }
            }
        }
    }
    public void parse() {
        // Class对象的唯一标识,如:
        // TopicMapper对应的字符串为"interface com.lixin.mapper.TopicMapper"
        String resource = type.toString();
        // 如果当前Class对象已经解析过,则不在解析
        if (!configuration.isResourceLoaded(resource)) {
            // 加载并解析指定的xml配置文件,Class所在的包对应文件路径,Class类名对应文件名称,如:
            // com.lixin.mapper.TopicMapper类对应的配置文件为com/lixin/mapper/TopicMapper.xml
            loadXmlResource();
            // 把Class对应的标识添加到已加载的资源列表中
            configuration.addLoadedResource(resource);
            // 设置当前namespace为接口Class的全限定名
            assistant.setCurrentNamespace(type.getName());
            // 解析缓存对象
            parseCache();
            // 解析缓存引用,会覆盖之前解析的缓存对象
            parseCacheRef();
            // 获取所有方法,解析方法上的注解,生成MappedStatement和ResultMap
            Method[] methods = type.getMethods();
            // 遍历所有获取到的方法
            for (Method method : methods) {
                try {
                    // issue #237
                    if (!method.isBridge()) {
                        // 解析一个方法生成对应的MapperedStatement对象
                        // 并添加到配置对象中
                        parseStatement(method);
                    }
                } catch (IncompleteElementException e) {
                    configuration.addIncompleteMethod(new MethodResolver(this, method));
                }
            }
        }
        // 解析挂起的方法
        parsePendingMethods();
    }
    private void loadXmlResource() {
        // Spring may not know the real resource name so we check a flag
        // to prevent loading again a resource twice
        // this flag is set at XMLMapperBuilder#bindMapperForNamespace
        // 如果已加载资源列表中指定key已存在,则不再解析xml文件
        // 资源名称为namepace:全限定名
        if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
            // 根据Class对象生成xml配置文件路径
            String xmlResource = type.getName().replace('.', '/') + ".xml";
            InputStream inputStream = null;
            try {
                // 获取文件字节流
                inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
            } catch (IOException e) {
                // ignore, resource is not required
                // 资源没找到,忽略异常情况
            }
            // 如果xml文件存在,则创建XMLMapperBuilder进行解析
            if (inputStream != null) {
                XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(),
                        xmlResource, configuration.getSqlFragments(), type.getName());
                // 从这里进入解析映射文件的流程
                xmlParser.parse();
            }
        }
    }

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

 1 public void parse() {
 2     // 检测映射文件是否已经被解析过
 3     if (!configuration.isResourceLoaded(resource)) {
 4         // 解析 mapper 节点
 5         configurationElement(parser.evalNode("/mapper"));
 6         // 添加资源路径到“已解析资源集合”中
 7         configuration.addLoadedResource(resource);
 8         // 通过命名空间绑定 Mapper 接口
 9         bindMapperForNamespace();
10     }
11 
12     parsePendingResultMaps();
13     parsePendingCacheRefs();
14     parsePendingStatements();
15 }

我们重点关注第5行和第9行的逻辑,也就是configurationElementbindMapperForNamespace方法

解析映射文件

在 MyBatis 映射文件中,可以配置多种节点。比如 以及 以及 等节点统称为 SQL 语句节点,其解析过程在buildStatementFromContext方法中: java private void buildStatementFromContext(List<XNode> list) { if (configuration.getDatabaseId() != null) { // 调用重载方法构建 Statement buildStatementFromContext(list, configuration.getDatabaseId()); } buildStatementFromContext(list, null); } private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) { for (XNode context : list) { // 创建 XMLStatementBuilder 建造类 final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId); try { /* * 解析sql语句节点,将其封装到 Statement 对象中 * 并将解析结果存储到 configuration 的 mappedStatements 集合中 */ statementParser.parseStatementNode(); } catch (IncompleteElementException e) { configuration.addIncompleteStatement(statementParser); } } } 我们继续看 statementParser.parseStatementNode(); java public void parseStatementNode() { // 获取 id 和 databaseId 属性 String id = context.getStringAttribute("id"); String databaseId = context.getStringAttribute("databaseId"); if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) { return; } // 获取各种属性 Integer fetchSize = context.getIntAttribute("fetchSize"); Integer timeout = context.getIntAttribute("timeout"); String parameterMap = context.getStringAttribute("parameterMap"); String parameterType = context.getStringAttribute("parameterType"); Class<?> parameterTypeClass = resolveClass(parameterType); String resultMap = context.getStringAttribute("resultMap"); String resultType = context.getStringAttribute("resultType"); String lang = context.getStringAttribute("lang"); LanguageDriver langDriver = getLanguageDriver(lang); // 通过别名解析 resultType 对应的类型 Class<?> resultTypeClass = resolveClass(resultType); String resultSetType = context.getStringAttribute("resultSetType"); // 解析 Statement 类型,默认为 PREPARED StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString())); // 解析 ResultSetType ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType); // 获取节点的名称,比如 <select> 节点名称为 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> 节点 XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant); includeParser.applyIncludes(context.getNode()); processSelectKeyNodes(id, parameterTypeClass, langDriver); // 解析 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的例子 ```xml user

<select id="findOne" resultType="User">
    SELECT  * FROM  <include refid="table"/> WHERE id = #{id}
</select>


<include> 节点的解析逻辑封装在 applyIncludes 中,该方法的代码如下:

```java
public void applyIncludes(Node source) {
    Properties variablesContext = new Properties();
    Properties configurationVariables = configuration.getVariables();
    if (configurationVariables != null) {
        // 将 configurationVariables 中的数据添加到 variablesContext 中
        variablesContext.putAll(configurationVariables);
    }

    // 调用重载方法处理 <include> 节点
    applyIncludes(source, variablesContext, false);
}

继续看 applyIncludes 方法

private void applyIncludes(Node source, final Properties variablesContext, boolean included) {

    // 第一个条件分支
    if (source.getNodeName().equals("include")) {

        //获取 <sql> 节点。
        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);
        }
        // 将 <select>节点中的 <include> 节点替换为 <sql> 节点
        source.getParentNode().replaceChild(toInclude, source);
        while (toInclude.hasChildNodes()) {
            // 将 <sql> 中的内容插入到 <sql> 节点之前
            toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
        }

        /*
         * 前面已经将 <sql> 节点的内容插入到 dom 中了,
         * 现在不需要 <sql> 节点了,这里将该节点从 dom 中移除
         */
        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);
                // 将 source 节点属性中的占位符 ${} 替换成具体的属性值
                attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
            }
        }

        NodeList children = source.getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
            // 递归调用
            applyIncludes(children.item(i), variablesContext, included);
        }

    // 第三个条件分支
    } else if (included && source.getNodeType() == Node.TEXT_NODE && !variablesContext.isEmpty()) {
        // 将文本(text)节点中的属性占位符 ${} 替换成具体的属性值
        source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
    }
}

我们先来看一下 applyIncludes 方法第一次被调用时的状态,source为 子节点列表,遍历子节点列表,将子节点作为参数,进行递归调用applyIncludes ,此时可获取到的子节点如下:

编号 子节点 类型 描述
1 SELECT * FROM TEXT_NODE 文本节点
2 ELEMENT_NODE 普通节点
3 WHERE id = #{id} TEXT_NODE 文本节点

接下来要做的事情是遍历列表,然后将子节点作为参数进行递归调用。第一个子节点调用applyIncludes方法,source为 SELECT * FROM 节点,节点类型:TEXT_NODE,进入分支三,没有${},不会替换,则节点一结束返回,什么都没有做。第二个节点调用applyIncludes方法,此时source为 节点,节点类型:ELEMENT_NODE,进入分支一,通过refid找到 sql 节点,也就是toInclude节点,然后执行source.getParentNode().replaceChild(toInclude, source);,直接将节点的父节点,也就是