Mybatis源码-加载映射文件与动态代理

前言

本篇文章将分析Mybatis在配置文件加载的过程中,如何解析映射文件中的SQL语句以及每条SQL语句如何与映射接口的方法进行关联。在看该部分源码之前,需要具备JDK动态代理的相关知识,如果该部分不是很了解,可以先看Java基础-动态代理学习JDk动态代理的原理。

正文

一. 映射文件/映射接口的配置

给出Mybatis的配置文件mybatis-config.xml如下所示。




    
        
    

    
        
            
            
                
                
                
                
            
        
    

    
        
    

上述配置文件的mappers节点用于配置映射文件/映射接口mappers节点下有两种子节点,标签分别为,这两种标签的说明如下所示。

标签 说明
该标签有三种属性,分别为resourceurlclass,且在同一个标签中,只能设置这三种属性中的一种,否则会报错。resourceurl属性均是通过告诉Mybatis映射文件所在的位置路径来注册映射文件,前者使用相对路径(相对于classpath,例如"mapper/BookMapper.xml"),后者使用绝对路径。class属性是通过告诉Mybatis映射文件对应的映射接口的全限定名来注册映射接口,此时要求映射文件与映射接口同名且同目录。
通过设置映射接口所在包名来注册映射接口,此时要求映射文件与映射接口同名且同目录。

根据上表所示,示例中的配置文件mybatis-config.xml是通过设置映射接口所在包名来注册映射接口的,所以映射文件与映射接口需要同名且目录,如下图所示。

具体的原因会在下文的源码分析中给出。

二. 加载映射文件的源码分析

Mybatis源码-配置加载中已经知道,使用Mybatis时会先读取配置文件mybatis-config.xml为字符流或者字节流,然后通过SqlSessionFactoryBuilder基于配置文件的字符流或字节流来构建SqlSessionFactory。在这整个过程中,会解析mybatis-config.xml并将解析结果丰富进Configuration,且ConfigurationMybatis中是一个单例,无论是配置文件的解析结果,还是映射文件的解析结果,亦或者是映射接口的解析结果,最终都会存在Configuration中。接着Mybatis源码-配置加载这篇文章末尾继续讲,配置文件的解析发生在XMLConfigBuilderparseConfiguration()方法中,如下所示。

private void parseConfiguration(XNode root) {
    try {
        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);
        environmentsElement(root.evalNode("environments"));
        databaseIdProviderElement(root.evalNode("databaseIdProvider"));
        typeHandlerElement(root.evalNode("typeHandlers"));
        //根据mappers标签的属性,找到映射文件/映射接口并解析
        mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
}

如上所示,在解析Mybatis的配置文件时,会根据配置文件中的标签的属性来找到映射文件/映射接口并进行解析。如下是mapperElement()方法的实现。

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            if ("package".equals(child.getName())) {
                //处理package子节点
                String mapperPackage = child.getStringAttribute("name");
                configuration.addMappers(mapperPackage);
            } else {
                String resource = child.getStringAttribute("resource");
                String url = child.getStringAttribute("url");
                String mapperClass = child.getStringAttribute("class");
                if (resource != null && url == null && mapperClass == null) {
                    //处理设置了resource属性的mapper子节点
                    ErrorContext.instance().resource(resource);
                    InputStream inputStream = Resources.getResourceAsStream(resource);
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(
                            inputStream, configuration, resource, configuration.getSqlFragments());
                    mapperParser.parse();
                } 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();
                } else if (resource == null && url == null && mapperClass != null) {
                    //处理设置了class属性的mapper子节点
                    Class mapperInterface = Resources.classForName(mapperClass);
                    configuration.addMapper(mapperInterface);
                } else {
                    //同时设置了mapper子节点的两个及以上的属性时,报错
                    throw new BuilderException(
                            "A mapper element may only specify a url, resource or class, but not more than one.");
                }
            }
        }
    }
}

结合示例中的配置文件,那么在mapperElement()方法中应该进入处理package子节点的分支,所以继续往下看,ConfigurationaddMappers(String packageName)方法如下所示。

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

mapperRegistryConfiguration内部的成员变量,其内部有三个重载的addMappers()方法,首先看addMappers(String packageName)方法,如下所示。

public void addMappers(String packageName) {
    addMappers(packageName, Object.class);
}

继续往下,addMappers(String packageName, Class superType)的实现如下所示。

public void addMappers(String packageName, Class superType) {
    ResolverUtil> resolverUtil = new ResolverUtil<>();
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    //获取包路径下的映射接口的Class对象
    Set>> mapperSet = resolverUtil.getClasses();
    for (Class mapperClass : mapperSet) {
        addMapper(mapperClass);
    }
}

最后,再看下addMapper(Class type)的实现,如下所示。

public  void addMapper(Class type) {
    if (type.isInterface()) {
        //判断knownMappers中是否已经有当前映射接口
        //knownMappers是一个map存储结构,key为映射接口Class对象,value为MapperProxyFactory
        //MapperProxyFactory为映射接口对应的动态代理工厂
        if (hasMapper(type)) {
            throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
        }
        boolean loadCompleted = false;
        try {
            knownMappers.put(type, new MapperProxyFactory<>(type));
            //依靠MapperAnnotationBuilder来完成映射文件和映射接口中的Sql解析
            //先解析映射文件,再解析映射接口
            MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
            parser.parse();
            loadCompleted = true;
        } finally {
            if (!loadCompleted) {
                knownMappers.remove(type);
            }
        }
    }
}

上面三个addMapper()方法一层一层的调用下来,实际就是根据配置文件中标签的子标签设置的映射文件/映射接口所在包的全限定名来获取映射接口的Class对象,然后基于每个映射接口的Class对象来创建一个MapperProxyFactory,顾名思义,MapperProxyFactory是映射接口的动态代理工厂,负责为对应的映射接口生成动态代理类,这里先简要看一下MapperProxyFactory的实现。

public class MapperProxyFactory {

    private final Class mapperInterface;
    private final Map methodCache = new ConcurrentHashMap<>();

    public MapperProxyFactory(Class mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    public Class getMapperInterface() {
        return mapperInterface;
    }

    public Map getMethodCache() {
        return methodCache;
    }

    @SuppressWarnings("unchecked")
    protected T newInstance(MapperProxy mapperProxy) {
        return (T) Proxy.newProxyInstance(
                mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
    }

    public T newInstance(SqlSession sqlSession) {
        final MapperProxy mapperProxy = new MapperProxy<>(
                sqlSession, mapperInterface, methodCache);
        return newInstance(mapperProxy);
    }

}

很标准的基于JDK动态代理的实现,所以可以知道,Mybatis会为每个映射接口创建一个MapperProxyFactory,然后将映射接口与MapperProxyFactory以键值对的形式存储在MapperRegistryknownMappers缓存中,然后MapperProxyFactory会为映射接口基于JDK动态代理的方式生成代理类,至于如何生成,将在第三小节中对MapperProxyFactory进一步分析。

继续之前的流程,为映射接口创建完MapperProxyFactory之后,就应该对映射文件和映射接口中的SQL进行解析,解析依靠的类为MapperAnnotationBuilder,其类图如下所示。

Mybatis源码-加载映射文件与动态代理_第1张图片

所以一个映射接口对应一个MapperAnnotationBuilder,并且每个MapperAnnotationBuilder中持有全局唯一的Configuration类,解析结果会丰富进Configuration中。MapperAnnotationBuilder的解析方法parse()如下所示。

public void parse() {
    String resource = type.toString();
    //判断映射接口是否解析过,没解析过才继续往下执行
    if (!configuration.isResourceLoaded(resource)) {
        //先解析映射文件中的Sql语句
        loadXmlResource();
        //将当前映射接口添加到缓存中,以表示当前映射接口已经被解析过
        configuration.addLoadedResource(resource);
        assistant.setCurrentNamespace(type.getName());
        parseCache();
        parseCacheRef();
        //解析映射接口中的Sql语句
        for (Method method : type.getMethods()) {
            if (!canHaveStatement(method)) {
                continue;
            }
            if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
                    && method.getAnnotation(ResultMap.class) == null) {
                parseResultMap(method);
            }
            try {
                parseStatement(method);
            } catch (IncompleteElementException e) {
                configuration.addIncompleteMethod(new MethodResolver(this, method));
            }
        }
    }
    parsePendingMethods();
}

按照parse()方法的执行流程,会先解析映射文件中的SQL语句,然后再解析映射接口中的SQL语句,这里以解析映射文件为例,进行说明。loadXmlResource()方法实现如下。

private void loadXmlResource() {
    if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
        //根据映射接口的全限定名拼接成映射文件的路径
        //这也解释了为什么要求映射文件和映射接口在同一目录
        String xmlResource = type.getName().replace('.', '/') + ".xml";
        InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
        if (inputStream == null) {
            try {
                inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
            } catch (IOException e2) {
            
            }
        }
        if (inputStream != null) {
            XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), 
                    xmlResource, configuration.getSqlFragments(), type.getName());
            //解析映射文件
            xmlParser.parse();
        }
    }
}

loadXmlResource()方法中,首先要根据映射接口的全限定名拼接出映射文件的路径,拼接规则就是将全限定名的"."替换成"/",然后在末尾加上".xml",这也是为什么要求映射文件和映射接口需要在同一目录下且同名。对于映射文件的解析,是依靠XMLMapperBuilder,其类图如下所示。

Mybatis源码-加载映射文件与动态代理_第2张图片

如图所示,解析配置文件和解析映射文件的解析类均继承于BaseBuilder,然后BaseBuilder中持有全局唯一的Configuration,所以解析结果会丰富进Configuration,特别注意,XMLMapperBuilder还有一个名为sqlFragments的缓存,用于存储标签对应的XNode,这个sqlFragmentsConfiguration中的sqlFragments是同一份缓存,这一点切记,后面在分析处理标签时会用到。XMLMapperBuilderparse()方法如下所示。

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
        //从映射文件的标签开始进行解析
        //解析结果会丰富进Configuration
        configurationElement(parser.evalNode("/mapper"));
        configuration.addLoadedResource(resource);
        bindMapperForNamespace();
    }

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

继续看configurationElement()方法的实现,如下所示。

private void configurationElement(XNode context) {
    try {
        String namespace = context.getStringAttribute("namespace");
        if (namespace == null || namespace.isEmpty()) {
            throw new BuilderException("Mapper's namespace cannot be empty");
        }
        builderAssistant.setCurrentNamespace(namespace);
        cacheRefElement(context.evalNode("cache-ref"));
        cacheElement(context.evalNode("cache"));
        //解析标签生成ParameterMap并缓存到Configuration
        parameterMapElement(context.evalNodes("/mapper/parameterMap"));
        //解析标签生成ResultMap并缓存到Configuration
        resultMapElements(context.evalNodes("/mapper/resultMap"));
        //将标签对应的节点XNode保存到sqlFragments中
        //实际也是保存到Configuration的sqlFragments缓存中
        sqlElement(context.evalNodes("/mapper/sql"));
        //解析,下面给出一个简单的表格对这些标签生成的类以及在Configuration中的唯一标识进行归纳。

标签 解析生成的类 Configuration中的唯一标识
ParameterMap namespace + "." + 标签id
ResultMap namespace + "." + 标签id
MappedStatement namespace + "." + 标签id

上面表格中的namespace是映射文件标签的namespace属性,因此对于映射文件里配置的parameterMapresultMap或者SQL执行语句,在Mybatis中的唯一标识就是namespace + "." + 标签id。下面以如何解析标签均会被创建一个MappedStatement //每个MappedStatement会存放在Configuration的mappedStatements缓存中 //mappedStatements是一个map,键为映射接口全限定名+"."+标签id,值为MappedStatement for (XNode context : list) { final XMLStatementBuilder statementParser = new XMLStatementBuilder( configuration, builderAssistant, context, requiredDatabaseId); try { statementParser.parseStatementNode(); } catch (IncompleteElementException e) { configuration.addIncompleteStatement(statementParser); } } }

对于每一个标签对应的节点XNode,以及帮助创建MappedStatement并丰富进ConfigurationMapperBuilderAssistant类。下面看一下XMLStatementBuilderparseStatementNode()方法。

public void parseStatementNode() {
    //获取标签id
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
        return;
    }

    String nodeName = context.getNode().getNodeName();
    //获取标签的类型,例如SELECT,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);

    //如果使用了标签,则将标签替换为匹配的标签中的Sql片段
    //匹配规则是在Configuration中根据namespace+"."+refid去匹配标签
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    //获取输入参数类型
    String parameterType = context.getStringAttribute("parameterType");
    Class parameterTypeClass = resolveClass(parameterType);

    //获取LanguageDriver以支持实现动态Sql
    //这里获取到的实际上为XMLLanguageDriver
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    //获取KeyGenerator
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    //先从缓存中获取KeyGenerator
    if (configuration.hasKeyGenerator(keyStatementId)) {
        keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
        //缓存中如果获取不到,则根据useGeneratedKeys的配置决定是否使用KeyGenerator
        //如果要使用,则Mybatis中使用的KeyGenerator为Jdbc3KeyGenerator
        keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
            configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
            ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }

    //通过XMLLanguageDriver创建SqlSource,可以理解为Sql语句
    //如果使用到了等标签进行动态Sql语句的拼接,则创建出来的SqlSource为DynamicSqlSource
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    StatementType statementType = StatementType
            .valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    //获取标签上的属性; 
  
  • 将获取到的SqlSource以及标签上的属性传入MapperBuilderAssistantaddMappedStatement()方法,以创建MappedStatement并添加到Configuration中。
  • MapperBuilderAssistant是最终创建MappedStatement以及将MappedStatement添加到Configuration的处理类,其addMappedStatement()方法如下所示。

    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");
        }
    
        //拼接出MappedStatement的唯一标识
        //规则是namespace+"."+id
        id = applyCurrentNamespace(id, false);
        boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    
        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
        MappedStatement statement = statementBuilder.build();
        //将MappedStatement添加到Configuration中
        configuration.addMappedStatement(statement);
        return statement;
    }

    至此,解析标签均会被创建一个MappedStatement并存放在ConfigurationmappedStatements缓存中,MappedStatement中主要包含着这个标签下的SQL语句,这个标签的参数信息和出参信息等。每一个MappedStatement的唯一标识为namespace + "." + 标签id,这样设置唯一标识的原因是为了调用映射接口的方法时能够根据映射接口的全限定名 + "." + "方法名"获取到和被调用方法关联的MappedStatement,因此,映射文件的namespace需要和映射接口的全限定名一致,每个标签的id需要和映射接口的方法名一致;

  • 调用Mybatis映射接口的方法时,调用请求的实际执行是由基于JDK动态代理为映射接口生成的代理对象来完成,映射接口的代理对象由MapperProxyFactorynewInstance()方法生成,每个映射接口对应一个MapperProxyFactory
  • MybatisJDK动态代理中,是由MapperProxy实现了InvocationHandler接口,因此MapperProxyMybatisJDK动态代理中扮演调用处理器的角色,即调用映射接口的方法时,实际上是调用的MapperProxy实现的invoke()方法;
  • MybatisJDK动态代理中,是不存在被代理对象的,可以理解为是对接口的代理,因此在MapperProxyinvoke()方法中,并没有去调用被代理对象的方法,而是会基于映射接口和被调用方法的方法对象生成MapperMethod并执行MapperMethodexecute()方法,即调用映射接口的方法的请求会发送到MapperMethod,可以理解为映射接口的方法由MapperMethod代理。
  • 你可能感兴趣的:(Mybatis源码-加载映射文件与动态代理)