前言
本篇文章将分析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&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>
,<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
中的动态代理执行流程,如下所示。
总结
本篇文章可以用如下内容进行总结。
- 映射文件中,每一个
<select>
,<insert>
,<update>
和<delete>
标签均会被创建一个MappedStatement
并存放在Configuration
的mappedStatements缓存中,MappedStatement
中主要包含着这个标签下的SQL
语句,这个标签的参数信息和出参信息等。每一个MappedStatement
的唯一标识为namespace + "." + 标签id,这样设置唯一标识的原因是为了调用映射接口的方法时能够根据映射接口的全限定名 + "." + "方法名"获取到和被调用方法关联的MappedStatement
,因此,映射文件的namespace需要和映射接口的全限定名一致,每个<select>
,<insert>
,<update>
和<delete>
标签均对应一个映射接口的方法,每个<select>
,<insert>
,<update>
和<delete>
标签的id需要和映射接口的方法名一致; - 调用
Mybatis
映射接口的方法时,调用请求的实际执行是由基于JDK
动态代理为映射接口生成的代理对象来完成,映射接口的代理对象由MapperProxyFactory
的newInstance()
方法生成,每个映射接口对应一个MapperProxyFactory
; - 在
Mybatis
的JDK
动态代理中,是由MapperProxy
实现了InvocationHandler
接口,因此MapperProxy
在Mybatis
的JDK
动态代理中扮演调用处理器的角色,即调用映射接口的方法时,实际上是调用的MapperProxy
实现的invoke()
方法; - 在
Mybatis
的JDK
动态代理中,是不存在被代理对象的,可以理解为是对接口的代理,因此在MapperProxy
的invoke()
方法中,并没有去调用被代理对象的方法,而是会基于映射接口和被调用方法的方法对象生成MapperMethod
并执行MapperMethod
的execute()
方法,即调用映射接口的方法的请求会发送到MapperMethod
,可以理解为映射接口的方法由MapperMethod
代理。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。