javascript
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解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 安全控件开发原理分析 支付宝安全控件开发
- 下一篇: 大图片加载到模糊