欢迎访问 生活随笔!

生活随笔

当前位置: 首页 > 前端技术 > javascript >内容正文

javascript

Mybatis-Spring源码分析(五) MapperMethod和MappedStatement解析

发布时间:2023/12/14 javascript 40 豆豆
生活随笔 收集整理的这篇文章主要介绍了 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解析的全部内容,希望文章能够帮你解决所遇到的问题。

如果觉得生活随笔网站内容还不错,欢迎将生活随笔推荐给好友。