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

56 篇文章 3 订阅
7 篇文章 1 订阅

前言

基本上这就是Mybatis-Spring源码的最后一篇了,如果想起来什么再单开博客。比起来Spring源码,Mybatis的确实简单一些,本篇就说一下Mybatis中两个十分重要的类MapperMethod,MappedStatement以及其在Mybatis的流程中的主要作用。更多Spring内容进入【Spring解读系列目录】

MapperMethod

首先什么是MapperMethod?它就有点像Spring中的BeanDefinition,用来描述一个Mapper里面一个方法的内容的。比如UserMapper接口里面有一个query()方法,那么这个的MapperMethod就是描述这个query()方法,比如有没有注解,参数是什么之类,用于后续调用执行。既然说到要解析这个类,那就要找到它出现的位置, MapperProxy#cachedInvoker方法,可以看到它的第一次使用是在PlainMethodInvokernew出来了,传入的方法是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接口,传入了方法名字,传入了当前的类,传入了SqlSessionConfiguration。那就说明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接口内的方法名字相同。因为源码里SQLid就是这样被构建:statementId = mapperInterface.getName() + "." + methodName;,定义的就是类名+方法名字。接着往下走,发现configuration.getMappedStatement(statementId);这句话,也就是说要找的MappedStatement并不是new出来的,而是通过statementIdConfiguration类对象中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);
}

但是此时我们调试看这句话是在什么时候执行的,断点运行:
在这里插入图片描述

上图红框里面的内容,有没有熟悉的名字,比如refreshcreateBeanparse等等,说明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);
}
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值