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,且在同一個(gè)<mapper>標(biāo)簽中,只能設(shè)置這三種屬性中的一種,否則會報(bào)錯(cuò)。resource和url屬性均是通過告訴MyBatis映射文件所在的位置路徑來注冊映射文件,前者使用相對路徑(相對于classpath,例如"mapper/BookMapper.xml"),后者使用絕對路徑。class屬性是通過告訴MyBatis映射文件對應(yīng)的映射接口的全限定名來注冊映射接口,此時(shí)要求映射文件與映射接口同名且同目錄。 |
<package> | 通過設(shè)置映射接口所在包名來注冊映射接口,此時(shí)要求映射文件與映射接口同名且同目錄。 |
根據(jù)上表所示,示例中的配置文件mybatis-config.xml是通過設(shè)置映射接口所在包名來注冊映射接口的,所以映射文件與映射接口需要同名且目錄,如下圖所示。
具體的原因會在下文的源碼分析中給出。
二. 加載映射文件的源碼分析
在淺析MyBatis的配置加載流程中已經(jīng)知道,使用MyBatis時(shí)會先讀取配置文件mybatis-config.xml為字符流或者字節(jié)流,然后通過SqlSessionFactoryBuilder基于配置文件的字符流或字節(jié)流來構(gòu)建SqlSessionFactory。
在這整個(gè)過程中,會解析mybatis-config.xml并將解析結(jié)果豐富進(jìn)Configuration,且Configuration在MyBatis中是一個(gè)單例,無論是配置文件的解析結(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的配置文件時(shí),會根據(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í)設(shè)置了mapper子節(jié)點(diǎn)的兩個(gè)及以上的屬性時(shí),報(bào)錯(cuò) 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)部有三個(gè)重載的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是一個(gè)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); } } } }
上面三個(gè)addMapper() 方法一層一層的調(diào)用下來,實(shí)際就是根據(jù)配置文件中<mappers>標(biāo)簽的<package>子標(biāo)簽設(shè)置的映射文件/映射接口所在包的全限定名來獲取映射接口的Class對象,然后基于每個(gè)映射接口的Class對象來創(chuàng)建一個(gè)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會為每個(gè)映射接口創(chuàng)建一個(gè)MapperProxyFactory,然后將映射接口與MapperProxyFactory以鍵值對的形式存儲在MapperRegistry的knownMappers緩存中,然后MapperProxyFactory會為映射接口基于JDK動態(tài)代理的方式生成代理類,至于如何生成,將在第三小節(jié)中對MapperProxyFactory進(jìn)一步分析。
繼續(xù)之前的流程,為映射接口創(chuàng)建完MapperProxyFactory之后,就應(yīng)該對映射文件和映射接口中的SQL進(jìn)行解析,解析依靠的類為MapperAnnotationBuilder,其類圖如下所示。
所以一個(gè)映射接口對應(yīng)一個(gè)MapperAnnotationBuilder,并且每個(gè)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還有一個(gè)名為sqlFragments的緩存,用于存儲<sql>標(biāo)簽對應(yīng)的XNode,這個(gè)sqlFragments和Configuration中的sqlFragments是同一份緩存,這一點(diǎn)切記,后面在分析處理<include>標(biāo)簽時(shí)會用到。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>下的各個(gè)子標(biāo)簽解析成相應(yīng)的類,然后緩存在Configuration中。通常,在映射文件的<mapper>標(biāo)簽下,常用的子標(biāo)簽為<parameterMap>,<resultMap>,<select>,<insert>,<update>和<delete>,下面給出一個(gè)簡單的表格對這些標(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) { // 每一個(gè)<select>,<insert>,<update>和<delete>標(biāo)簽均會被創(chuàng)建一個(gè)MappedStatement // 每個(gè)MappedStatement會存放在Configuration的mappedStatements緩存中 // mappedStatements是一個(gè)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); } } }
對于每一個(gè)<select>,<insert>,<update>和<delete>標(biāo)簽,均會創(chuàng)建一個(gè)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() 方法,在這個(gè)方法中,會根據(jù)配置文件中<mappers>標(biāo)簽的子標(biāo)簽的不同,進(jìn)入不同的分支執(zhí)行加載映射文件/映射接口的邏輯,實(shí)際上,整個(gè)加載映射文件/加載映射接口的流程是一個(gè)環(huán)形,可以用下圖進(jìn)行示意。
XMLConfigBuilder中的mapperElement() 方法的不同分支只是從不同的入口進(jìn)入整個(gè)加載的流程中,同時(shí)MyBatis會在每個(gè)操作執(zhí)行前判斷是否已經(jīng)做過當(dāng)前操作,做過就不再重復(fù)執(zhí)行,因此保證了整個(gè)環(huán)形處理流程只會執(zhí)行一遍,不會死循環(huán)。以及,如果是在項(xiàng)目中基于JavaConfig的方式來配置MyBatis,那么通常會直接對Configuration設(shè)置參數(shù)值,以及調(diào)用Configuration的addMappers(String packageName) 來加載映射文件/映射接口。
三. MyBatis中的動態(tài)代理
已知在MapperRegistry中有一個(gè)叫做knownMappers的map緩存,其鍵為映射接口的Class對象,值為MyBatis為映射接口創(chuàng)建的動態(tài)代理工廠MapperProxyFactory,當(dāng)調(diào)用映射接口定義的方法執(zhí)行數(shù)據(jù)庫操作時(shí),實(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是一個(gè)map緩存,其鍵為映射接口的方法對象,值為這個(gè)方法對應(yīng)的MapperMethodInvoker,實(shí)際上,SQL的執(zhí)行最終會由MapperMethodInvoker完成,后面會詳細(xì)說明。
現(xiàn)在再觀察MapperProxyFactory中兩個(gè)重載的newInstance() 方法,可以知道這是基于JDK的動態(tài)代理,在public T newInstance(SqlSession sqlSession) 這個(gè)方法中,會創(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時(shí)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)建一個(gè)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)代理生成的映射接口的代理對象的方法時(shí),最終調(diào)用請求會發(fā)送到MapperProxy的invoke() 方法,在MapperProxy的invoke() 方法中實(shí)際就是根據(jù)映射接口被調(diào)用的方法的對象去methodCache緩存中獲取MapperMethodInvoker來實(shí)際執(zhí)行請求,如果獲取不到那么就先為當(dāng)前的方法對象創(chuàng)建一個(gè)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)建一個(gè)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)建一個(gè)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時(shí),需要先創(chuàng)建MapperMethod,而PlainMethodInvoker在執(zhí)行時(shí)也是將執(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í)需要傳入的參數(shù)為映射接口的Class
對象,映射接口被調(diào)用的方法的對象和配置類Configuration
,在MapperMethod的構(gòu)造函數(shù)中,會基于上述三個(gè)參數(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,則直接返回這個(gè)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,同時(shí)resolveMappedStatement() 方法會從映射接口遞歸的遍歷到聲明被調(diào)用方法的接口,遞歸的終止條件如下所示。
- 根據(jù)接口全限定名 + "." + "方法名" 作為statementId去Configuration的緩存中獲取到了MappedStatement;
- 從映射接口遞歸遍歷到了聲明被調(diào)用方法的接口,且根據(jù)聲明被調(diào)用方法的接口的全限定名 + "." + "方法名" 作為statementId去Configuration的緩存中獲取不到MappedStatement。
上面說得比較繞,下面用一個(gè)例子說明一下resolveMappedStatement() 方法這樣寫的原因。下圖是映射接口和映射文件所在的包路徑。
BaseMapper,BookBaseMapper和BookMapper的關(guān)系如下圖所示。
那么MyBatis會為BaseMapper,BookBaseMapper和BookMapper都生成一個(gè)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í)行方法時(shí)的一個(gè)執(zhí)行鏈進(jìn)行說明。
首先,通過JDK動態(tài)代理的原理我們可以知道,調(diào)用代理對象的方法時(shí),調(diào)用請求會發(fā)送到代理對象中的InvocationHandler,在MyBatis中,調(diào)用映射接口的代理對象的方法的請求會發(fā)送到MapperProxy,所以調(diào)用映射接口的代理對象的方法時(shí),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)建一個(gè)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. 每個(gè)CRUD
標(biāo)簽唯一對應(yīng)一個(gè)MappedStatement
對象
具體對應(yīng)關(guān)系可以用下圖進(jìn)行示意。
映射文件中,每一個(gè)<select>,<insert>,<update>和<delete>標(biāo)簽均會被創(chuàng)建一個(gè)MappedStatement并存放在Configuration的mappedStatements緩存中,MappedStatement中主要包含著這個(gè)標(biāo)簽下的SQL語句,這個(gè)標(biāo)簽的參數(shù)信息和出參信息等。每一個(gè)MappedStatement的唯一標(biāo)識為namespace + "." + 標(biāo)簽id,這樣設(shè)置唯一標(biāo)識的原因是為了調(diào)用映射接口的方法時(shí)能夠根據(jù)映射接口的全限定名 + "." + "方法名"獲取到和被調(diào)用方法關(guān)聯(lián)的MappedStatement,因此,映射文件的namespace需要和映射接口的全限定名一致,每個(gè)<select>,<insert>,<update>和<delete>標(biāo)簽均對應(yīng)一個(gè)映射接口的方法,每個(gè)<select>,<insert>,<update>和<delete>標(biāo)簽的id需要和映射接口的方法名一致;
2. 每個(gè)映射接口對應(yīng)一個(gè)JDK
動態(tài)代理對象
調(diào)用MyBatis映射接口的方法時(shí),調(diào)用請求的實(shí)際執(zhí)行是由基于JDK動態(tài)代理為映射接口生成的代理對象來完成,映射接口的代理對象由MapperProxyFactory的newInstance() 方法生成,每個(gè)映射接口對應(yīng)一個(gè)MapperProxyFactory,對應(yīng)一個(gè)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í),實(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-12SpringBoot中驗(yàn)證用戶上傳的圖片資源的方法
這篇文章主要介紹了在SpringBoot中驗(yàn)證用戶上傳的圖片資源,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-09-09Spring?@Conditional通過條件控制bean注冊過程
這篇文章主要為大家介紹了Spring?@Conditional通過條件控制bean注冊過程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02基于idea 的 Java中的get/set方法之優(yōu)雅的寫法
這篇文章主要介紹了基于idea 的 Java中的get/set方法之優(yōu)雅的寫法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2019-01-01Java日常練習(xí)題,每天進(jìn)步一點(diǎn)點(diǎn)(18)
下面小編就為大家?guī)硪黄狫ava基礎(chǔ)的幾道練習(xí)題(分享)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧,希望可以幫到你2021-07-07