目录
前言正文一. 映射文件/映射接口的配置二. 加载映射文件的源码分析三. MyBatis中的动态代理总结
前言
本篇文章将分析
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
代理。
到此这篇关于MyBatis加载映射文件和动态代理的实现的文章就介绍到这了,更多相关MyBatis加载映射文件和动态代理内容请搜索中国红客联盟以前的文章或继续浏览下面的相关文章希望大家以后多多支持中国红客联盟!