关于动态代理:Mybatis源码加载映射文件与动态代理

前言

本篇文章将剖析Mybatis在配置文件加载的过程中,如何解析映射文件中的SQL语句以及每条SQL语句如何与映射接口的办法进行关联。在看该局部源码之前,须要具备JDK动静代理的相干常识,如果该局部不是很理解,能够先看Java根底-动静代理学习JDk动静代理的原理。

注释

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

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

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="useGeneratedKeys" value="true"/>
    </settings>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&amp;serverTimezone=UTC&amp;useSSL=false"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <package name="com.mybatis.learn.dao"/>
    </mappers>
</configuration>

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

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

依据上表所示,示例中的配置文件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的配置文件时,会依据配置文件中的<mappers>标签的属性来找到映射文件/映射接口并进行解析。如下是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<Class<?>> resolverUtil = new ResolverUtil<>();
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    //获取包门路下的映射接口的Class对象
    Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
    for (Class<?> mapperClass : mapperSet) {
        addMapper(mapperClass);
    }
}

最初,再看下addMapper(Class<T> type)的实现,如下所示。

public <T> void addMapper(Class<T> 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()办法一层一层的调用下来,理论就是依据配置文件中<mappers>标签的<package>子标签设置的映射文件/映射接口所在包的全限定名来获取映射接口的Class对象,而后基于每个映射接口的Class对象来创立一个MapperProxyFactory,顾名思义,MapperProxyFactory是映射接口的动静代理工厂,负责为对应的映射接口生成动静代理类,这里先简要看一下MapperProxyFactory的实现。

public class MapperProxyFactory<T> {

    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();

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

    public Class<T> getMapperInterface() {
        return mapperInterface;
    }

    public Map<Method, MapperMethodInvoker> getMethodCache() {
        return methodCache;
    }

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

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

}

很规范的基于JDK动静代理的实现,所以能够晓得,Mybatis会为每个映射接口创立一个MapperProxyFactory,而后将映射接口与MapperProxyFactory以键值对的模式存储在MapperRegistryknownMappers缓存中,而后MapperProxyFactory会为映射接口基于JDK动静代理的形式生成代理类,至于如何生成,将在第三大节中对MapperProxyFactory进一步剖析。

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

所以一个映射接口对应一个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,其类图如下所示。

如图所示,解析配置文件和解析映射文件的解析类均继承于BaseBuilder,而后BaseBuilder中持有全局惟一的Configuration,所以解析后果会丰盛进Configuration,特地留神,XMLMapperBuilder还有一个名为sqlFragments的缓存,用于存储<sql>标签对应的XNode,这个sqlFragmentsConfiguration中的sqlFragments是同一份缓存,这一点切记,前面在剖析解决<include>标签时会用到。XMLMapperBuilderparse()办法如下所示。

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
        //从映射文件的<mapper>标签开始进行解析
        //解析后果会丰盛进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>标签生成ParameterMap并缓存到Configuration
        parameterMapElement(context.evalNodes("/mapper/parameterMap"));
        //解析<resultMap>标签生成ResultMap并缓存到Configuration
        resultMapElements(context.evalNodes("/mapper/resultMap"));
        //将<sql>标签对应的节点XNode保留到sqlFragments中
        //理论也是保留到Configuration的sqlFragments缓存中
        sqlElement(context.evalNodes("/mapper/sql"));
        //解析<select>,<insert>,<update>和<delete>标签
        //生成MappedStatement并缓存到Configuration
        buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing Mapper XML. The XML location is '" 
                + resource + "'. Cause: " + e, e);
    }
}

configurationElement()办法会将映射文件<mapper>下的各个子标签解析成相应的类,而后缓存在Configuration中。通常,在映射文件的<mapper>标签下,罕用的子标签为<parameterMap><resultMap><select><insert><update><delete>,上面给出一个简略的表格对这些标签生成的类以及在Configuration中的惟一标识进行演绎。

标签 解析生成的类 Configuration中的惟一标识
<parameterMap> ParameterMap namespace + “.” + 标签id
<resultMap> ResultMap namespace + “.” + 标签id
<select><insert><update><delete> MappedStatement namespace + “.” + 标签id

下面表格中的namespace是映射文件<mapper>标签的namespace属性,因而对于映射文件里配置的parameterMapresultMap或者SQL执行语句,在Mybatis中的惟一标识就是namespace + “.” + 标签id。上面以如何解析<select><insert><update><delete>标签的内容为例,进行阐明,buildStatementFromContext()办法如下所示。

private void buildStatementFromContext(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
        buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
}

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    //每一个<select>,<insert>,<update>和<delete>标签均会被创立一个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);
        }
    }
}

对于每一个<select><insert><update><delete>标签,均会创立一个XMLStatementBuilder来进行解析并生成MappedStatement,同样,看一下XMLStatementBuilder的类图,如下所示。

XMLStatementBuilder中持有<select><insert><update><delete>标签对应的节点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);

    //如果应用了<include>标签,则将<include>标签替换为匹配的<sql>标签中的Sql片段
    //匹配规定是在Configuration中依据namespace+"."+refid去匹配<sql>标签
    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语句
    //如果应用到了<if>,<foreach>等标签进行动静Sql语句的拼接,则创立进去的SqlSource为DynamicSqlSource
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    StatementType statementType = StatementType
            .valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    //获取<select>,<insert>,<update>和<delete>标签上的属性
    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");

    //依据下面获取到的参数,创立MappedStatement并增加到Configuration中
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

parseStatementNode()办法整体流程稍长,总结概括起来该办法做了如下几件事件。

  • <include>标签替换为其指向的SQL片段;
  • 如果未应用动静SQL,则创立RawSqlSource以保留SQL语句,如果应用了动静SQL(例如应用了<if><foreach>等标签),则创立DynamicSqlSource以反对SQL语句的动静拼接;
  • 获取<select><insert><update><delete>标签上的属性;
  • 将获取到的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;
}

至此,解析<select><insert><update><delete>标签的内容而后生成MappedStatement并增加到Configuration的流程剖析结束,实际上,解析<parameterMap>标签,解析<resultMap>标签的大体流程和下面基本一致,最终都是借助MapperBuilderAssistant生成对应的类(例如ParameterMapResultMap)而后再缓存到Configuration中,且每种解析生成的类在对应缓存中的惟一标识为namespace + “.” + 标签id

最初,回到本大节结尾,即XMLConfigBuilder中的mapperElement()办法,在这个办法中,会依据配置文件中<mappers>标签的子标签的不同,进入不同的分支执行加载映射文件/映射接口的逻辑,实际上,整个加载映射文件/加载映射接口的流程是一个环形,能够用下图进行示意。

XMLConfigBuilder中的mapperElement()办法的不同分支只是从不同的入口进入整个加载的流程中,同时Mybatis会在每个操作执行前判断是否曾经做过以后操作,做过就不再反复执行,因而保障了整个环形解决流程只会执行一遍,不会死循环。以及,如果是在我的项目中基于JavaConfig的形式来配置Mybatis,那么通常会间接对Configuration设置参数值,以及调用ConfigurationaddMappers(String packageName)来加载映射文件/映射接口。

三. Mybatis中的动静代理

已知在MapperRegistry中有一个叫做knownMappersmap缓存,其键为映射接口的Class对象,值为Mybatis为映射接口创立的动静代理工厂MapperProxyFactory,当调用映射接口定义的办法执行数据库操作时,理论调用申请会由MapperProxyFactory为映射接口生成的代理对象来实现。这里给出MapperProxyFactory的实现,如下所示。

public class MapperProxyFactory<T> {

    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();

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

    public Class<T> getMapperInterface() {
        return mapperInterface;
    }

    public Map<Method, MapperMethodInvoker> getMethodCache() {
        return methodCache;
    }

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

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

}

MapperProxyFactory中,mapperInterface为映射接口的Class对象,methodCache是一个map缓存,其键为映射接口的办法对象,值为这个办法对应的MapperMethodInvoker,实际上,SQL的执行最终会由MapperMethodInvoker实现,前面会具体阐明。当初再察看MapperProxyFactory中两个重载的newInstance()办法,能够晓得这是基于JDK的动静代理,在public T newInstance(SqlSession sqlSession)这个办法中,会创立MapperProxy,并将其作为参数调用protected T newInstance(MapperProxy<T> mapperProxy)办法,在该办法中会应用ProxynewProxyInstance()办法创立动静代理对象,所以能够判定,MapperProxy必定会实现InvocationHandler接口,MapperProxy的类图如下所示。

果然,MapperProxy实现了InvocationHandler接口,并在创立MapperProxyMapperProxyFactory会将其持有的methodCache传递给MapperProxy,因而methodCache的理论的读写是由MapperProxy来实现。上面看一下MapperProxy实现的invoke()办法,如下所示。

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        } else {
            //从methodCache中依据办法对象获取MapperMethodInvoker来执行Sql
            //如果获取不到,则创立一个MapperMethodInvoker并增加到methodCache中,再执行Sql
            return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
        }
    } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
    }
}

基于JDK动静代理的原理能够晓得,当调用JDK动静代理生成的映射接口的代理对象的办法时,最终调用申请会发送到MapperProxyinvoke()办法,在MapperProxyinvoke()办法中理论就是依据映射接口被调用的办法的对象去methodCache缓存中获取MapperMethodInvoker来理论执行申请,如果获取不到那么就先为以后的办法对象创立一个MapperMethodInvoker并退出methodCache缓存,而后再用创立进去的MapperMethodInvoker去执行申请。cachedInvoker()办法实现如下所示。

private MapperProxy.MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
    try {
        MapperProxy.MapperMethodInvoker invoker = methodCache.get(method);
        //从methodCache缓存中获取到MapperMethodInvoker不为空则间接返回
        if (invoker != null) {
            return invoker;
        }

        //从methodCache缓存中获取到MapperMethodInvoker为空
        //则创立一个MapperMethodInvoker而后增加到methodCache缓存,并返回
        return methodCache.computeIfAbsent(method, m -> {
            //JDK1.8接口中的default()办法解决逻辑
            if (m.isDefault()) {
                try {
                    if (privateLookupInMethod == null) {
                        return new MapperProxy.DefaultMethodInvoker(getMethodHandleJava8(method));
                    } else {
                        return new MapperProxy.DefaultMethodInvoker(getMethodHandleJava9(method));
                    }
                } catch (IllegalAccessException | InstantiationException | InvocationTargetException
                        | NoSuchMethodException e) {
                    throw new RuntimeException(e);
                }
            } else {
                //先创立一个MapperMethod
                //再将MapperMethod作为参数创立PlainMethodInvoker
                return new MapperProxy.PlainMethodInvoker(
                    new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
            }
        });
    } catch (RuntimeException re) {
        Throwable cause = re.getCause();
        throw cause == null ? re : cause;
    }
}

MapperMethodInvoker是接口,通常创立进去的MapperMethodInvokerPlainMethodInvoker,看一下PlainMethodInvoker的构造函数。

public PlainMethodInvoker(MapperMethod mapperMethod) {
    super();
    this.mapperMethod = mapperMethod;
}

因而创立PlainMethodInvoker时,须要先创立MapperMethod,而PlainMethodInvoker在执行时也是将执行的申请传递给MapperMethod,所以持续往下,MapperMethod的构造函数如下所示。

public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    this.command = new SqlCommand(config, mapperInterface, method);
    this.method = new MethodSignature(config, mapperInterface, method);
}

创立MapperMethod时须要传入的参数为映射接口的Class对象映射接口被调用的办法的对象配置类Configuration,在MapperMethod的构造函数中,会基于上述三个参数创立SqlCommandMethodSignatureSqlCommand次要是保留和映射接口被调用办法所关联的MappedStatement的信息,MethodSignature次要是存储映射接口被调用办法的参数信息和返回值信息,先看一下SqlCommand的构造函数,如下所示。

public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
    //获取映射接口被调用办法的办法名
    final String methodName = method.getName();
    //获取申明被调用办法的接口的Class对象
    final Class<?> declaringClass = method.getDeclaringClass();
    //获取和映射接口被调用办法关联的MappedStatement对象
    MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
            configuration);
    if (ms == null) {
        if (method.getAnnotation(Flush.class) != null) {
            name = null;
            type = SqlCommandType.FLUSH;
        } else {
            throw new BindingException("Invalid bound statement (not found): "
                    + mapperInterface.getName() + "." + methodName);
        }
    } else {
        //将MappedStatement的id赋值给SqlCommand的name字段
        name = ms.getId();
        //将MappedStatement的Sql命令类型赋值给SqlCommand的type字段
        //比方SELECT,INSERT等
        type = ms.getSqlCommandType();
        if (type == SqlCommandType.UNKNOWN) {
            throw new BindingException("Unknown execution method for: " + name);
        }
    }
}

构造函数中次要做了这些事件:先获取和被调用办法关联的MappedStatement对象,而后将MappedStatementid字段赋值给SqlCommandname字段,最初将MappedStatementsqlCommandType字段赋值给SqlCommandtype字段,这样一来,SqlCommand就具备了和被调用办法关联的MappedStatement的信息。那么如何获取和被调用办法关联的MappedStatement对象呢,持续看resolveMappedStatement()的实现,如下所示。

private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
                                               Class<?> declaringClass, Configuration configuration) {
    //依据接口全限定名+"."+办法名拼接出MappedStatement的id
    String statementId = mapperInterface.getName() + "." + methodName;
    //如果Configuration中缓存了statementId对应的MappedStatement,则间接返回这个MappedStatement
    //这是递归的终止条件之一
    if (configuration.hasStatement(statementId)) {
        return configuration.getMappedStatement(statementId);
    } else if (mapperInterface.equals(declaringClass)) {
        //以后mapperInterface曾经是申明被调用办法的接口的Class对象,且未匹配到缓存的MappedStatement,返回null
        //这是resolveMappedStatement()递归的终止条件之一
        return null;
    }
    //递归调用
    for (Class<?> superInterface : mapperInterface.getInterfaces()) {
        if (declaringClass.isAssignableFrom(superInterface)) {
            MappedStatement ms = resolveMappedStatement(superInterface, methodName,
                    declaringClass, configuration);
            if (ms != null) {
                return ms;
            }
        }
    }
    return null;
}

resolveMappedStatement()办法会依据接口全限定名 + “.” + “办法名”作为statementIdConfiguration的缓存中获取MappedStatement,同时resolveMappedStatement()办法会从映射接口递归的遍历到申明被调用办法的接口,递归的终止条件如下所示。

  • 依据接口全限定名 + “.” + “办法名”作为statementIdConfiguration的缓存中获取到了MappedStatement
  • 从映射接口递归遍历到了申明被调用办法的接口,且依据申明被调用办法的接口的全限定名 + “.” + “办法名”作为statementIdConfiguration的缓存中获取不到MappedStatement

下面说得比拟绕,上面用一个例子阐明一下resolveMappedStatement()办法这样写的起因。下图是映射接口和映射文件所在的包门路。

BaseMapperBookBaseMapperBookMapper的关系如下图所示。

那么Mybatis会为BaseMapperBookBaseMapperBookMapper都生成一个MapperProxyFactory,如下所示。

同样,在Configuration中也会缓存着解析BookBaseMapper.xml映射文件所生成的MappedStatement,如下所示。

Mybatis3.4.2及以前的版本,只会依据映射接口的全限定名 + “.” + 办法名申明被调用办法的接口的全限定名 + “.” + 办法名ConfigurationmappedStatements缓存中获取MappedStatement,那么依照这样的逻辑,BookMapper对应的SqlCommand就只会依据com.mybatis.learn.dao.BookMapper.selectAllBookscom.mybatis.learn.dao.BaseMapper.selectAllBooksmappedStatements缓存中获取MappedStatement,那么联合下面图示给出的mappedStatements缓存内容,是无奈获取到MappedStatement的,因而在Mybatis3.4.3及之后的版本中,采纳了resolveMappedStatement()办法中的逻辑,以反对继承了映射接口的接口对应的SqlCommand也能和映射接口对应的MappedStatement相关联

对于SqlCommand的剖析到此为止,而MapperMethod中的MethodSignature次要是用于存储被调用办法的参数信息和返回值信息,这里也不再赘述。

最初对映射接口的代理对象执行办法时的一个执行链进行阐明。首先,通过JDK动静代理的原理咱们能够晓得,调用代理对象的办法时,调用申请会发送到代理对象中的InvocationHandler,在Mybatis中,调用映射接口的代理对象的办法的申请会发送到MapperProxy,所以调用映射接口的代理对象的办法时,MapperProxyinvoke()办法会执行,实现如下所示。

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        } else {
            //从methodCache中依据办法对象获取MapperMethodInvoker来执行Sql
            //如果获取不到,则创立一个MapperMethodInvoker并增加到methodCache中,再执行Sql
            return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
        }
    } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
    }
}

所以到这里,Mybatis就和传统的JDK动静代理产生了一点差异,传统JDK动静代理通常在其InvocationHandler中会在被代理对象办法执行前和执行后减少一些装璜逻辑,而在Mybatis中,是不存在被代理对象的,只有被代理接口,所以也不存在调用被代理对象的办法这一逻辑,取而代之的是依据被调用办法的办法对象获取MapperMethodInvoker并执行其invoke()办法,通常获取到的是PlainMethodInvoker,所以持续看PlainMethodInvokerinvoke()办法,如下所示。

@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
    return mapperMethod.execute(sqlSession, args);
}

PlainMethodInvokerinvoke()办法也没有什么逻辑,就是持续调用其MapperMethodexecute()办法,而通过下面的剖析曾经晓得,MapperMethod中的SqlCommand关联着MappedStatement,而MappedStatement中蕴含着和被调用办法所关联的SQL信息,联合着SqlSession,就能够实现对数据库的操作。对于如何对数据库操作,将在后续的文章中介绍,本篇文章对于Mybatis中的动静代理的剖析就到此为止。最初以一张图演绎一下Mybatis中的动静代理执行流程,如下所示。

总结

本篇文章能够用如下内容进行总结。

  • 映射文件中,每一个<select><insert><update><delete>标签均会被创立一个MappedStatement并存放在ConfigurationmappedStatements缓存中,MappedStatement中次要蕴含着这个标签下的SQL语句,这个标签的参数信息和出参信息等。每一个MappedStatement的惟一标识为namespace + “.” + 标签id,这样设置惟一标识的起因是为了调用映射接口的办法时可能依据映射接口的全限定名 + “.” + “办法名”获取到和被调用办法关联的MappedStatement,因而,映射文件的namespace须要和映射接口的全限定名统一,每个<select><insert><update><delete>标签均对应一个映射接口的办法,每个<select><insert><update><delete>标签的id须要和映射接口的办法名统一;
  • 调用Mybatis映射接口的办法时,调用申请的理论执行是由基于JDK动静代理为映射接口生成的代理对象来实现,映射接口的代理对象由MapperProxyFactorynewInstance()办法生成,每个映射接口对应一个MapperProxyFactory
  • MybatisJDK动静代理中,是由MapperProxy实现了InvocationHandler接口,因而MapperProxyMybatisJDK动静代理中表演调用处理器的角色,即调用映射接口的办法时,实际上是调用的MapperProxy实现的invoke()办法;
  • MybatisJDK动静代理中,是不存在被代理对象的,能够了解为是对接口的代理,因而在MapperProxyinvoke()办法中,并没有去调用被代理对象的办法,而是会基于映射接口和被调用办法的办法对象生成MapperMethod并执行MapperMethodexecute()办法,即调用映射接口的办法的申请会发送到MapperMethod,能够了解为映射接口的办法由MapperMethod代理。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理