前言
本篇文章将剖析Mybatis
在配置文件加载的过程中,如何解析映射文件中的SQL
语句以及每条SQL
语句如何与映射接口的办法进行关联。在看该局部源码之前,须要具备JDK
动静代理的相干常识,如果该局部不是很理解,能够先看Java根底-动静代理学习JDk
动静代理的原理。
注释
一. 映射文件/映射接口的配置
给出Mybatis
的配置文件mybatis-config.xml
如下所示。
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration> <settings> <setting name="useGeneratedKeys" value="true"/> </settings> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&serverTimezone=UTC&useSSL=false"/> <property name="username" value="root"/> <property name="password" value="root"/> </dataSource> </environment> </environments> <mappers> <package name="com.mybatis.learn.dao"/> </mappers></configuration>
上述配置文件的mappers节点用于配置映射文件/映射接口,mappers节点下有两种子节点,标签别离为<mapper>
和<package>
,这两种标签的阐明如下所示。
标签 | 阐明 |
---|---|
<mapper> | 该标签有三种属性,别离为resource,url和class,且在同一个<mapper> 标签中,只能设置这三种属性中的一种,否则会报错。resource和url属性均是通过通知Mybatis 映射文件所在的地位门路来注册映射文件,前者应用相对路径(绝对于classpath,例如"mapper/BookMapper.xml"),后者应用绝对路径。class属性是通过通知Mybatis 映射文件对应的映射接口的全限定名来注册映射接口,此时要求映射文件与映射接口同名且同目录。 |
<package> | 通过设置映射接口所在包名来注册映射接口,此时要求映射文件与映射接口同名且同目录。 |
依据上表所示,示例中的配置文件mybatis-config.xml
是通过设置映射接口所在包名来注册映射接口的,所以映射文件与映射接口须要同名且目录,如下图所示。
具体的起因会在下文的源码剖析中给出。
二. 加载映射文件的源码剖析
在Mybatis源码-配置加载中曾经晓得,应用Mybatis
时会先读取配置文件mybatis-config.xml
为字符流或者字节流,而后通过SqlSessionFactoryBuilder
基于配置文件的字符流或字节流来构建SqlSessionFactory
。在这整个过程中,会解析mybatis-config.xml
并将解析后果丰盛进Configuration
,且Configuration
在Mybatis
中是一个单例,无论是配置文件的解析后果,还是映射文件的解析后果,亦或者是映射接口的解析后果,最终都会存在Configuration
中。接着Mybatis源码-配置加载这篇文章开端持续讲,配置文件的解析产生在XMLConfigBuilder
的parseConfiguration()
办法中,如下所示。
private void parseConfiguration(XNode root) { try { propertiesElement(root.evalNode("properties")); Properties settings = settingsAsProperties(root.evalNode("settings")); loadCustomVfs(settings); loadCustomLogImpl(settings); typeAliasesElement(root.evalNode("typeAliases")); pluginElement(root.evalNode("plugins")); objectFactoryElement(root.evalNode("objectFactory")); objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); reflectorFactoryElement(root.evalNode("reflectorFactory")); settingsElement(settings); environmentsElement(root.evalNode("environments")); databaseIdProviderElement(root.evalNode("databaseIdProvider")); typeHandlerElement(root.evalNode("typeHandlers")); //依据mappers标签的属性,找到映射文件/映射接口并解析 mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); }}
如上所示,在解析Mybatis
的配置文件时,会依据配置文件中的<mappers>
标签的属性来找到映射文件/映射接口并进行解析。如下是mapperElement()
办法的实现。
private void mapperElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { if ("package".equals(child.getName())) { //解决package子节点 String mapperPackage = child.getStringAttribute("name"); configuration.addMappers(mapperPackage); } else { String resource = child.getStringAttribute("resource"); String url = child.getStringAttribute("url"); String mapperClass = child.getStringAttribute("class"); if (resource != null && url == null && mapperClass == null) { //解决设置了resource属性的mapper子节点 ErrorContext.instance().resource(resource); InputStream inputStream = Resources.getResourceAsStream(resource); XMLMapperBuilder mapperParser = new XMLMapperBuilder( inputStream, configuration, resource, configuration.getSqlFragments()); mapperParser.parse(); } else if (resource == null && url != null && mapperClass == null) { //解决设置了url属性的mapper子节点 ErrorContext.instance().resource(url); InputStream inputStream = Resources.getUrlAsStream(url); XMLMapperBuilder mapperParser = new XMLMapperBuilder( inputStream, configuration, url, configuration.getSqlFragments()); mapperParser.parse(); } else if (resource == null && url == null && mapperClass != null) { //解决设置了class属性的mapper子节点 Class<?> mapperInterface = Resources.classForName(mapperClass); configuration.addMapper(mapperInterface); } else { //同时设置了mapper子节点的两个及以上的属性时,报错 throw new BuilderException( "A mapper element may only specify a url, resource or class, but not more than one."); } } } }}
联合示例中的配置文件,那么在mapperElement()
办法中应该进入解决package子节点的分支,所以持续往下看,Configuration
的addMappers(String packageName)
办法如下所示。
public void addMappers(String packageName) { mapperRegistry.addMappers(packageName);}
mapperRegistry是Configuration
外部的成员变量,其外部有三个重载的addMappers()
办法,首先看addMappers(String packageName)
办法,如下所示。
public void addMappers(String packageName) { addMappers(packageName, Object.class);}
持续往下,addMappers(String packageName, Class<?> superType)
的实现如下所示。
public void addMappers(String packageName, Class<?> superType) { ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>(); resolverUtil.find(new ResolverUtil.IsA(superType), packageName); //获取包门路下的映射接口的Class对象 Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses(); for (Class<?> mapperClass : mapperSet) { addMapper(mapperClass); }}
最初,再看下addMapper(Class<T> type)
的实现,如下所示。
public <T> void addMapper(Class<T> type) { if (type.isInterface()) { //判断knownMappers中是否曾经有以后映射接口 //knownMappers是一个map存储构造,key为映射接口Class对象,value为MapperProxyFactory //MapperProxyFactory为映射接口对应的动静代理工厂 if (hasMapper(type)) { throw new BindingException("Type " + type + " is already known to the MapperRegistry."); } boolean loadCompleted = false; try { knownMappers.put(type, new MapperProxyFactory<>(type)); //依附MapperAnnotationBuilder来实现映射文件和映射接口中的Sql解析 //先解析映射文件,再解析映射接口 MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); parser.parse(); loadCompleted = true; } finally { if (!loadCompleted) { knownMappers.remove(type); } } }}
下面三个addMapper()
办法一层一层的调用下来,理论就是依据配置文件中<mappers>
标签的<package>
子标签设置的映射文件/映射接口所在包的全限定名来获取映射接口的Class
对象,而后基于每个映射接口的Class
对象来创立一个MapperProxyFactory
,顾名思义,MapperProxyFactory
是映射接口的动静代理工厂,负责为对应的映射接口生成动静代理类,这里先简要看一下MapperProxyFactory
的实现。
public class MapperProxyFactory<T> { private final Class<T> mapperInterface; private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>(); public MapperProxyFactory(Class<T> mapperInterface) { this.mapperInterface = mapperInterface; } public Class<T> getMapperInterface() { return mapperInterface; } public Map<Method, MapperMethodInvoker> getMethodCache() { return methodCache; } @SuppressWarnings("unchecked") protected T newInstance(MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance( mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } public T newInstance(SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<>( sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); }}
很规范的基于JDK
动静代理的实现,所以能够晓得,Mybatis
会为每个映射接口创立一个MapperProxyFactory
,而后将映射接口与MapperProxyFactory
以键值对的模式存储在MapperRegistry
的knownMappers缓存中,而后MapperProxyFactory
会为映射接口基于JDK
动静代理的形式生成代理类,至于如何生成,将在第三大节中对MapperProxyFactory
进一步剖析。
持续之前的流程,为映射接口创立完MapperProxyFactory
之后,就应该对映射文件和映射接口中的SQL
进行解析,解析依附的类为MapperAnnotationBuilder
,其类图如下所示。
所以一个映射接口对应一个MapperAnnotationBuilder
,并且每个MapperAnnotationBuilder
中持有全局惟一的Configuration
类,解析后果会丰盛进Configuration
中。MapperAnnotationBuilder
的解析办法parse()
如下所示。
public void parse() { String resource = type.toString(); //判断映射接口是否解析过,没解析过才持续往下执行 if (!configuration.isResourceLoaded(resource)) { //先解析映射文件中的Sql语句 loadXmlResource(); //将以后映射接口增加到缓存中,以示意以后映射接口曾经被解析过 configuration.addLoadedResource(resource); assistant.setCurrentNamespace(type.getName()); parseCache(); parseCacheRef(); //解析映射接口中的Sql语句 for (Method method : type.getMethods()) { if (!canHaveStatement(method)) { continue; } if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent() && method.getAnnotation(ResultMap.class) == null) { parseResultMap(method); } try { parseStatement(method); } catch (IncompleteElementException e) { configuration.addIncompleteMethod(new MethodResolver(this, method)); } } } parsePendingMethods();}
依照parse()
办法的执行流程,会先解析映射文件中的SQL
语句,而后再解析映射接口中的SQL
语句,这里以解析映射文件为例,进行阐明。loadXmlResource()
办法实现如下。
private void loadXmlResource() { if (!configuration.isResourceLoaded("namespace:" + type.getName())) { //依据映射接口的全限定名拼接成映射文件的门路 //这也解释了为什么要求映射文件和映射接口在同一目录 String xmlResource = type.getName().replace('.', '/') + ".xml"; InputStream inputStream = type.getResourceAsStream("/" + xmlResource); if (inputStream == null) { try { inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource); } catch (IOException e2) { } } if (inputStream != null) { XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName()); //解析映射文件 xmlParser.parse(); } }}
loadXmlResource()
办法中,首先要依据映射接口的全限定名拼接出映射文件的门路,拼接规定就是将全限定名的"."替换成"/",而后在开端加上".xml",这也是为什么要求映射文件和映射接口须要在同一目录下且同名。对于映射文件的解析,是依附XMLMapperBuilder
,其类图如下所示。
如图所示,解析配置文件和解析映射文件的解析类均继承于BaseBuilder
,而后BaseBuilder
中持有全局惟一的Configuration
,所以解析后果会丰盛进Configuration
,特地留神,XMLMapperBuilder
还有一个名为sqlFragments的缓存,用于存储<sql>
标签对应的XNode
,这个sqlFragments和Configuration
中的sqlFragments是同一份缓存,这一点切记,前面在剖析解决<include>
标签时会用到。XMLMapperBuilder
的parse()
办法如下所示。
public void parse() { if (!configuration.isResourceLoaded(resource)) { //从映射文件的<mapper>标签开始进行解析 //解析后果会丰盛进Configuration configurationElement(parser.evalNode("/mapper")); configuration.addLoadedResource(resource); bindMapperForNamespace(); } parsePendingResultMaps(); parsePendingCacheRefs(); parsePendingStatements();}
持续看configurationElement()
办法的实现,如下所示。
private void configurationElement(XNode context) { try { String namespace = context.getStringAttribute("namespace"); if (namespace == null || namespace.isEmpty()) { throw new BuilderException("Mapper's namespace cannot be empty"); } builderAssistant.setCurrentNamespace(namespace); cacheRefElement(context.evalNode("cache-ref")); cacheElement(context.evalNode("cache")); //解析<parameterMap>标签生成ParameterMap并缓存到Configuration parameterMapElement(context.evalNodes("/mapper/parameterMap")); //解析<resultMap>标签生成ResultMap并缓存到Configuration resultMapElements(context.evalNodes("/mapper/resultMap")); //将<sql>标签对应的节点XNode保留到sqlFragments中 //理论也是保留到Configuration的sqlFragments缓存中 sqlElement(context.evalNodes("/mapper/sql")); //解析<select>,<insert>,<update>和<delete>标签 //生成MappedStatement并缓存到Configuration buildStatementFromContext(context.evalNodes("select|insert|update|delete")); } catch (Exception e) { throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e); }}
configurationElement()
办法会将映射文件<mapper>
下的各个子标签解析成相应的类,而后缓存在Configuration
中。通常,在映射文件的<mapper>
标签下,罕用的子标签为<parameterMap>
,<resultMap>
,<select>
,<insert>
,<update>
和<delete>
,上面给出一个简略的表格对这些标签生成的类以及在Configuration
中的惟一标识进行演绎。
标签 | 解析生成的类 | 在Configuration 中的惟一标识 |
---|---|---|
<parameterMap> | ParameterMap | namespace + "." + 标签id |
<resultMap> | ResultMap | namespace + "." + 标签id |
<select> ,<insert> ,<update> ,<delete> | MappedStatement | namespace + "." + 标签id |
下面表格中的namespace是映射文件<mapper>
标签的namespace属性,因而对于映射文件里配置的parameterMap,resultMap或者SQL
执行语句,在Mybatis
中的惟一标识就是namespace + "." + 标签id。上面以如何解析<select>
,<insert>
,<update>
和<delete>
标签的内容为例,进行阐明,buildStatementFromContext()
办法如下所示。
private void buildStatementFromContext(List<XNode> list) { if (configuration.getDatabaseId() != null) { buildStatementFromContext(list, configuration.getDatabaseId()); } buildStatementFromContext(list, null);}private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) { //每一个<select>,<insert>,<update>和<delete>标签均会被创立一个MappedStatement //每个MappedStatement会寄存在Configuration的mappedStatements缓存中 //mappedStatements是一个map,键为映射接口全限定名+"."+标签id,值为MappedStatement for (XNode context : list) { final XMLStatementBuilder statementParser = new XMLStatementBuilder( configuration, builderAssistant, context, requiredDatabaseId); try { statementParser.parseStatementNode(); } catch (IncompleteElementException e) { configuration.addIncompleteStatement(statementParser); } }}
对于每一个<select>
,<insert>
,<update>
和<delete>
标签,均会创立一个XMLStatementBuilder
来进行解析并生成MappedStatement
,同样,看一下XMLStatementBuilder
的类图,如下所示。
XMLStatementBuilder
中持有<select>
,<insert>
,<update>
和<delete>
标签对应的节点XNode
,以及帮忙创立MappedStatement
并丰盛进Configuration
的MapperBuilderAssistant
类。上面看一下XMLStatementBuilder
的parseStatementNode()
办法。
public void parseStatementNode() { //获取标签id String id = context.getStringAttribute("id"); String databaseId = context.getStringAttribute("databaseId"); if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) { return; } String nodeName = context.getNode().getNodeName(); //获取标签的类型,例如SELECT,INSERT等 SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH)); boolean isSelect = sqlCommandType == SqlCommandType.SELECT; boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect); boolean useCache = context.getBooleanAttribute("useCache", isSelect); boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false); //如果应用了<include>标签,则将<include>标签替换为匹配的<sql>标签中的Sql片段 //匹配规定是在Configuration中依据namespace+"."+refid去匹配<sql>标签 XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant); includeParser.applyIncludes(context.getNode()); //获取输出参数类型 String parameterType = context.getStringAttribute("parameterType"); Class<?> parameterTypeClass = resolveClass(parameterType); //获取LanguageDriver以反对实现动静Sql //这里获取到的实际上为XMLLanguageDriver String lang = context.getStringAttribute("lang"); LanguageDriver langDriver = getLanguageDriver(lang); processSelectKeyNodes(id, parameterTypeClass, langDriver); //获取KeyGenerator KeyGenerator keyGenerator; String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX; keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true); //先从缓存中获取KeyGenerator if (configuration.hasKeyGenerator(keyStatementId)) { keyGenerator = configuration.getKeyGenerator(keyStatementId); } else { //缓存中如果获取不到,则依据useGeneratedKeys的配置决定是否应用KeyGenerator //如果要应用,则Mybatis中应用的KeyGenerator为Jdbc3KeyGenerator keyGenerator = context.getBooleanAttribute("useGeneratedKeys", configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE; } //通过XMLLanguageDriver创立SqlSource,能够了解为Sql语句 //如果应用到了<if>,<foreach>等标签进行动静Sql语句的拼接,则创立进去的SqlSource为DynamicSqlSource SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass); StatementType statementType = StatementType .valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString())); //获取<select>,<insert>,<update>和<delete>标签上的属性 Integer fetchSize = context.getIntAttribute("fetchSize"); Integer timeout = context.getIntAttribute("timeout"); String parameterMap = context.getStringAttribute("parameterMap"); String resultType = context.getStringAttribute("resultType"); Class<?> resultTypeClass = resolveClass(resultType); String resultMap = context.getStringAttribute("resultMap"); String resultSetType = context.getStringAttribute("resultSetType"); ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType); if (resultSetTypeEnum == null) { resultSetTypeEnum = configuration.getDefaultResultSetType(); } String keyProperty = context.getStringAttribute("keyProperty"); String keyColumn = context.getStringAttribute("keyColumn"); String resultSets = context.getStringAttribute("resultSets"); //依据下面获取到的参数,创立MappedStatement并增加到Configuration中 builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered, keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);}
parseStatementNode()
办法整体流程稍长,总结概括起来该办法做了如下几件事件。
- 将
<include>
标签替换为其指向的SQL
片段; - 如果未应用动静
SQL
,则创立RawSqlSource
以保留SQL
语句,如果应用了动静SQL
(例如应用了<if>
,<foreach>
等标签),则创立DynamicSqlSource
以反对SQL
语句的动静拼接; - 获取
<select>
,<insert>
,<update>
和<delete>
标签上的属性; - 将获取到的
SqlSource
以及标签上的属性传入MapperBuilderAssistant
的addMappedStatement()
办法,以创立MappedStatement
并增加到Configuration
中。
MapperBuilderAssistant
是最终创立MappedStatement
以及将MappedStatement
增加到Configuration
的解决类,其addMappedStatement()
办法如下所示。
public MappedStatement addMappedStatement( String id, SqlSource sqlSource, StatementType statementType, SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout, String parameterMap, Class<?> parameterType, String resultMap, Class<?> resultType, ResultSetType resultSetType, boolean flushCache, boolean useCache, boolean resultOrdered, KeyGenerator keyGenerator, String keyProperty, String keyColumn, String databaseId, LanguageDriver lang, String resultSets) { if (unresolvedCacheRef) { throw new IncompleteElementException("Cache-ref not yet resolved"); } //拼接出MappedStatement的惟一标识 //规定是namespace+"."+id id = applyCurrentNamespace(id, false); boolean isSelect = sqlCommandType == SqlCommandType.SELECT; MappedStatement.Builder statementBuilder = new MappedStatement .Builder(configuration, id, sqlSource, sqlCommandType) .resource(resource) .fetchSize(fetchSize) .timeout(timeout) .statementType(statementType) .keyGenerator(keyGenerator) .keyProperty(keyProperty) .keyColumn(keyColumn) .databaseId(databaseId) .lang(lang) .resultOrdered(resultOrdered) .resultSets(resultSets) .resultMaps(getStatementResultMaps(resultMap, resultType, id)) .resultSetType(resultSetType) .flushCacheRequired(valueOrDefault(flushCache, !isSelect)) .useCache(valueOrDefault(useCache, isSelect)) .cache(currentCache); ParameterMap statementParameterMap = getStatementParameterMap( parameterMap, parameterType, id); if (statementParameterMap != null) { statementBuilder.parameterMap(statementParameterMap); } //创立MappedStatement MappedStatement statement = statementBuilder.build(); //将MappedStatement增加到Configuration中 configuration.addMappedStatement(statement); return statement;}
至此,解析<select>
,<insert>
,<update>
和<delete>
标签的内容而后生成MappedStatement
并增加到Configuration
的流程剖析结束,实际上,解析<parameterMap>
标签,解析<resultMap>
标签的大体流程和下面基本一致,最终都是借助MapperBuilderAssistant
生成对应的类(例如ParameterMap
,ResultMap
)而后再缓存到Configuration
中,且每种解析生成的类在对应缓存中的惟一标识为namespace + "." + 标签id。
最初,回到本大节结尾,即XMLConfigBuilder
中的mapperElement()
办法,在这个办法中,会依据配置文件中<mappers>
标签的子标签的不同,进入不同的分支执行加载映射文件/映射接口的逻辑,实际上,整个加载映射文件/加载映射接口的流程是一个环形,能够用下图进行示意。
XMLConfigBuilder
中的mapperElement()
办法的不同分支只是从不同的入口进入整个加载的流程中,同时Mybatis
会在每个操作执行前判断是否曾经做过以后操作,做过就不再反复执行,因而保障了整个环形解决流程只会执行一遍,不会死循环。以及,如果是在我的项目中基于JavaConfig
的形式来配置Mybatis
,那么通常会间接对Configuration
设置参数值,以及调用Configuration
的addMappers(String packageName)
来加载映射文件/映射接口。
三. Mybatis中的动静代理
已知在MapperRegistry
中有一个叫做knownMappers的map缓存,其键为映射接口的Class
对象,值为Mybatis
为映射接口创立的动静代理工厂MapperProxyFactory
,当调用映射接口定义的办法执行数据库操作时,理论调用申请会由MapperProxyFactory
为映射接口生成的代理对象来实现。这里给出MapperProxyFactory
的实现,如下所示。
public class MapperProxyFactory<T> { private final Class<T> mapperInterface; private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>(); public MapperProxyFactory(Class<T> mapperInterface) { this.mapperInterface = mapperInterface; } public Class<T> getMapperInterface() { return mapperInterface; } public Map<Method, MapperMethodInvoker> getMethodCache() { return methodCache; } @SuppressWarnings("unchecked") protected T newInstance(MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance( mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } public T newInstance(SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<>( sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); }}
在MapperProxyFactory
中,mapperInterface为映射接口的Class
对象,methodCache是一个map缓存,其键为映射接口的办法对象,值为这个办法对应的MapperMethodInvoker
,实际上,SQL
的执行最终会由MapperMethodInvoker
实现,前面会具体阐明。当初再察看MapperProxyFactory
中两个重载的newInstance()
办法,能够晓得这是基于JDK
的动静代理,在public T newInstance(SqlSession sqlSession)
这个办法中,会创立MapperProxy
,并将其作为参数调用protected T newInstance(MapperProxy<T> mapperProxy)
办法,在该办法中会应用Proxy
的newProxyInstance()
办法创立动静代理对象,所以能够判定,MapperProxy
必定会实现InvocationHandler
接口,MapperProxy
的类图如下所示。
果然,MapperProxy
实现了InvocationHandler
接口,并在创立MapperProxy
时MapperProxyFactory
会将其持有的methodCache传递给MapperProxy
,因而methodCache的理论的读写是由MapperProxy
来实现。上面看一下MapperProxy
实现的invoke()
办法,如下所示。
@Overridepublic 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 MapperProxy.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()
办法会执行,实现如下所示。
@Overridepublic 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()
办法,如下所示。
@Overridepublic Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable { return mapperMethod.execute(sqlSession, args);}
PlainMethodInvoker
的invoke()
办法也没有什么逻辑,就是持续调用其MapperMethod
的execute()
办法,而通过下面的剖析曾经晓得,MapperMethod
中的SqlCommand
关联着MappedStatement
,而MappedStatement
中蕴含着和被调用办法所关联的SQL
信息,联合着SqlSession
,就能够实现对数据库的操作。对于如何对数据库操作,将在后续的文章中介绍,本篇文章对于Mybatis
中的动静代理的剖析就到此为止。最初以一张图演绎一下Mybatis
中的动静代理执行流程,如下所示。
总结
本篇文章能够用如下内容进行总结。
- 映射文件中,每一个
<select>
,<insert>
,<update>
和<delete>
标签均会被创立一个MappedStatement
并存放在Configuration
的mappedStatements缓存中,MappedStatement
中次要蕴含着这个标签下的SQL
语句,这个标签的参数信息和出参信息等。每一个MappedStatement
的惟一标识为namespace + "." + 标签id,这样设置惟一标识的起因是为了调用映射接口的办法时可能依据映射接口的全限定名 + "." + "办法名"获取到和被调用办法关联的MappedStatement
,因而,映射文件的namespace须要和映射接口的全限定名统一,每个<select>
,<insert>
,<update>
和<delete>
标签均对应一个映射接口的办法,每个<select>
,<insert>
,<update>
和<delete>
标签的id须要和映射接口的办法名统一; - 调用
Mybatis
映射接口的办法时,调用申请的理论执行是由基于JDK
动静代理为映射接口生成的代理对象来实现,映射接口的代理对象由MapperProxyFactory
的newInstance()
办法生成,每个映射接口对应一个MapperProxyFactory
; - 在
Mybatis
的JDK
动静代理中,是由MapperProxy
实现了InvocationHandler
接口,因而MapperProxy
在Mybatis
的JDK
动静代理中表演调用处理器的角色,即调用映射接口的办法时,实际上是调用的MapperProxy
实现的invoke()
办法; - 在
Mybatis
的JDK
动静代理中,是不存在被代理对象的,能够了解为是对接口的代理,因而在MapperProxy
的invoke()
办法中,并没有去调用被代理对象的办法,而是会基于映射接口和被调用办法的办法对象生成MapperMethod
并执行MapperMethod
的execute()
办法,即调用映射接口的办法的申请会发送到MapperMethod
,能够了解为映射接口的办法由MapperMethod
代理。