MyBatis加載映射文件和動態(tài)代理的實(shí)現(xiàn)
前言
本篇文章將分析MyBatis在配置文件加載的過程中,如何解析映射文件中的SQL語句以及每條SQL語句如何與映射接口的方法進(jìn)行關(guān)聯(lián)。
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節(jié)點(diǎn)用于配置映射文件/映射接口,mappers節(jié)點(diǎn)下有兩種子節(jié)點(diǎn),標(biāo)簽分別為<mapper>和<package>,這兩種標(biāo)簽的說明如下所示。
| 標(biāo)簽 | 說明 |
|---|---|
| <mapper> | 該標(biāo)簽有三種屬性,分別為resource,url和class,且在同一個<mapper>標(biāo)簽中,只能設(shè)置這三種屬性中的一種,否則會報錯。resource和url屬性均是通過告訴MyBatis映射文件所在的位置路徑來注冊映射文件,前者使用相對路徑(相對于classpath,例如"mapper/BookMapper.xml"),后者使用絕對路徑。class屬性是通過告訴MyBatis映射文件對應(yīng)的映射接口的全限定名來注冊映射接口,此時要求映射文件與映射接口同名且同目錄。 |
| <package> | 通過設(shè)置映射接口所在包名來注冊映射接口,此時要求映射文件與映射接口同名且同目錄。 |
根據(jù)上表所示,示例中的配置文件mybatis-config.xml是通過設(shè)置映射接口所在包名來注冊映射接口的,所以映射文件與映射接口需要同名且目錄,如下圖所示。

具體的原因會在下文的源碼分析中給出。
二. 加載映射文件的源碼分析
在淺析MyBatis的配置加載流程中已經(jīng)知道,使用MyBatis時會先讀取配置文件mybatis-config.xml為字符流或者字節(jié)流,然后通過SqlSessionFactoryBuilder基于配置文件的字符流或字節(jié)流來構(gòu)建SqlSessionFactory。
在這整個過程中,會解析mybatis-config.xml并將解析結(jié)果豐富進(jìn)Configuration,且Configuration在MyBatis中是一個單例,無論是配置文件的解析結(jié)果,還是映射文件的解析結(jié)果,亦或者是映射接口的解析結(jié)果,最終都會緩存在Configuration中。
接著淺析MyBatis的配置加載流程這篇文章末尾繼續(xù)講,配置文件的解析發(fā)生在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"));
// 根據(jù)mappers標(biāo)簽的屬性,找到映射文件/映射接口并解析
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}如上所示,在解析MyBatis的配置文件時,會根據(jù)配置文件中的<mappers>標(biāo)簽的屬性來找到映射文件/映射接口并進(jìn)行解析。如下是mapperElement() 方法的實(shí)現(xiàn)。
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
// 處理package子節(jié)點(diǎn)
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) {
// 處理設(shè)置了resource屬性的mapper子節(jié)點(diǎn)
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) {
// 處理設(shè)置了url屬性的mapper子節(jié)點(diǎn)
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) {
// 處理設(shè)置了class屬性的mapper子節(jié)點(diǎn)
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
// 同時設(shè)置了mapper子節(jié)點(diǎn)的兩個及以上的屬性時,報錯
throw new BuilderException(
"A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}結(jié)合示例中的配置文件,那么在mapperElement() 方法中應(yīng)該進(jìn)入處理package子節(jié)點(diǎn)的分支,所以繼續(xù)往下看,Configuration的addMappers(String packageName) 方法如下所示。
public void addMappers(String packageName) {
mapperRegistry.addMappers(packageName);
}mapperRegistry是Configuration內(nèi)部的成員變量,其內(nèi)部有三個重載的addMappers() 方法,首先看addMappers(String packageName) 方法,如下所示。
public void addMappers(String packageName) {
addMappers(packageName, Object.class);
}繼續(xù)往下,addMappers(String packageName, Class<?> superType) 的實(shí)現(xiàn)如下所示。
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) 的實(shí)現(xiàn),如下所示。
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
// 判斷knownMappers中是否已經(jīng)有當(dāng)前映射接口
// knownMappers是一個map存儲結(jié)構(gòu),key為映射接口Class對象,value為MapperProxyFactory
// MapperProxyFactory為映射接口對應(yīng)的動態(tài)代理工廠
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() 方法一層一層的調(diào)用下來,實(shí)際就是根據(jù)配置文件中<mappers>標(biāo)簽的<package>子標(biāo)簽設(shè)置的映射文件/映射接口所在包的全限定名來獲取映射接口的Class對象,然后基于每個映射接口的Class對象來創(chuàng)建一個MapperProxyFactory,顧名思義,MapperProxyFactory是映射接口的動態(tài)代理工廠,負(fù)責(zé)為對應(yīng)的映射接口生成動態(tài)代理類,這里先簡要看一下MapperProxyFactory的實(shí)現(xiàn)。
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);
}
}很標(biāo)準(zhǔn)的基于JDK動態(tài)代理的實(shí)現(xiàn),所以可以知道,MyBatis會為每個映射接口創(chuàng)建一個MapperProxyFactory,然后將映射接口與MapperProxyFactory以鍵值對的形式存儲在MapperRegistry的knownMappers緩存中,然后MapperProxyFactory會為映射接口基于JDK動態(tài)代理的方式生成代理類,至于如何生成,將在第三小節(jié)中對MapperProxyFactory進(jìn)一步分析。
繼續(xù)之前的流程,為映射接口創(chuàng)建完MapperProxyFactory之后,就應(yīng)該對映射文件和映射接口中的SQL進(jìn)行解析,解析依靠的類為MapperAnnotationBuilder,其類圖如下所示。

所以一個映射接口對應(yīng)一個MapperAnnotationBuilder,并且每個MapperAnnotationBuilder中持有全局唯一的Configuration類,解析結(jié)果會豐富進(jìn)Configuration中。MapperAnnotationBuilder的解析方法parse() 如下所示。
public void parse() {
String resource = type.toString();
// 判斷映射接口是否解析過,沒解析過才繼續(xù)往下執(zhí)行
if (!configuration.isResourceLoaded(resource)) {
// 先解析映射文件中的Sql語句
loadXmlResource();
// 將當(dāng)前映射接口添加到緩存中,以表示當(dāng)前映射接口已經(jīng)被解析過
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() 方法的執(zhí)行流程,會先解析映射文件中的SQL語句,然后再解析映射接口中的SQL語句,這里以解析映射文件為例,進(jìn)行說明。loadXmlResource() 方法實(shí)現(xiàn)如下。
private void loadXmlResource() {
if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
// 根據(jù)映射接口的全限定名拼接成映射文件的路徑
// 這也解釋了為什么要求映射文件和映射接口在同一目錄
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() 方法中,首先要根據(jù)映射接口的全限定名拼接出映射文件的路徑,拼接規(guī)則就是將全限定名的"."替換成"/",然后在末尾加上".xml",這也是為什么要求映射文件和映射接口需要在同一目錄下且同名。對于映射文件的解析,是依靠XMLMapperBuilder,其類圖如下所示。

如圖所示,解析配置文件和解析映射文件的解析類均繼承于BaseBuilder,然后BaseBuilder中持有全局唯一的Configuration,所以解析結(jié)果會豐富進(jìn)Configuration,特別注意,XMLMapperBuilder還有一個名為sqlFragments的緩存,用于存儲<sql>標(biāo)簽對應(yīng)的XNode,這個sqlFragments和Configuration中的sqlFragments是同一份緩存,這一點(diǎn)切記,后面在分析處理<include>標(biāo)簽時會用到。XMLMapperBuilder的parse() 方法如下所示。
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
// 從映射文件的<mapper>標(biāo)簽開始進(jìn)行解析
// 解析結(jié)果會豐富進(jìn)Configuration
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}繼續(xù)看configurationElement() 方法的實(shí)現(xiàn),如下所示。
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>標(biāo)簽生成ParameterMap并緩存到Configuration
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// 解析<resultMap>標(biāo)簽生成ResultMap并緩存到Configuration
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 將<sql>標(biāo)簽對應(yīng)的節(jié)點(diǎn)XNode保存到sqlFragments中
// 實(shí)際也是保存到Configuration的sqlFragments緩存中
sqlElement(context.evalNodes("/mapper/sql"));
// 解析<select>,<insert>,<update>和<delete>標(biāo)簽
// 生成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>下的各個子標(biāo)簽解析成相應(yīng)的類,然后緩存在Configuration中。通常,在映射文件的<mapper>標(biāo)簽下,常用的子標(biāo)簽為<parameterMap>,<resultMap>,<select>,<insert>,<update>和<delete>,下面給出一個簡單的表格對這些標(biāo)簽生成的類以及在Configuration中的唯一標(biāo)識進(jìn)行歸納。
| 標(biāo)簽 | 解析生成的類 | 在Configuration中的唯一標(biāo)識 |
|---|---|---|
| <parameterMap> | ParameterMap | namespace + "." + 標(biāo)簽id |
| <resultMap> | ResultMap | namespace + "." + 標(biāo)簽id |
| <select>,<insert>,<update>,<delete> | MappedStatement | namespace + "." + 標(biāo)簽id |
上面表格中的namespace是映射文件<mapper>標(biāo)簽的namespace屬性,因此對于映射文件里配置的parameterMap,resultMap或者SQL執(zhí)行語句,在MyBatis中的唯一標(biāo)識就是namespace + "." + 標(biāo)簽id。下圖可以直觀的展示<select>標(biāo)簽解析后在Configuration中的形態(tài)。

下面以如何解析<select>,<insert>,<update>和<delete>標(biāo)簽的內(nèi)容為例,進(jìn)行說明,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>標(biāo)簽均會被創(chuàng)建一個MappedStatement
// 每個MappedStatement會存放在Configuration的mappedStatements緩存中
// mappedStatements是一個map,鍵為映射接口全限定名+"."+標(biāo)簽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>標(biāo)簽,均會創(chuàng)建一個XMLStatementBuilder來進(jìn)行解析并生成MappedStatement,同樣,看一下XMLStatementBuilder的類圖,如下所示。

XMLStatementBuilder中持有<select>,<insert>,<update>和<delete>標(biāo)簽對應(yīng)的節(jié)點(diǎn)XNode,以及幫助創(chuàng)建MappedStatement并豐富進(jìn)Configuration的MapperBuilderAssistant類。下面看一下XMLStatementBuilder的parseStatementNode() 方法。
public void parseStatementNode() {
// 獲取標(biāo)簽id
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
String nodeName = context.getNode().getNodeName();
// 獲取標(biāo)簽的類型,例如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>標(biāo)簽,則將<include>標(biāo)簽替換為匹配的<sql>標(biāo)簽中的Sql片段
// 匹配規(guī)則是在Configuration中根據(jù)namespace+"."+refid去匹配<sql>標(biāo)簽
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// 獲取輸入?yún)?shù)類型
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
// 獲取LanguageDriver以支持實(shí)現(xiàn)動態(tài)Sql
// 這里獲取到的實(shí)際上為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 {
// 緩存中如果獲取不到,則根據(jù)useGeneratedKeys的配置決定是否使用KeyGenerator
// 如果要使用,則MyBatis中使用的KeyGenerator為Jdbc3KeyGenerator
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
// 通過XMLLanguageDriver創(chuàng)建SqlSource,可以理解為Sql語句
// 如果使用到了<if>,<foreach>等標(biāo)簽進(jìn)行動態(tài)Sql語句的拼接,則創(chuàng)建出來的SqlSource為DynamicSqlSource
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
StatementType statementType = StatementType
.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
// 獲取<select>,<insert>,<update>和<delete>標(biāo)簽上的屬性
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");
// 根據(jù)上面獲取到的參數(shù),創(chuàng)建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() 方法整體流程稍長,總結(jié)概括起來該方法做了如下幾件事情。
- 將<include>標(biāo)簽替換為其指向的SQL片段;
- 如果未使用動態(tài)SQL,則創(chuàng)建RawSqlSource以保存SQL語句,如果使用了動態(tài)SQL(例如使用了<if>,<foreach>等標(biāo)簽),則創(chuàng)建DynamicSqlSource以支持SQL語句的動態(tài)拼接;
- 獲取<select>,<insert>,<update>和<delete>標(biāo)簽上的屬性;
- 將獲取到的SqlSource以及標(biāo)簽上的屬性傳入MapperBuilderAssistant的addMappedStatement() 方法,以創(chuàng)建MappedStatement并添加到Configuration中。
MapperBuilderAssistant是最終創(chuàng)建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的唯一標(biāo)識
// 規(guī)則是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);
}
// 創(chuàng)建MappedStatement
MappedStatement statement = statementBuilder.build();
// 將MappedStatement添加到Configuration中
configuration.addMappedStatement(statement);
return statement;
}至此,解析<select>,<insert>,<update>和<delete>標(biāo)簽的內(nèi)容然后生成MappedStatement并添加到Configuration的流程分析完畢,實(shí)際上,解析<parameterMap>標(biāo)簽,解析<resultMap>標(biāo)簽的大體流程和上面基本一致,最終都是借助MapperBuilderAssistant生成對應(yīng)的類(例如ParameterMap,ResultMap)然后再緩存到Configuration中,且每種解析生成的類在對應(yīng)緩存中的唯一標(biāo)識為namespace + "." + 標(biāo)簽id。
最后,回到本小節(jié)開頭,即XMLConfigBuilder中的mapperElement() 方法,在這個方法中,會根據(jù)配置文件中<mappers>標(biāo)簽的子標(biāo)簽的不同,進(jìn)入不同的分支執(zhí)行加載映射文件/映射接口的邏輯,實(shí)際上,整個加載映射文件/加載映射接口的流程是一個環(huán)形,可以用下圖進(jìn)行示意。

XMLConfigBuilder中的mapperElement() 方法的不同分支只是從不同的入口進(jìn)入整個加載的流程中,同時MyBatis會在每個操作執(zhí)行前判斷是否已經(jīng)做過當(dāng)前操作,做過就不再重復(fù)執(zhí)行,因此保證了整個環(huán)形處理流程只會執(zhí)行一遍,不會死循環(huán)。以及,如果是在項(xiàng)目中基于JavaConfig的方式來配置MyBatis,那么通常會直接對Configuration設(shè)置參數(shù)值,以及調(diào)用Configuration的addMappers(String packageName) 來加載映射文件/映射接口。
三. MyBatis中的動態(tài)代理
已知在MapperRegistry中有一個叫做knownMappers的map緩存,其鍵為映射接口的Class對象,值為MyBatis為映射接口創(chuàng)建的動態(tài)代理工廠MapperProxyFactory,當(dāng)調(diào)用映射接口定義的方法執(zhí)行數(shù)據(jù)庫操作時,實(shí)際調(diào)用請求會由MapperProxyFactory為映射接口生成的代理對象來完成。這里給出MapperProxyFactory的實(shí)現(xiàn),如下所示。
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緩存,其鍵為映射接口的方法對象,值為這個方法對應(yīng)的MapperMethodInvoker,實(shí)際上,SQL的執(zhí)行最終會由MapperMethodInvoker完成,后面會詳細(xì)說明。
現(xiàn)在再觀察MapperProxyFactory中兩個重載的newInstance() 方法,可以知道這是基于JDK的動態(tài)代理,在public T newInstance(SqlSession sqlSession) 這個方法中,會創(chuàng)建MapperProxy,并將其作為參數(shù)調(diào)用protected T newInstance(MapperProxy<T> mapperProxy) 方法,在該方法中會使用Proxy的newProxyInstance() 方法創(chuàng)建動態(tài)代理對象,所以可以斷定,MapperProxy肯定會實(shí)現(xiàn)InvocationHandler接口,MapperProxy的類圖如下所示。

果然,MapperProxy實(shí)現(xiàn)了InvocationHandler接口,并在創(chuàng)建MapperProxy時MapperProxyFactory會將其持有的methodCache傳遞給MapperProxy,因此methodCache的實(shí)際的讀寫是由MapperProxy來完成。下面看一下MapperProxy實(shí)現(xiàn)的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中根據(jù)方法對象獲取MapperMethodInvoker來執(zhí)行Sql
// 如果獲取不到,則創(chuàng)建一個MapperMethodInvoker并添加到methodCache中,再執(zhí)行Sql
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}基于JDK動態(tài)代理的原理可以知道,當(dāng)調(diào)用JDK動態(tài)代理生成的映射接口的代理對象的方法時,最終調(diào)用請求會發(fā)送到MapperProxy的invoke() 方法,在MapperProxy的invoke() 方法中實(shí)際就是根據(jù)映射接口被調(diào)用的方法的對象去methodCache緩存中獲取MapperMethodInvoker來實(shí)際執(zhí)行請求,如果獲取不到那么就先為當(dāng)前的方法對象創(chuàng)建一個MapperMethodInvoker并加入methodCache緩存,然后再用創(chuàng)建出來的MapperMethodInvoker去執(zhí)行請求。cachedInvoker() 方法實(shí)現(xiàn)如下所示。
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
MapperProxy.MapperMethodInvoker invoker = methodCache.get(method);
// 從methodCache緩存中獲取到MapperMethodInvoker不為空則直接返回
if (invoker != null) {
return invoker;
}
// 從methodCache緩存中獲取到MapperMethodInvoker為空
// 則創(chuàng)建一個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 {
// 先創(chuàng)建一個MapperMethod
// 再將MapperMethod作為參數(shù)創(chuàng)建PlainMethodInvoker
return new MapperProxy.PlainMethodInvoker(
new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
});
} catch (RuntimeException re) {
Throwable cause = re.getCause();
throw cause == null ? re : cause;
}
}MapperMethodInvoker是接口,通常創(chuàng)建出來的MapperMethodInvoker為PlainMethodInvoker,看一下PlainMethodInvoker的構(gòu)造函數(shù)。
public PlainMethodInvoker(MapperMethod mapperMethod) {
super();
this.mapperMethod = mapperMethod;
}因此創(chuàng)建PlainMethodInvoker時,需要先創(chuàng)建MapperMethod,而PlainMethodInvoker在執(zhí)行時也是將執(zhí)行的請求傳遞給MapperMethod,所以繼續(xù)往下,MapperMethod的構(gòu)造函數(shù)如下所示。
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}創(chuàng)建MapperMethod時需要傳入的參數(shù)為映射接口的Class對象,映射接口被調(diào)用的方法的對象和配置類Configuration,在MapperMethod的構(gòu)造函數(shù)中,會基于上述三個參數(shù)創(chuàng)建SqlCommand和MethodSignature:
- SqlCommand主要是保存和映射接口被調(diào)用方法所關(guān)聯(lián)的MappedStatement的信息;
- MethodSignature主要是存儲映射接口被調(diào)用方法的參數(shù)信息和返回值信息。
先看一下SqlCommand的構(gòu)造函數(shù),如下所示。
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
// 獲取映射接口被調(diào)用方法的方法名
final String methodName = method.getName();
// 獲取聲明被調(diào)用方法的接口的Class對象
final Class<?> declaringClass = method.getDeclaringClass();
// 獲取和映射接口被調(diào)用方法關(guān)聯(lián)的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);
}
}
}構(gòu)造函數(shù)中主要做了這些事情:
- 先獲取和被調(diào)用方法關(guān)聯(lián)的MappedStatement對象;
- 然后將MappedStatement的id字段賦值給SqlCommand的name字段;
- 最后將MappedStatement的sqlCommandType字段賦值給SqlCommand的type字段。
這樣一來,SqlCommand就具備了和被調(diào)用方法關(guān)聯(lián)的MappedStatement的信息。那么如何獲取和被調(diào)用方法關(guān)聯(lián)的MappedStatement對象呢,繼續(xù)看resolveMappedStatement() 的實(shí)現(xiàn),如下所示。
private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
Class<?> declaringClass, Configuration configuration) {
// 根據(jù)接口全限定名+"."+方法名拼接出MappedStatement的id
String statementId = mapperInterface.getName() + "." + methodName;
// 如果Configuration中緩存了statementId對應(yīng)的MappedStatement,則直接返回這個MappedStatement
// 這是遞歸的終止條件之一
if (configuration.hasStatement(statementId)) {
return configuration.getMappedStatement(statementId);
} else if (mapperInterface.equals(declaringClass)) {
// 當(dāng)前mapperInterface已經(jīng)是聲明被調(diào)用方法的接口的Class對象,且未匹配到緩存的MappedStatement,返回null
// 這是resolveMappedStatement()遞歸的終止條件之一
return null;
}
// 遞歸調(diào)用
for (Class<?> superInterface : mapperInterface.getInterfaces()) {
if (declaringClass.isAssignableFrom(superInterface)) {
MappedStatement ms = resolveMappedStatement(superInterface, methodName,
declaringClass, configuration);
if (ms != null) {
return ms;
}
}
}
return null;
}resolveMappedStatement() 方法會根據(jù)接口全限定名 + "." + "方法名" 作為statementId去Configuration的緩存中獲取MappedStatement,同時resolveMappedStatement() 方法會從映射接口遞歸的遍歷到聲明被調(diào)用方法的接口,遞歸的終止條件如下所示。
- 根據(jù)接口全限定名 + "." + "方法名" 作為statementId去Configuration的緩存中獲取到了MappedStatement;
- 從映射接口遞歸遍歷到了聲明被調(diào)用方法的接口,且根據(jù)聲明被調(diào)用方法的接口的全限定名 + "." + "方法名" 作為statementId去Configuration的緩存中獲取不到MappedStatement。
上面說得比較繞,下面用一個例子說明一下resolveMappedStatement() 方法這樣寫的原因。下圖是映射接口和映射文件所在的包路徑。

BaseMapper,BookBaseMapper和BookMapper的關(guān)系如下圖所示。

那么MyBatis會為BaseMapper,BookBaseMapper和BookMapper都生成一個MapperProxyFactory,如下所示。

同樣,在Configuration中也會緩存著解析BookBaseMapper.xml映射文件所生成的MappedStatement,如下所示。

在MyBatis的3.4.2及以前的版本,只會根據(jù)映射接口的全限定名 + "." + 方法名和聲明被調(diào)用方法的接口的全限定名 + "." + 方法名去Configuration的mappedStatements緩存中獲取MappedStatement,那么按照這樣的邏輯,BookMapper對應(yīng)的SqlCommand就只會根據(jù)com.mybatis.learn.dao.BookMapper.selectAllBooks和com.mybatis.learn.dao.BaseMapper.selectAllBooks去mappedStatements緩存中獲取MappedStatement,那么結(jié)合上面圖示給出的mappedStatements緩存內(nèi)容,是無法獲取到MappedStatement的,因此在MyBatis的3.4.3及之后的版本中,采用了resolveMappedStatement() 方法中的邏輯,以支持繼承了映射接口的接口對應(yīng)的SqlCommand也能和映射接口對應(yīng)的MappedStatement相關(guān)聯(lián)。
對于SqlCommand的分析到此為止,而MapperMethod中的MethodSignature主要是用于存儲被調(diào)用方法的參數(shù)信息和返回值信息,這里也不再贅述。
最后對映射接口的代理對象執(zhí)行方法時的一個執(zhí)行鏈進(jìn)行說明。
首先,通過JDK動態(tài)代理的原理我們可以知道,調(diào)用代理對象的方法時,調(diào)用請求會發(fā)送到代理對象中的InvocationHandler,在MyBatis中,調(diào)用映射接口的代理對象的方法的請求會發(fā)送到MapperProxy,所以調(diào)用映射接口的代理對象的方法時,MapperProxy的invoke() 方法會執(zhí)行,實(shí)現(xiàn)如下所示。
@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中根據(jù)方法對象獲取MapperMethodInvoker來執(zhí)行Sql
// 如果獲取不到,則創(chuàng)建一個MapperMethodInvoker并添加到methodCache中,再執(zhí)行Sql
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}所以到這里,MyBatis就和傳統(tǒng)的JDK動態(tài)代理產(chǎn)生了一點(diǎn)差別,傳統(tǒng)JDK動態(tài)代理通常在其InvocationHandler中會在被代理對象方法執(zhí)行前和執(zhí)行后增加一些裝飾邏輯,而在MyBatis中,是不存在被代理對象的,只有被代理接口,所以也不存在調(diào)用被代理對象的方法這一邏輯,取而代之的是根據(jù)被調(diào)用方法的方法對象獲取MapperMethodInvoker并執(zhí)行其invoke() 方法,通常獲取到的是PlainMethodInvoker,所以繼續(xù)看PlainMethodInvoker的invoke() 方法,如下所示。
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
return mapperMethod.execute(sqlSession, args);
}PlainMethodInvoker的invoke() 方法也沒有什么邏輯,就是繼續(xù)調(diào)用其MapperMethod的execute() 方法,而通過上面的分析已經(jīng)知道,MapperMethod中的SqlCommand關(guān)聯(lián)著MappedStatement,而MappedStatement中包含著和被調(diào)用方法所關(guān)聯(lián)的SQL信息,結(jié)合著SqlSession,就可以完成對數(shù)據(jù)庫的操作。關(guān)于如何對數(shù)據(jù)庫操作,將在后續(xù)的文章中介紹,本篇文章對于MyBatis中的動態(tài)代理的分析就到此為止。
最后以一張圖歸納一下MyBatis中的動態(tài)代理執(zhí)行流程,如下所示。

總結(jié)
本篇文章總結(jié)如下。
1. 每個CRUD標(biāo)簽唯一對應(yīng)一個MappedStatement對象
具體對應(yīng)關(guān)系可以用下圖進(jìn)行示意。

映射文件中,每一個<select>,<insert>,<update>和<delete>標(biāo)簽均會被創(chuàng)建一個MappedStatement并存放在Configuration的mappedStatements緩存中,MappedStatement中主要包含著這個標(biāo)簽下的SQL語句,這個標(biāo)簽的參數(shù)信息和出參信息等。每一個MappedStatement的唯一標(biāo)識為namespace + "." + 標(biāo)簽id,這樣設(shè)置唯一標(biāo)識的原因是為了調(diào)用映射接口的方法時能夠根據(jù)映射接口的全限定名 + "." + "方法名"獲取到和被調(diào)用方法關(guān)聯(lián)的MappedStatement,因此,映射文件的namespace需要和映射接口的全限定名一致,每個<select>,<insert>,<update>和<delete>標(biāo)簽均對應(yīng)一個映射接口的方法,每個<select>,<insert>,<update>和<delete>標(biāo)簽的id需要和映射接口的方法名一致;
2. 每個映射接口對應(yīng)一個JDK動態(tài)代理對象
調(diào)用MyBatis映射接口的方法時,調(diào)用請求的實(shí)際執(zhí)行是由基于JDK動態(tài)代理為映射接口生成的代理對象來完成,映射接口的代理對象由MapperProxyFactory的newInstance() 方法生成,每個映射接口對應(yīng)一個MapperProxyFactory,對應(yīng)一個JDK動態(tài)代理對象;
3. MyBatis中的動態(tài)代理是對接口的代理
在MyBatis的JDK動態(tài)代理中,是不存在被代理對象的,是對接口的代理。MapperProxy實(shí)現(xiàn)了InvocationHandler接口,因此MapperProxy在MyBatis的JDK動態(tài)代理中扮演調(diào)用處理器的角色,即調(diào)用映射接口的方法時,實(shí)際上是調(diào)用的MapperProxy實(shí)現(xiàn)的invoke() 方法,又因?yàn)椴淮嬖诒淮韺ο螅栽?strong>MapperProxy的invoke() 方法中,并沒有去調(diào)用被代理對象的方法,而是會基于映射接口和被調(diào)用方法的方法對象生成MapperMethod并執(zhí)行MapperMethod的execute() 方法,即調(diào)用映射接口的方法的請求會發(fā)送到MapperMethod。
可以理解為映射接口的方法由MapperMethod代理。
到此這篇關(guān)于MyBatis加載映射文件和動態(tài)代理的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)MyBatis加載映射文件和動態(tài)代理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring中的@CrossOrigin注冊處理方法源碼解析
這篇文章主要介紹了Spring中的@CrossOrigin注冊處理方法源碼解析,@CrossOrigin是基于@RequestMapping,@RequestMapping注釋方法掃描注冊的起點(diǎn)是equestMappingHandlerMapping.afterPropertiesSet(),需要的朋友可以參考下2023-12-12
SpringBoot中驗(yàn)證用戶上傳的圖片資源的方法
這篇文章主要介紹了在SpringBoot中驗(yàn)證用戶上傳的圖片資源,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-09-09
Spring?@Conditional通過條件控制bean注冊過程
這篇文章主要為大家介紹了Spring?@Conditional通過條件控制bean注冊過程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02
基于idea 的 Java中的get/set方法之優(yōu)雅的寫法
這篇文章主要介紹了基于idea 的 Java中的get/set方法之優(yōu)雅的寫法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-01-01
Java日常練習(xí)題,每天進(jìn)步一點(diǎn)點(diǎn)(18)
下面小編就為大家?guī)硪黄狫ava基礎(chǔ)的幾道練習(xí)題(分享)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧,希望可以幫到你2021-07-07

