前言
基本上这就是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 id
String statementId = mapperInterface.getName() + "." + methodName;
//判断是不是包含这个id
if (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循环解析每一个method
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();
}
接着到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 else
SelectKey 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 #348
timeout = 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,
// ParameterMapID
null,
parameterTypeClass,
resultMapId,
getReturnType(method),
resultSetType,
flushCache,
useCache,
// TODO gcode issue #577
false,
keyGenerator,
keyProperty,
keyColumn,
statementAnnotation.getDatabaseId(),
languageDriver,
// ResultSets
options != 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;
//构建MappedStatement
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 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);
}
//解析@SqlProvider
return new ProviderSqlSource(assistant.getConfiguration(), annotation, type, method);
}