前言
本篇文章将分析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>,这两种标签的说明如下所示。
标签 | 说明 |
---|---|
<mapper> | 该标签有三种属性,分别为resource,url和class,且在同一个<mapper>标签中,只能设置这三种属性中的一种,否则会报错。resource和url属性均是通过告诉MyBatis映射文件所在的位置路径来注册映射文件,前者使用相对路径(相对于classpath,例如"mapper/BookMapper.xml"),后者使用绝对路径。class属性是通过告诉MyBatis映射文件对应的映射接口的全限定名来注册映射接口,此时要求映射文件与映射接口同名且同目录。 |
<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<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以键值对的形式存储在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中的唯一标识进行归纳。
标签 | 解析生成的类 | 在Configuration 中的唯一标识 |
---|---|---|
<parameterMap> | ParameterMap | namespace + "." + 标签id |
<resultMap> | ResultMap | namespace + "." + 标签id |
<select>,<insert>,<update>,<delete> | MappedStatement | namespace + "." + 标签id |
上面表格中的namespace是映射文件<mapper>标签的namespace属性,因此对于映射文件里配置的parameterMap,resultMap或者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生成对应的类(例如ParameterMap,ResultMap)然后再缓存到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<T> mapperProxy) 方法,在该方法中会使用Proxy的newProxyInstance() 方法创建动态代理对象,所以可以断定,MapperProxy肯定会实现InvocationHandler接口,MapperProxy的类图如下所示。
果然,MapperProxy实现了InvocationHandler接口,并在创建MapperProxy时MapperProxyFactory会将其持有的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动态代理生成的映射接口的代理对象的方法时,最终调用请求会发送到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:
- SqlCommand主要是保存和映射接口被调用方法所关联的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对象;
- 然后将MappedStatement的id字段赋值给SqlCommand的name字段;
- 最后将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() 方法这样写的原因。下图是映射接口和映射文件所在的包路径。
BaseMapper,BookBaseMapper和BookMapper的关系如下图所示。
那么MyBatis会为BaseMapper,BookBaseMapper和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() 方法会执行,实现如下所示。
@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,所以继续看PlainMethodInvoker的invoke() 方法,如下所示。
@Override public 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
代理。