MyBatis3源码深度解析(十五)SqlSession的创建与执行(二)Mapper接口和XML配置文件的注册与获取

本文主要是介绍MyBatis3源码深度解析(十五)SqlSession的创建与执行(二)Mapper接口和XML配置文件的注册与获取,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

    • 前言
    • 5.5 Mapper接口与XML配置文件的注册过程
      • 5.5.1 Mapper接口的注册过程
      • 5.5.2 XML配置文件的注册过程
    • 5.6 MappedStatement对象的注册过程
    • 5.7 Mapper接口的动态代理对象的获取

前言

SqlSession对象创建后,接下来是执行Mapper。执行Mapper的过程可以拆解为三步:注册Mapper接口与XML配置文件;注册MappedStatement对象;调用Mapper方法。

5.5 Mapper接口与XML配置文件的注册过程

Mapper接口用于定义执行SQL语句相关的方法,方法名一般和Mapper XML配置文件中的<select|update|insert|delete>标签的id属性相同,Mapper接口的完全限定名一般和Mapper XML配置文件的命名空间相同。例如:

public interface UserMapper {List<User> selectAll();@Select("select * from user where id = #{id, jdbcType=INTEGER}")User selectById(@Param("id") Integer id);}
<!--UserMapper.xml-->
<mapper namespace="com.star.mybatis.mapper.UserMapper"><select id="selectAll" resultType="User">select * from user</select>
</mapper>

如上面的代码所示,UserMapper接口的selectAll()方法名与UserMapper.xml中的<select>标签的id属性相同;UserMapper接口的完全限定名与<mapper>标签的namespace属性相同。

如果要将Mapper接口或Mapper XML配置文件注册到Configuration组件中,需要在主配置文件mybatis-config.xml中配置<mappers>标签。

<!--mybatis-config.xml-->
<mappers><!--方式一:通过指定XML文件的类路径来注册--><mapper resource="mapper/UserMapper.xml"/><!--方式二:通过指定XML文件的完全限定资源定位符来注册--><mapper url="file:///C:\workspace\mybatis_demo2\src\main\resources\mapper\UserMapper.xml"/><!--方式三:通过Mapper接口的类路径来注册--><mapper class="com.star.mybatis.mapper.UserMapper"/><!--方式四:通过Mapper接口所在包路径类注册--><package name="com.star.mybatis.mapper"/>
</mappers>

如上面的代码所示,<mappers>标签支持四种配置方式,其中方式一和方式二指定XML配置文件的相对和绝对路径,方式三和方式四指定Mapper接口的类路径和所在包路径。实际开发中,选择其一即可。

在【MyBatis3源码深度解析(十四)Configuration与SqlSession的创建过程 5.2 Configuration实例创建过程】中已经研究过,在mappersElement()方法中,会对<mappers>标签的四种配置方式分别进行解析。

源码1org.apache.ibatis.builder.xml.XMLConfigBuilderprivate void mappersElement(XNode context) throws Exception {if (context == null) {return;}for (XNode child : context.getChildren()) {if ("package".equals(child.getName())) {// 方式四的处理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) {// 方式一的处理 ...ErrorContext.instance().resource(resource);try (InputStream inputStream = Resources.getResourceAsStream(resource)) {XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource,configuration.getSqlFragments());mapperParser.parse();}} else if (resource == null && url != null && mapperClass == null) {// 方式二的处理 ...ErrorContext.instance().resource(url);try (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<?> mapperInterface = Resources.classForName(mapperClass);configuration.addMapper(mapperInterface);} else {// throw ...}}
}

5.5.1 Mapper接口的注册过程

由 源码1 可知,如果是方式三或方式四这两种配置Mapper接口的方式,则会调用Configuration对象的addMappers()方法。

源码2org.apache.ibatis.session.Configurationprotected final MapperRegistry mapperRegistry = new MapperRegistry(this);
// 用于处理指定包路径下的所有Mapper接口的注册
public void addMappers(String packageName) {mapperRegistry.addMappers(packageName);
}
// 用于处理指定的Mapper接口的注册
public <T> void addMapper(Class<T> type) {mapperRegistry.addMapper(type);
}

由 源码2 可知,Configuration组件中组合了一个MapperRegistry对象,其addMappers(String)方法用于处理指定包路径下的所有Mapper接口的注册,其addMapper(Class<T>)方法用于处理指定的Mapper接口的注册。

先来看addMapper(Class<T>)方法:

源码3org.apache.ibatis.binding.MapperRegistryprivate final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new ConcurrentHashMap<>();
public <T> void addMapper(Class<T> type) {// 判断是否是Mapper接口if (type.isInterface()) {// 判断是否已经注册过if (hasMapper(type)) {throw new BindingException("Type " + type + " is already known to the MapperRegistry.");}boolean loadCompleted = false;try {// 注册Mapper接口到knownMappers属性中knownMappers.put(type, new MapperProxyFactory<>(type));MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);parser.parse();loadCompleted = true;} finally {if (!loadCompleted) {knownMappers.remove(type);}}}
}

由 源码3 可知,MapperRegistry类中有一个knownMappers属性,用于注册Mapper接口对应的Class对象和MapperProxyFactory对象之间的关系。addMapper(Class<T>)方法经过type.isInterface()hasMapper(type)两个前置检查之后,然后为Mapper接口对应的Class对象创建一个MapperProxyFactory对象,并添加到knownMappers属性中。

再来看addMappers(String)方法:

源码4org.apache.ibatis.binding.MapperRegistrypublic void addMappers(String packageName, Class<?> superType) {// 借助ResolverUtil工具类获取指定包路径下的全部Mapper接口对应的Class对象ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();resolverUtil.find(new ResolverUtil.IsA(superType), packageName);Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();// 遍历Class对象集合,分别调用addMapper方法注册到MapperRegistry类的knownMappers属性中for (Class<?> mapperClass : mapperSet) {addMapper(mapperClass);}
}

由 源码4 可知,addMappers(String)方法借助ResolverUtil工具类获取指定包路径下的全部Mapper接口对应的Class对象,然后遍历Class对象集合,分别调用addMapper(Class<T>)方法将Class对象注册到MapperRegistry类的knownMappers属性中。

再深入resolverUtil.find()方法的源码可以发现,ResolverUtil是使用VFS工具扫描指定包路径下所有Class文件,然后再交给addMapper(Class<T>)方法进行筛选。

至此,Mapper接口的注册完毕,MapperRegistry类的knownMappers属性中保存了Mapper接口对应的Class对象与MapperProxyFactory对象之间的映射关系。

5.5.2 XML配置文件的注册过程

回到 源码1 可知,如果是方式一或方式二这两种配置Mapper XML配置文件的方式,则会先获取Mapper XML配置文件的输入流,再调用XMLMapperBuilder类的parse()方法来完成注册。

源码5org.apache.ibatis.builder.xml.XMLMapperBuilderprivate final XPathParser parser;
public void parse() {if (!configuration.isResourceLoaded(resource)) {// 调用XPathParser的evalNode方法获取mapper根节点对应的XNode对象// 调用configurationElement方法进一步解析configurationElement(parser.evalNode("/mapper"));// 将资源路径添加到Configuration对象中configuration.addLoadedResource(resource);bindMapperForNamespace();}// ......
}

由 源码5 可知,parse()方法首先调用XPathParser的evalNode()方法获取<mapper>根节点对应的XNode对象,然后调用configurationElement()方法进一步解析XNode对象。

源码6org.apache.ibatis.builder.xml.XMLMapperBuilderprivate 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);// 解析<cache-ref>标签cacheRefElement(context.evalNode("cache-ref"));// 解析<cache>标签cacheElement(context.evalNode("cache"));// 解析<parameterMap>标签parameterMapElement(context.evalNodes("/mapper/parameterMap"));// 解析<resultMap>标签resultMapElements(context.evalNodes("/mapper/resultMap"));// 解析<sql>标签sqlElement(context.evalNodes("/mapper/sql"));// 解析<select|insert|update|delete>标签buildStatementFromContext(context.evalNodes("select|insert|update|delete"));} // catch ......
}

由 源码6 可知,configurationElement()方法会对Mapper XML配置文件的所有标签进行解析,并封装成一个MappedStatement对象。

5.6 MappedStatement对象的注册过程

重点关注<select|insert|update|delete>标签的解析。由 源码6 可知,获取<select|insert|update|delete>标签节点对应的XNode对象后,调用buildStatementFromContext()方法做进一步的解析。

源码7org.apache.ibatis.builder.xml.XMLMapperBuilderprivate void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {for (XNode context : list) {// 创建XMLStatementBuilder对象final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context,requiredDatabaseId);try {// 真正解析标签statementParser.parseStatementNode();} catch (IncompleteElementException e) {configuration.addIncompleteStatement(statementParser);}}
}

由 源码7 可知,buildStatementFromContext()方法会对所有的XNode对象进行遍历,为每一个XNode对象创建一个XMLStatementBuilder对象。然后调用该对象的parseStatementNode()方法真正进行解析标签。

源码8org.apache.ibatis.builder.xml.XMLStatementBuilderpublic void parseStatementNode() {// 获取id、databaseId属性并进行前置判断String id = context.getStringAttribute("id");String databaseId = context.getStringAttribute("databaseId");if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {return;}// 根据标签名字得到SQL语句的类型String nodeName = context.getNode().getNodeName();SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));// 获取flushCache属性,如果是SELECT语句,则默认为falseboolean isSelect = sqlCommandType == SqlCommandType.SELECT;boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);// 获取useCache属性,如果是SELECT语句,则默认为trueboolean useCache = context.getBooleanAttribute("useCache", isSelect);// 获取resultOrdered属性,默认为falseboolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);// 将<include>标签内容替换为<sql>标签定义的SQL片段XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);includeParser.applyIncludes(context.getNode());// 获取parameterType属性,并转换成对应的Class对象String parameterType = context.getStringAttribute("parameterType");Class<?> parameterTypeClass = resolveClass(parameterType);// 获取lang属性,并转换成对应的LanguageDriver对象String lang = context.getStringAttribute("lang");LanguageDriver langDriver = getLanguageDriver(lang);// 解析<selectKey>标签processSelectKeyNodes(id, parameterTypeClass, langDriver);// 获取主键生成策略KeyGenerator keyGenerator;String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);if (configuration.hasKeyGenerator(keyStatementId)) {keyGenerator = configuration.getKeyGenerator(keyStatementId);} else {keyGenerator = context.getBooleanAttribute("useGeneratedKeys",configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;}// 通过LanguageDriver解析SQL内容,生成SqlSource对象SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);// 获取statementType属性,默认为PREPARED类型StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));// 获取fetchSize、timeout、parameterMap属性Integer fetchSize = context.getIntAttribute("fetchSize");Integer timeout = context.getIntAttribute("timeout");String parameterMap = context.getStringAttribute("parameterMap");// 获取resultType属性,并转换为对应的Class对象String resultType = context.getStringAttribute("resultType");Class<?> resultTypeClass = resolveClass(resultType);String resultMap = context.getStringAttribute("resultMap");if (resultTypeClass == null && resultMap == null) {resultTypeClass = MapperAnnotationBuilder.getMethodReturnType(builderAssistant.getCurrentNamespace(), id);}// 获取resultSetType属性,并转换为对应的ResultSetType对象String resultSetType = context.getStringAttribute("resultSetType");ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);if (resultSetTypeEnum == null) {resultSetTypeEnum = configuration.getDefaultResultSetType();}// 获取keyProperty、keyColumn、resultSets、affectData属性String keyProperty = context.getStringAttribute("keyProperty");String keyColumn = context.getStringAttribute("keyColumn");String resultSets = context.getStringAttribute("resultSets");boolean dirtySelect = context.getBooleanAttribute("affectData", Boolean.FALSE);// 使用上面获取的所有属性构造一个MappedStatement对象builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap,parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered,keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets, dirtySelect);
}

如 源码8 所示,parseStatementNode()方法篇幅很长,但逻辑很清晰,主要做了以下几件事情:

(1)获取<select|insert|update|delete>标签的所有属性信息。
(2)将<include>标签引用的SQL片段替换为对应的<sql>标签中定义的内容。
(3)获取lang属性指定的LanguageDriver对象,通过该对象创建代表SQL资源的SqlSource对象。
(4)获取KeyGenerator对象。KeyGenerator的不同实例代表不同的主键生成策略。
(5)所有解析工作完成后,使用MapperBuilderAssistant对象的addMappedStatement()方法创建一个MappedStatement对象。

源码9org.apache.ibatis.builder.MapperBuilderAssistantpublic 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, boolean dirtySelect) {if (unresolvedCacheRef) {throw new IncompleteElementException("Cache-ref not yet resolved");}id = applyCurrentNamespace(id, false);// 创建MappedStatement.Builder对象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(flushCache).useCache(useCache).cache(currentCache).dirtySelect(dirtySelect);ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);if (statementParameterMap != null) {statementBuilder.parameterMap(statementParameterMap);}// 创建MappedStatement对象MappedStatement statement = statementBuilder.build();// 注册到Configuration组件中configuration.addMappedStatement(statement);return statement;
}

由 源码9 可知,addMappedStatement()方法会通过MappedStatement.Builder对象的build()方法创建一个MappedStatement对象,最后注册到Configuration组件中。

至此,Mapper接口和Mapper XML配置文件的解析全部完成,保存这些信息的位置是Configuration组件中的mappedStatements属性和mapperRegistry属性。

借助Debug,可以查看到这两个属性保存的信息:

5.7 Mapper接口的动态代理对象的获取

Mapper接口和Mapper XML配置文件解析注册完成后,接下来是执行Mapper接口中定义的方法。

为了执行Mapper接口中定义的方法,首先需要调用SqlSession对象的getMapper()方法获取一个Mapper接口的动态代理对象,然后通过代理对象调用Mapper方法。

代码如下:

SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> userList = userMapper.selectAll();

SqlSession对象的getMapper()方法的源码如下:

源码10org.apache.ibatis.session.defaults.DefaultSqlSession@Override
public <T> T getMapper(Class<T> type) {return configuration.getMapper(type, this);
}
源码11org.apache.ibatis.session.Configurationpublic <T> T getMapper(Class<T> type, SqlSession sqlSession) {return mapperRegistry.getMapper(type, sqlSession);
}
源码12org.apache.ibatis.binding.MapperRegistryprivate final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new ConcurrentHashMap<>();
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);if (mapperProxyFactory == null) {throw new BindingException("Type " + type + " is not known to the MapperRegistry.");}try {return mapperProxyFactory.newInstance(sqlSession);} catch (Exception e) {throw new BindingException("Error getting mapper instance. Cause: " + e, e);}
}

由 源码10-12 可知,SqlSession对象的getMapper()方法最终会从MapperRegistry对象的knownMappers属性中,根据传入的Class对象取出一个MapperProxyFactory对象,并调用其newInstance()方法创建一个实例并返回。

由类名可知,MapperProxyFactory是一个代理工厂类,其newInstance()方法会创建一个代理对象实例。

源码13org.apache.ibatis.binding.MapperProxyFactorypublic T newInstance(SqlSession sqlSession) {final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);return newInstance(mapperProxy);
}protected T newInstance(MapperProxy<T> mapperProxy) {return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

由 源码13 可知,MapperProxyFactory对象的newInstance()方法会创建一个MapperProxy对象,并调用Proxy类的newProxyInstance()方法创建一个代理对象实例。

源码14org.apache.ibatis.binding.MapperProxypublic class MapperProxy<T> implements InvocationHandler, Serializable {public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethodInvoker> methodCache) {this.sqlSession = sqlSession;this.mapperInterface = mapperInterface;this.methodCache = methodCache;}
}@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {try {if (Object.class.equals(method.getDeclaringClass())) {return method.invoke(this, args);}return cachedInvoker(method).invoke(proxy, method, args, sqlSession);} catch (Throwable t) {throw ExceptionUtil.unwrapThrowable(t);}
}

由 源码14 可知,MapperProxy类实现了InvocationHandler接口,使用的是JDK内置的动态代理

通过以上分析可以知道,SqlSession对象的getMapper()方法返回的是一个MapperProxy动态代理对象。


本节完,更多内容请查阅分类专栏:MyBatis3源码深度解析

这篇关于MyBatis3源码深度解析(十五)SqlSession的创建与执行(二)Mapper接口和XML配置文件的注册与获取的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/825319

相关文章

JavaScript中的reduce方法执行过程、使用场景及进阶用法

《JavaScript中的reduce方法执行过程、使用场景及进阶用法》:本文主要介绍JavaScript中的reduce方法执行过程、使用场景及进阶用法的相关资料,reduce是JavaScri... 目录1. 什么是reduce2. reduce语法2.1 语法2.2 参数说明3. reduce执行过程

C#中读取XML文件的四种常用方法

《C#中读取XML文件的四种常用方法》Xml是Internet环境中跨平台的,依赖于内容的技术,是当前处理结构化文档信息的有力工具,下面我们就来看看C#中读取XML文件的方法都有哪些吧... 目录XML简介格式C#读取XML文件方法使用XmlDocument使用XmlTextReader/XmlTextWr

Android 悬浮窗开发示例((动态权限请求 | 前台服务和通知 | 悬浮窗创建 )

《Android悬浮窗开发示例((动态权限请求|前台服务和通知|悬浮窗创建)》本文介绍了Android悬浮窗的实现效果,包括动态权限请求、前台服务和通知的使用,悬浮窗权限需要动态申请并引导... 目录一、悬浮窗 动态权限请求1、动态请求权限2、悬浮窗权限说明3、检查动态权限4、申请动态权限5、权限设置完毕后

在MySQL执行UPDATE语句时遇到的错误1175的解决方案

《在MySQL执行UPDATE语句时遇到的错误1175的解决方案》MySQL安全更新模式(SafeUpdateMode)限制了UPDATE和DELETE操作,要求使用WHERE子句时必须基于主键或索引... mysql 中遇到的 Error Code: 1175 是由于启用了 安全更新模式(Safe Upd

Go路由注册方法详解

《Go路由注册方法详解》Go语言中,http.NewServeMux()和http.HandleFunc()是两种不同的路由注册方式,前者创建独立的ServeMux实例,适合模块化和分层路由,灵活性高... 目录Go路由注册方法1. 路由注册的方式2. 路由器的独立性3. 灵活性4. 启动服务器的方式5.

如何利用Java获取当天的开始和结束时间

《如何利用Java获取当天的开始和结束时间》:本文主要介绍如何使用Java8的LocalDate和LocalDateTime类获取指定日期的开始和结束时间,展示了如何通过这些类进行日期和时间的处... 目录前言1. Java日期时间API概述2. 获取当天的开始和结束时间代码解析运行结果3. 总结前言在J

Deepseek R1模型本地化部署+API接口调用详细教程(释放AI生产力)

《DeepseekR1模型本地化部署+API接口调用详细教程(释放AI生产力)》本文介绍了本地部署DeepSeekR1模型和通过API调用将其集成到VSCode中的过程,作者详细步骤展示了如何下载和... 目录前言一、deepseek R1模型与chatGPT o1系列模型对比二、本地部署步骤1.安装oll

Java深度学习库DJL实现Python的NumPy方式

《Java深度学习库DJL实现Python的NumPy方式》本文介绍了DJL库的背景和基本功能,包括NDArray的创建、数学运算、数据获取和设置等,同时,还展示了如何使用NDArray进行数据预处理... 目录1 NDArray 的背景介绍1.1 架构2 JavaDJL使用2.1 安装DJL2.2 基本操

最长公共子序列问题的深度分析与Java实现方式

《最长公共子序列问题的深度分析与Java实现方式》本文详细介绍了最长公共子序列(LCS)问题,包括其概念、暴力解法、动态规划解法,并提供了Java代码实现,暴力解法虽然简单,但在大数据处理中效率较低,... 目录最长公共子序列问题概述问题理解与示例分析暴力解法思路与示例代码动态规划解法DP 表的构建与意义动

Python创建Excel的4种方式小结

《Python创建Excel的4种方式小结》这篇文章主要为大家详细介绍了Python中创建Excel的4种常见方式,文中的示例代码简洁易懂,具有一定的参考价值,感兴趣的小伙伴可以学习一下... 目录库的安装代码1——pandas代码2——openpyxl代码3——xlsxwriterwww.cppcns.c