【MyBatis源码学习】参数解析


一、几种入参形式

这里只分析带有入参的方法。

1.单个入参

UserInfo selectByPrimaryKey(String id);

2.多个入参

List<UserInfo> getByOpenIdAndUsername2(@Param("openid") String openId, @Param("username") String username);

3.入参为实体对象

List<UserInfo> getByOpenIdAndUsername3(UserInfo userInfo);

4.入参为Map

List<UserInfo> getByOpenIdAndUsername(Map<String, Object> params);

二、mybatis执行入口

还是以之前的一个例子来进入我们今天的正题。

@Test
// 快速入门
public void quickStart() throws IOException {
    //--------------------第二阶段---------------------------
    // 2.获取sqlSession
    SqlSession sqlSession = sqlSessionFactory.openSession();
    // 3.获取对应mapper
    UserInfoMapper mapper = sqlSession.getMapper(UserInfoMapper.class);
    //--------------------第三阶段---------------------------
    // 4.执行查询语句并返回单条数据
    UserInfo user = mapper.selectByPrimaryKey("1");
    System.out.println(user);
}

当我们执行到这一行时,

UserInfoMapper mapper = sqlSession.getMapper(UserInfoMapper.class);

通过调试我们可以看到,这个mapper其实是通过MapperProxy代理执行的。我们拿到的其实就是个动态代理对象。如下图:

""

当我们执行查询时,进入MapperProxy动态代理过程。

""

最终交由MapperMethod类的execute()方法执行,源代码如下:

//三步翻译在此完成
public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  //第一步 根据sql语句类型以及接口返回的参数选择调用不同的方法
  switch (command.getType()) {
    case INSERT: {
  	Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.insert(command.getName(), param));
      break;
    }
    case UPDATE: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.update(command.getName(), param));
      break;
    }
    case DELETE: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
      break;
    }
    case SELECT:
      if (method.returnsVoid() && method.hasResultHandler()) {//返回值为void
        executeWithResultHandler(sqlSession, args);
        result = null;
      } else if (method.returnsMany()) {//返回值为集合或者数组
        result = executeForMany(sqlSession, args);
      } else if (method.returnsMap()) {//返回值为map
        result = executeForMap(sqlSession, args);
      } else if (method.returnsCursor()) {//返回值为游标
        result = executeForCursor(sqlSession, args);
      } else {//处理返回为单一对象的情况
        //通过参数解析器解析解析参数
        Object param = method.convertArgsToSqlCommandParam(args);//第三步翻译,将入参转化成Map
        result = sqlSession.selectOne(command.getName(), param);
        if (method.returnsOptional() &&
            (result == null || !method.getReturnType().equals(result.getClass()))) {
          result = OptionalUtil.ofNullable(result);
        }
      }
      break;
    case FLUSH:
      result = sqlSession.flushStatements();
      break;
    default:
      throw new BindingException("Unknown execution method for: " + command.getName());
  }
  if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
    throw new BindingException("Mapper method '" + command.getName()
        + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
  }
  return result;
}

所以,本章参数解析的过程,我们重点关注这个方法即可,这个方法就是convertArgsToSqlCommandParam。它其实是方法的参数解析器ParamNameResolver的getNamedParams()方法完成的。

""

而这个ParamNameResolver则是在MapperProxy获取mapperMethod时(先不说从cache中取)进行初始化的,

""
""
""

而ParamNameResolver实例化时,主要工作就是进行初步的映射关系存储,其字段names是一个SortedMap,存储了参数名的顺序映射。

/**
   * <p>
   * The key is the index and the value is the name of the parameter.<br />
   * The name is obtained from {@link Param} if specified. When {@link Param} is not specified,
   * the parameter index is used. Note that this index could be different from the actual index
   * when the method has special parameters (i.e. {@link RowBounds} or {@link ResultHandler}).
   * </p>
   * <ul>
   * <li>aMethod(@Param("M") int a, @Param("N") int b) -&gt; {{0, "M"}, {1, "N"}}</li>
   * <li>aMethod(int a, int b) -&gt; {{0, "0"}, {1, "1"}}</li>
   * <li>aMethod(int a, RowBounds rb, int b) -&gt; {{0, "0"}, {2, "1"}}</li>
   * </ul>
   */
  private final SortedMap<Integer, String> names;

继续看getNamedParams()方法。

//将多个参数封装成MAP
public Object getNamedParams(Object[] args) {
  final int paramCount = names.size();
  if (args == null || paramCount == 0) {
    return null;
  } else if (!hasParamAnnotation && paramCount == 1) {
    return args[names.firstKey()];
  } else {
    final Map<String, Object> param = new ParamMap<>();
    int i = 0;
    for (Map.Entry<Integer, String> entry : names.entrySet()) {
      param.put(entry.getValue(), args[entry.getKey()]);
      // add generic param names (param1, param2, ...)
      final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
      // ensure not to overwrite parameter named with @Param
      if (!names.containsValue(genericParamName)) {
        param.put(genericParamName, args[entry.getKey()]);
      }
      i++;
    }
    return param;
  }
}

本例子中,由于无@Param注解,所以在第二个else if那里就返回了。

三、参数解析流程

以下面的代码为例,其他形式的入参大同小异。

@Test
public void testManyParamQuery() {
    // 2.获取sqlSession
    SqlSession sqlSession = sqlSessionFactory.openSession();
    // 3.获取对应mapper
    UserInfoMapper mapper = sqlSession.getMapper(UserInfoMapper.class);
    String username = "zyx";
    String openId = "zyxelva";

    // 第一种方式使用map
    Map<String, Object> params = new HashMap<>();
    params.put("username", username);
    params.put("openid", openId);
    List<UserInfo> list1 = mapper.getByUsernameAndOpenId(params);
    System.out.println(list1);
}

对应的mapper.xml方法

<select id="getByUsernameAndOpenId" resultMap="BaseResultMap">
   select
    <include refid="Base_Column_List"/>
    from user_info 
    where username=#{username} and openid=#{openid}
</select>

我们从方法

List<UserInfo> list1 = mapper.getByUsernameAndOpenId(params);

开始断点调试。我们看到,进入到了MapperProxy的动态代理过程。直接进入mapperMethod.execute(sqlSession, args);

""

来到MapperMethod中的execute方法中。由于我们的例子是查询操作,故进入Select。又例子的返回类型是List,故进入第二个if语句中。

""
""

进入方法executeForMany(). 没有分页,所以进入else语句中。而convertArgsToSqlCommandParam方法我们在二中已经分析了,这里不再具体梳理。

""

进入DefaultSQLSession的selectList方法中。可以看下statement实际形式是namespace+id,就可以从MappedStatement中获取。

""

而MappedStatement也有很多信息,主要是

""

而sql的实际执行者,则为Executor. 代理对象执行方法被拦截器拦截,执行以下方法。

""

先看看getBoundSql干了啥:

""
""

进入RawSqlSource,因例子的sql无${},无动态SQL节点。

""

先看看RawSqlSource的构造方法,看看#{}是怎么替换成?的,发现主要是SqlSourceBuilder在干活儿。

""
""

主要看GenericTokenParser.parse().该方法主要完成占位符的定位工作,然后把占位符的替换工作交给与其关联的 TokenHandler 处理.

""

TokenHandler有四个小弟,

""

BindingTokenParser:该对象的handleToken方法会取出占位符中的变量,然后使用该变量作为键去上下文环境中寻找对应的值。之后,会用找到的值替换占位符。因此,该对象可以完成占位符的替换工作;
DynamicCheckerTokenParser:该对象的 handleToken 方法会置位成员属性isDynamic。因此该对象可以记录自身是否遇到过占位符。
ParameterMappingTokenHandler:将DynamicSqlSource和RawSqlSource中的“#{}”符号替换掉,从而将他们转化为StaticSqlSource;
VariableTokenHandler:handleToken方法中传入输入参数后,该方法会以输入参数为键尝试从variables属性中寻找对应的值返回。

""

到此BoundSql的解析过程基本结束。getBoundSql方法执行完后,我们再看看一级缓存的key生成策略。

""
""

可以看出,cacheKey由namespace的id,分页参数,sql语句,入参以及节点的信息组成。
回到查询过程,实际执行的方法为

""

而首次查询不会进入if语句,调用BaseExecutor的方法:

""

本地缓存没有结果,故需要查询数据库,进入queryFromDatabase().

""

doQuery()则调用的为SimpleExecutor的方法。

""

看看newStatementHandler()。

""
""

由于我们的例子当中sql带有#{},故进入PREPARED,生成PreparedStatementHandler。
执行完语句后,放入一级缓存。

""

后续就是结果映射执行过程,这里不再赘述,后续跟上。

四、总结

本章主要讲述了mybatis参数解析的过程,重点跟踪了执行sql时,变量占位符${}以及参数占位符#{}的替换和解析过程。实际开发过程中,要注意这两者的区别,能用#{}的地方尽量用#。而${}主要用于order by语句、原生jdbc、表名作参数。


文章作者: Kezade
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Kezade !
评论
  目录