Mybatis-Spring源码分析(五) MapperMethod和MappedStatement解析
前言
基本上这就是Mybatis-Spring源码的最后一篇了,如果想起来什么再单开博客。比起来Spring源码,Mybatis的确实简单一些,本篇就说一下Mybatis中两个十分重要的类MapperMethod,MappedStatement以及其在Mybatis的流程中的主要作用。更多Spring内容进入【Spring解读系列目录】。
MapperMethod
首先什么是MapperMethod
?它就有点像Spring中的BeanDefinition
,用来描述一个Mapper
里面一个方法的内容的。比如UserMapper
接口里面有一个query()
方法,那么这个的MapperMethod
就是描述这个query()
方法,比如有没有注解,参数是什么之类,用于后续调用执行。既然说到要解析这个类,那就要找到它出现的位置, MapperProxy#cachedInvoker
方法,可以看到它的第一次使用是在PlainMethodInvoker
中new
出来了,传入的方法是mapperInterface
用来表示是哪个mapper
接口;method
方法用来表示是接口中的哪个方法;最后sqlSession这个其实是一个代理。关于这部分的详细解析参考 【Mybatis-Spring源码分析(二) Mapper接口代理的生成】。
return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
那就通过这里进入MapperMethod
类的构造方法:
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {this.command = new SqlCommand(config, mapperInterface, method);this.method = new MethodSignature(config, mapperInterface, method);
}
很明显new SqlCommand(config, mapperInterface, method);
应该就是存放的我们写的SQL
语句,那么就进入这个SqlCommand
的构造方法看看它是怎么拿到SQL
语句的。
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {//拿到方法名字final String methodName = method.getName();//拿到所在的类名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 {name = ms.getId();type = ms.getSqlCommandType();if (type == SqlCommandType.UNKNOWN) {throw new BindingException("Unknown execution method for: " + name);}}
}
进入这个构造方法以后,首先还是要初始化各种属性,拿到方法的名字,拿到所在的类名,后面就碰见了另一个非常核心的类MappedStatement
。我们看这里传入了Mapper
接口,传入了方法名字,传入了当前的类,传入了SqlSession
的Configuration
。那就说明MapperMethod.SqlCommand#resolveMappedStatement
这个方法可能是一个关键方法,因为我们所需要的执行SQL
的参数都在这里。是的MappedStatement
经过这个方法以后确实就保有了一系列的关键信息,例如下图。
MappedStatement
既然知道我们最终需要探究MappedStatement
的信息来源,就进入这个方法:
private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,Class<?> declaringClass, Configuration configuration) {//构建sql idString statementId = mapperInterface.getName() + "." + methodName;//判断是不是包含这个idif (configuration.hasStatement(statementId)) {//通过statementId拿到MappedStatement,这里一定会进入的,因为只要有一个mapper就会被初始化一个return configuration.getMappedStatement(statementId);} else if (mapperInterface.equals(declaringClass)) {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;}
}
进入后可以看到首先就是构建了statementId
,注意到这个id
是由接口名字加上方法名字来的。所以这句话其实也解决一个很经典的问题,就是Mybatis
中的SQL
的方法id
为什么和Mapper
接口内的方法名字相同。因为源码里SQL
的id
就是这样被构建:statementId = mapperInterface.getName() + "." + methodName;
,定义的就是类名+方法名字
。接着往下走,发现configuration.getMappedStatement(statementId);
这句话,也就是说要找的MappedStatement
并不是new
出来的,而是通过statementId
从Configuration
类对象中get
出来的。也就是说很早之前MappedStatement
在很早之前就已经被初始化,并且放到Configuration
对象里面。方法的类型,方法的查询类型,SQL
语句都可以通过MappedStatement
拿到。也就是说Mybatis
里面的所有信息,返回类型,SQL
语句等等都在MappedStatement
里面。那么就看怎么get
到的,进入Configuration#getMappedStatement(java.lang.String)
。
public MappedStatement getMappedStatement(String id) {return this.getMappedStatement(id, true);
}
继续进入this.getMappedStatement()
方法:
public MappedStatement getMappedStatement(String id, boolean validateIncompleteStatements) {if (validateIncompleteStatements) {buildAllStatements();}return mappedStatements.get(id);
}
到这里发现返回的是mappedStatements.get(id)
,找到定义:
Map<String, MappedStatement> mappedStatements
发现这是一个map
。那么到了这里我们就有了下面这样一个逻辑。
query()方法 --> mappedStatements.get(methodName) --> SQL --> execute
追踪到这里就必须知道什么时候mappedStatements
被初始化了,里面的内容是怎么被填充的。 既然知道是一个map
,那就只有去找mappedStatements.put()
方法了,那么直接搜索发现在Configuration#addMappedStatement()
方法里面:
public void addMappedStatement(MappedStatement ms) {mappedStatements.put(ms.getId(), ms);
}
但是此时我们调试看这句话是在什么时候执行的,断点运行:
上图红框里面的内容,有没有熟悉的名字,比如refresh
、createBean
、parse
等等,说明mappedStatements
这个map的初始化是在Spring运行伊始就开始被解析并加载了。我们写的Mapper
接口以及里面写的方法和SQL
语句的解析是在Spring容器的初始化Mapper
接口的时候就已经开始了。并不是调用的时候,也不是Mybatis做的。具体的初始化内容如果看过笔者之前的博客,看到afterPropertiesSet()
基本上应该明白是怎么做的,这里放上链接【Mybatis-Spring源码分析(四) Mybatis的初始化】。
总结
当执行一个SQL
语句的时候,Spring初始化Mapper
接口,然后Mybatis通过扩展点InitializingBean
把拿到包名,类名,方法名拼成一个字符串放到mappedStatements
中,然后从mappedStatements
中拿出一个MappedStatement
对象,然后拿到这个对象去执行SQL
语句。
执行流程
当我们到afterPropertiesSet()
里面以后就到了checkDaoConfig()
:
@Override
public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException {checkDaoConfig();//。。。。。略
}
转到MapperFactoryBean#checkDaoConfig
:
protected void checkDaoConfig() {super.checkDaoConfig();notNull(this.mapperInterface, "Property 'mapperInterface' is required");Configuration configuration = getSqlSession().getConfiguration();if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {try {configuration.addMapper(this.mapperInterface);} catch (Exception e) {logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e);throw new IllegalArgumentException(e);} finally {ErrorContext.instance().reset();}}
}
继续进入Configuration#addMapper
:
public <T> void addMapper(Class<T> type) {mapperRegistry.addMapper(type);
}
接着往下走MapperRegistry#addMapper
:
public <T> void addMapper(Class<T> type) {if (type.isInterface()) {if (hasMapper(type)) {throw new BindingException("Type " + type + " is already known to the MapperRegistry.");}boolean loadCompleted = false;try {knownMappers.put(type, new MapperProxyFactory<>(type));// It's important that the type is added before the parser is run// otherwise the binding may automatically be attempted by the// mapper parser. If the type is already known, it won't try.MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);parser.parse();loadCompleted = true;} finally {if (!loadCompleted) {knownMappers.remove(type);}}}
}
调用MapperAnnotationBuilder#parse
,这点和Spring很像:
public void parse() {String resource = type.toString();if (!configuration.isResourceLoaded(resource)) {loadXmlResource();configuration.addLoadedResource(resource);assistant.setCurrentNamespace(type.getName());parseCache();parseCacheRef();for (Method method : type.getMethods()) { //for循环解析每一个methodif (!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();
}
接着到MapperAnnotationBuilder#parseStatement
方法里面,看看是如何解析的:
void parseStatement(Method method) {final Class<?> parameterTypeClass = getParameterType(method);final LanguageDriver languageDriver = getLanguageDriver(method);//解析各种各样的内容getAnnotationWrapper(method, true, statementAnnotationTypes).ifPresent(statementAnnotation -> {final SqlSource sqlSource = buildSqlSource(statementAnnotation.getAnnotation(), parameterTypeClass, languageDriver, method);final SqlCommandType sqlCommandType = statementAnnotation.getSqlCommandType();final Options options = getAnnotationWrapper(method, false, Options.class).map(x -> (Options)x.getAnnotation()).orElse(null);final String mappedStatementId = type.getName() + "." + method.getName();//解析以后判断是哪种SQL语句final KeyGenerator keyGenerator;String keyProperty = null;String keyColumn = null;if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {// first check for SelectKey annotation - that overrides everything elseSelectKey selectKey = getAnnotationWrapper(method, false, SelectKey.class).map(x -> (SelectKey)x.getAnnotation()).orElse(null);if (selectKey != null) {keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver);keyProperty = selectKey.keyProperty();} else if (options == null) {keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;} else {keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;keyProperty = options.keyProperty();keyColumn = options.keyColumn();}} else {keyGenerator = NoKeyGenerator.INSTANCE;}Integer fetchSize = null;Integer timeout = null;StatementType statementType = StatementType.PREPARED;ResultSetType resultSetType = configuration.getDefaultResultSetType();boolean isSelect = sqlCommandType == SqlCommandType.SELECT;boolean flushCache = !isSelect;boolean useCache = isSelect;if (options != null) {if (FlushCachePolicy.TRUE.equals(options.flushCache())) {flushCache = true;} else if (FlushCachePolicy.FALSE.equals(options.flushCache())) {flushCache = false;}useCache = options.useCache();fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348timeout = options.timeout() > -1 ? options.timeout() : null;statementType = options.statementType();if (options.resultSetType() != ResultSetType.DEFAULT) {resultSetType = options.resultSetType();}}String resultMapId = null;if (isSelect) {ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);if (resultMapAnnotation != null) {resultMapId = String.join(",", resultMapAnnotation.value());} else {resultMapId = generateResultMapName(method);}}//把解析出来的内容传入addMappedStatement方法中。assistant.addMappedStatement(mappedStatementId,sqlSource,statementType,sqlCommandType,fetchSize,timeout,// ParameterMapIDnull,parameterTypeClass,resultMapId,getReturnType(method),resultSetType,flushCache,useCache,// TODO gcode issue #577false,keyGenerator,keyProperty,keyColumn,statementAnnotation.getDatabaseId(),languageDriver,// ResultSetsoptions != null ? nullOrEmpty(options.resultSets()) : null);});
}
上面把东西解析出来以后调MapperBuilderAssistant#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");}id = applyCurrentNamespace(id, false);boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
//构建MappedStatementMappedStatement.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 statement = statementBuilder.build();
//调用addMappedStatement添加到Map中configuration.addMappedStatement(statement);return statement;
}
最后调用Configuration#addMappedStatement
,就和我们上面的内容接上了:
public void addMappedStatement(MappedStatement ms) {mappedStatements.put(ms.getId(), ms);
}
Mybatis解析SqlProvider
还有一点要特别注意一下,Mybatis也是可以解析SqlProvider
的,就在MapperAnnotationBuilder#parseStatement
处理建立SqlSource
的地方:
SqlSource sqlSource = buildSqlSource(statementAnnotation.getAnnotation(), parameterTypeClass, languageDriver, method);
进入以后这里解析的是Method
上的注解,是不是@Select
,@Update
等等.如果都不是的话,会返回ProviderSqlSource
,Mybatis中的SqlProvider
就是在这里解析并放到map中的。
private SqlSource buildSqlSource(Annotation annotation, Class<?> parameterType, LanguageDriver languageDriver,Method method) {if (annotation instanceof Select) {return buildSqlSourceFromStrings(((Select) annotation).value(), parameterType, languageDriver);} else if (annotation instanceof Update) {return buildSqlSourceFromStrings(((Update) annotation).value(), parameterType, languageDriver);} else if (annotation instanceof Insert) {return buildSqlSourceFromStrings(((Insert) annotation).value(), parameterType, languageDriver);} else if (annotation instanceof Delete) {return buildSqlSourceFromStrings(((Delete) annotation).value(), parameterType, languageDriver);} else if (annotation instanceof SelectKey) {return buildSqlSourceFromStrings(((SelectKey) annotation).statement(), parameterType, languageDriver);}//解析@SqlProviderreturn new ProviderSqlSource(assistant.getConfiguration(), annotation, type, method);
}
Mybatis-Spring源码分析(五) MapperMethod和MappedStatement解析相关推荐
- Spring源码分析(三)
Spring源码分析 第三章 手写Ioc和Aop 文章目录 Spring源码分析 前言 一.模拟业务场景 (一) 功能介绍 (二) 关键功能代码 (三) 问题分析 二.使用ioc和aop重构 (一) ...
- Spring源码分析——汇总全集
文章目录 一.背景 二.源码分析目录 三.源码番外篇(补充) 更新时间 更新内容 备注 2022-04-01 Spring源码分析目录和计划 2022-04-10 Spring源码分析一:容器篇-re ...
- spring源码分析第五天------springAOP核心原理及源码分析
spring源码分析第五天------springAOP核心原理及源码分析 1. 面向切面编程.可以通过预 编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术 切面(A ...
- Spring源码分析八:Mybatis ORM映射框架原理
文章目录 (一)Mybatis单独操作数据库程序 1.1.数据库表 1.2.建立PO 1.3.建立mapper接口映射 1.4.建立Mybatis配置文件 1.5.建立mapper映射文件 1.6.测 ...
- Spring 源码分析(三) —— AOP(五)创建代理
2019独角兽企业重金招聘Python工程师标准>>> 创建代理 代理的定义其实非常简单,就是改变原来目标对象方法调用的运行轨迹.这种改变,首先会对这些方法进行拦截,从而为这些方法提 ...
- 【mybatis源码】 mybatis底层源码分析
[mybatis源码] mybatis底层源码分析 1.测试用例 2.开撸源码 2.1 SqlSessionFactory对象的创建与获取 2.2 获取SqlSession对象 2.3 获取接口的代理 ...
- Spring源码分析之Bean的创建过程详解
前文传送门: Spring源码分析之预启动流程 Spring源码分析之BeanFactory体系结构 Spring源码分析之BeanFactoryPostProcessor调用过程详解 本文内容: 在 ...
- Spring源码分析【1】-Tomcat的初始化
org.apache.catalina.startup.ContextConfig.configureStart() org.apache.catalina.startup.ContextConfig ...
- spring源码分析之spring-core总结篇
1.spring-core概览 spring-core是spring框架的基石,它为spring框架提供了基础的支持. spring-core从源码上看,分为6个package,分别是asm,cgli ...
最新文章
- Android开发中使用七牛云存储进行图片上传下载
- DF学数据结构系列——B树(B-树和B+树)介绍
- spring框架(三)mvc
- 不同维度的矩阵相乘的时间复杂度
- python 数学公式显示_ipython jupyter notebook中显示图像和数学公式实例
- 【学步者日记】UnityEditor扩展菜单以及ScriptableObject
- pycharm2020版本界面中英文注释
- 初探内核之《Linux内核设计与实现》笔记上
- [转载] [转载] python set集合如何有序输出_python set集合的用法
- 使用Java 自身Timer API实现定时器的方法
- Git小乌龟汉化步骤
- java动漫项目_狂拽酷炫diao炸天的开源动画项目:lottie-android,拿来就用!
- 生产环境部署springcloud微服务启动慢的问题排查
- C++函数的定义与使用
- vulcan 编程_我如何在四天内使用Vulcan.js构建应用程序
- C# DataGridView控件动态添加行与列
- 移动端框架 - Bootstrap
- python模块安装(大合集)
- Linux shell的简单学习
- python最好用的助手_python 好用
热门文章
- 昼短苦夜长,何不秉烛游
- note8 android p,值得买的手机 篇一:2020年,红米note8pro使用评测
- word(1):word插入参考文献/引文并更新参考文献/引文编号
- javaweb JAVA JSP 流浪狗管理系统(宠物狗管理系统)jsp小宠物在线管理网站源码
- HTML5文件夹隐藏了怎么打开,怎样显示隐藏文件夹|win7系统如何显示隐藏文件夹...
- 面试题61. 扑克牌中的顺子
- 巨量jsvmp,扣逻辑分析
- Metal(二) Metal语法规范
- python释放变量内存_Python变量内存管理
- 【研究生本科】如何与导师有效沟通你的论文选题?