写点什么

详解 MyBatis 加载映射文件和动态代理

  • 2023-03-23
    湖南
  • 本文字数:18959 字

    阅读完需:约 62 分钟

本篇文章将分析 MyBatis 在配置文件加载的过程中,如何解析映射文件中的 SQL 语句以及每条 SQL 语句如何与映射接口的方法进行关联。


MyBatis 版本:3.5.6

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

给出 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&serverTimezone=UTC&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>,这两种标签的说明如下所示:

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

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

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

在浅析 MyBatis 的配置加载流程中已经知道,使用 MyBatis 时会先读取配置文件 mybatis-config.xml 为字符流或者字节流,然后通过 SqlSessionFactoryBuilder 基于配置文件的字符流或字节流来构建 SqlSessionFactory


在这整个过程中,会解析 mybatis-config.xml 并将解析结果丰富进 Configuration,且 Configuration 在 MyBatis 中是一个单例,无论是配置文件的解析结果,还是映射文件的解析结果,亦或者是映射接口的解析结果,最终都会缓存在 Configuration 中。


接着浅析 MyBatis 的配置加载流程这篇文章末尾继续讲,配置文件的解析发生在 XMLConfigBuilder 的 parseConfiguration() 方法中,如下所示:

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 子节点的分支,所以继续往下看,Configuration 的 addMappers(String packageName) 方法如下所示:

public void addMappers(String packageName) {    mapperRegistry.addMappers(packageName);}
复制代码

mapperRegistry 是 Configuration 内部的成员变量,其内部有三个重载的 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<Ttype) 的实现,如下所示:

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 以键值对的形式存储在 MapperRegistry 的 knownMappers 缓存中,然后 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,这个 sqlFragments 和 Configuration 中的 sqlFragments 是同一份缓存,这一点切记,后面在分析处理 <include> 标签时会用到。


XMLMapperBuilder 的 parse() 方法如下所示:

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 中的唯一标识进行归纳。

上面表格中的 namespace 是映射文件<mapper>标签的 namespace 属性,因此对于映射文件里配置的 parameterMapresultMap 或者 SQL 执行语句,在 MyBatis 中的唯一标识就是 namespace + "." + 标签 id


下图可以直观的展示<select>标签解析后在 Configuration 中的形态:


下面以如何解析 <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 并丰富进 Configuration 的 MapperBuilderAssistant 类。


下面看一下 XMLStatementBuilder 的 parseStatementNode() 方法:

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 以及标签上的属性传入 MapperBuilderAssistant 的 addMappedStatement() 方法,以创建 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 设置参数值,以及调用 Configuration 的 addMappers(String packageName) 来加载映射文件/映射接口。

三、MyBatis 中的动态代理

已知在 MapperRegistry 中有一个叫做 knownMappers 的 map 缓存,其键为映射接口的 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<TmapperProxy) 方法,在该方法中会使用 Proxy 的 newProxyInstance() 方法创建动态代理对象,所以可以断定,MapperProxy 肯定会实现 InvocationHandler 接口。


MapperProxy 的类图如下所示:

果然,MapperProxy 实现了 InvocationHandler 接口,并在创建 MapperProxy 时 MapperProxyFactory 会将其持有的 methodCache 传递给 MapperProxy,因此 methodCache 的实际的读写是由 MapperProxy 来完成。下面看一下 MapperProxy 实现的 invoke() 方法,如下所示:

@Overridepublic 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 动态代理生成的映射接口的代理对象的方法时,最终调用请求会发送到 MapperProxy 的 invoke() 方法,在 MapperProxy 的 invoke() 方法中实际就是根据映射接口被调用的方法的对象去 methodCache 缓存中获取 MapperMethodInvoker 来实际执行请求。


如果获取不到那么就先为当前的方法对象创建一个 MapperMethodInvoker 并加入 methodCache 缓存,然后再用创建出来的 MapperMethodInvoker 去执行请求。cachedInvoker() 方法实现如下所示:

private 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 是接口,通常创建出来的 MapperMethodInvoker 为 PlainMethodInvoker,看一下 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 的构造函数中,会基于上述三个参数创建 SqlCommand 和 MethodSignature

  1. SqlCommand 主要是保存和映射接口被调用方法所关联的 MappedStatement 的信息;

  2. 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);        }    }}
复制代码

构造函数中主要做了这些事情:

  1. 先获取和被调用方法关联的 MappedStatement 对象;

  2. 然后将 MappedStatement 的 id 字段赋值给 SqlCommand 的 name 字段;

  3. 最后将 MappedStatement 的 sqlCommandType 字段赋值给 SqlCommand 的 type 字段。


这样一来,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() 方法会根据接口全限定名 + "." + "方法名" 作为 statementId 去 Configuration 的缓存中获取 MappedStatement,同时 resolveMappedStatement() 方法会从映射接口递归的遍历到声明被调用方法的接口,递归的终止条件如下所示:

  • 根据接口全限定名 + "." + "方法名" 作为 statementId 去 Configuration 的缓存中获取到了 MappedStatement

  • 从映射接口递归遍历到了声明被调用方法的接口,且根据声明被调用方法的接口的全限定名 + "." + "方法名" 作为 statementId 去 Configuration 的缓存中获取不到 MappedStatement


上面说得比较绕,下面用一个例子说明一下 resolveMappedStatement() 方法这样写的原因。下图是映射接口和映射文件所在的包路径:


BaseMapperBookBaseMapper 和 BookMapper 的关系如下图所示:


那么 MyBatis 会为 BaseMapperBookBaseMapper 和 BookMapper 都生成一个 MapperProxyFactory,如下所示:


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


MyBatis 3.4.2 及以前的版本,只会根据映射接口的全限定名 + "." + 方法名声明被调用方法的接口的全限定名 + "." + 方法名去 Configuration 的 mappedStatements 缓存中获取 MappedStatement


那么按照这样的逻辑,BookMapper 对应的 SqlCommand 就只会根据 com.mybatis.learn.dao.BookMapper.selectAllBooks 和 com.mybatis.learn.dao.BaseMapper.selectAllBooks 去 mappedStatements 缓存中获取 MappedStatement


那么结合上面图示给出的 mappedStatements 缓存内容,是无法获取到 MappedStatement 的,因此在 MyBatis 3.4.3 及之后的版本中,采用了 resolveMappedStatement() 方法中的逻辑,以支持继承了映射接口的接口对应的 SqlCommand 也能和映射接口对应的 MappedStatement 相关联


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


最后对映射接口的代理对象执行方法时的一个执行链进行说明。


首先,通过 JDK 动态代理的原理我们可以知道,调用代理对象的方法时,调用请求会发送到代理对象中的 InvocationHandler,在 MyBatis 中,调用映射接口的代理对象的方法的请求会发送到 MapperProxy,所以调用映射接口的代理对象的方法时,MapperProxy 的 invoke() 方法会执行,实现如下所示:

@Overridepublic 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,所以继续看 PlainMethodInvoker 的 invoke() 方法,如下所示:

@Overridepublic Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {    return mapperMethod.execute(sqlSession, args);}
复制代码

PlainMethodInvoker 的 invoke() 方法也没有什么逻辑,就是继续调用其 MapperMethod 的 execute() 方法,而通过上面的分析已经知道,MapperMethod 中的 SqlCommand 关联着 MappedStatement


而 MappedStatement 中包含着和被调用方法所关联的 SQL 信息,结合着 SqlSession,就可以完成对数据库的操作。关于如何对数据库操作,将在后续的文章中介绍,本篇文章对于 MyBatis 中的动态代理的分析就到此为止。


最后以一张图归纳一下 MyBatis 中的动态代理执行流程,如下所示:

总结

1. 每个 CRUD 标签唯一对应一个 MappedStatement 对象

具体对应关系可以用下图进行示意:


映射文件中,每一个 <select>,<insert>,<update> 和 <delete> 标签均会被创建一个 MappedStatement 并存放在 Configuration 的 mappedStatements 缓存中,MappedStatement 中主要包含着这个标签下的 SQL 语句,这个标签的参数信息和出参信息等。

每一个 MappedStatement 的唯一标识为 namespace + "." + 标签 id,这样设置唯一标识的原因是为了调用映射接口的方法时能够根据映射接口的权限定名 + "." + "方法名"获取到和被调用方法关联的 MappedStatement


因此,映射文件的 namespace 需要和映射接口的全限定名一致,每个 <select>,<insert>,<update> 和 <delete> 标签均对应一个映射接口的方法,每个 <select>,<insert>,<update> 和 <delete> 标签的 id 需要和映射接口的方法名一致;

2. 每个映射接口对应一个 JDK 动态代理对象

调用 MyBatis 映射接口的方法时,调用请求的实际执行是由基于 JDK 动态代理为映射接口生成的代理对象来完成,映射接口的代理对象由 MapperProxyFactory 的 newInstance() 方法生成,每个映射接口对应一个 MapperProxyFactory,对应一个 JDK 动态代理对象;

3. MyBatis 中的动态代理是对接口的代理

在 MyBatis 的 JDK 动态代理中,是不存在被代理对象的,是对接口的代理。MapperProxy 实现了 InvocationHandler 接口,因此 MapperProxy 在 MyBatis 的 JDK 动态代理中扮演调用处理器的角色,即调用映射接口的方法时。


实际上是调用的 MapperProxy 实现的 invoke() 方法,又因为不存在被代理对象,所以在 MapperProxy 的 invoke() 方法中,并没有去调用被代理对象的方法,而是会基于映射接口和被调用方法的方法对象生成 MapperMethod 并执行 MapperMethod 的 execute() 方法,即调用映射接口的方法的请求会发送到 MapperMethod


可以理解为映射接口的方法由 MapperMethod 代理


作者:半夏之沫

链接:https://juejin.cn/post/7203925850398883896

来源:稀土掘金

用户头像

还未添加个人签名 2021-07-28 加入

公众号:该用户快成仙了

评论

发布
暂无评论
详解MyBatis加载映射文件和动态代理_Java_做梦都在改BUG_InfoQ写作社区