mybatis源码(三)接口方法与xml中sql绑定

上篇文章mybatis源码中如何找到xml文件并提取xml中的sql,解决了前两个问题,本篇文章继续追踪源码,尝试答疑后面的两个问题,在开始正式追踪源码,再次确认下我们要研究的问题:
        1、mybatis如何找到存储sql的xml文件
        2、mybatis如何解析xml中sql相关标签并拼装成完整的sql语句
        3、mybatis如何将xml中sql与接口中方法绑定
        4、mybatis如何将sql发送出去并获得结果

这里我们还是先从demo样例中看:

 可以看到xml解析器解析xml文件后,最后会生成一个默认的sqlsession工厂。因为要根据工厂对象创建sqlsession对象,这里我们再看下opensession方法的实现:

 创建sqlsessionfactory的时候返回的是DefaultSqlSessionFactory,所以接下来我们跳入该类中查看:

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      //从配置类获取当前环境对象,并以此生成事务工厂和事务对象(因为不是此次源码探究的目的,所以事务这里不深入研究)
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      //创建执行器
      final Executor executor = configuration.newExecutor(tx, execType);
      //创建sqlsession会话并返回
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

 (看到这里我有个小建议或者个人理解吧,就是纯粹探查mybatis源码流程,可以根据代码流来一行行查看,但是如果想完全理解mybatis,而不是囫囵吞枣,对于configuration、enviroment、mapperRegistry等等类或者属性的作用以及包含嵌套关系也要关注了解下)

至此我们已经拿到了SqlSession,但是后两问的源码答案还没出现,不要急,我们接着向下看:

 这个方法比较重要,下面详细介绍下:

 首先可以看到该方法是一个泛型方法,入参类型是一个泛型类,返回的是一个泛型对象,实际入参则是我们即将使用的接口类。下面的configuration对象则是解析xml配置文件生成的,包含了配置文件中的信息,并在生成当前sqlsession会话时被传入了当前会话中。所以这个方法的整体作用可以概括为:根据xml配置文件以及接口类生成一个代理对象
在深入探查如何生成代理对象前,我们可以再大胆猜想一个代理对象的功能,那就是后续所有对接口中方法的调用,都通过该代理对象找到对应的xml中配置的sql,然后根据接口中方法的入参,再动态生成完整的sql,最后再通过jdbc提交查询,接口返回后再通过xml的配置解析成对应的java类并返回。到这对于第三问和第四问是不是有了一点头绪了,至于是不是猜想的这样,我暂时也不知道,后面会再讲。这里我们还是回到现在的流程,继续深入探究下代理对象的生成逻辑:

这个mapperRegistry是不是有点熟悉,它是我们解析xml中生成的对象,该对象有两个属性一个是config,引用了当前Configuration对象,还有一个则是 knownMappers,它是一个map集合,key是接口类,value则是一个包含了当前接口类的mapper代理工厂

 可以看到,首先是从map集合中获取当前接口的mapper代理工厂,随后则通过newInstance方法生成代理对象,我们接着向下看:

  public T newInstance(SqlSession sqlSession) {
    //创建要代理的mapper对象(注意区分代理对象与要代理的对象)
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    //生成代理对象
    return newInstance(mapperProxy);
  }

前一个方法主要是创建要代理的mapper对象,并将session会话,接口以及需要缓存的方法等属性信息传入mapper对象中。后一个方法则是创建代理对象的方法,我们继续看一下:

 到这里很多人会发懵,生成个动态代理对象怎么没了,这后续的逻辑怎么追踪一探究竟。

不要急,我们到这先大致理一下本文前面的思路,总结起来大致是根据xml配置信息生成sqlsession会话对象,然后该会话依据接口和配置信息动态生成了一个mapper代理对象。后续对接口的访问都会落到mapper代理对象上。因为该对象是动态生成的,所以我们这里看不到后续具体的执行逻辑。具体的执行逻辑只能在执行具体的方法时才可以窥探到。所以这里我们接着向下看:

 可以看到此时的接口对象已经是一个mapper代理对象了,而接口方法则只能由该代理对象实现,接下来我们看下queryById的执行逻辑:

因为queryById的执行代码是动态生成的,我们直接点击该方法只会进入到接口类中,如果想进入具体的实现逻辑中,必须要通过debug进入,下面截图是IDEA进入的方式:

可以看到,此时进入到了要代理的mapper对象中。

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      //如果代理方法属于Object类进行的处理(不能达到我们探查源码目的,不深究)
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      //如果代理方法属于默认方法进行的处理(不能达到我们探查源码目的,不深究)
      } else if (isDefaultMethod(method)) {
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    //获取要代理的mapper方法的对象,从map集合中获取,获取不到则新建并放入集合中
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    //执行代理的mapper方法
    return mapperMethod.execute(sqlSession, args);
  }

可以看到,核心在最后两行,我们先看mappermethod的获取:

  private MapperMethod cachedMapperMethod(Method method) {
    //从方法缓存集合中获取,获取不到则新建并放入集合
    MapperMethod mapperMethod = methodCache.get(method);
    if (mapperMethod == null) {
      //通过构造器创建 MapperMethod 对象
      mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
      methodCache.put(method, mapperMethod);
    }
    return mapperMethod;
  }

  public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    //通过配置信息,接口类以及方法信息创建SqlCommand对象(包含了要执行的sql信息)
    this.command = new SqlCommand(config, mapperInterface, method);
    //创建MethodSignature,主要用于存储结果返回类型相关的信息
    this.method = new MethodSignature(config, mapperInterface, method);
  }

可以看到先是从一个map集合获取MapperMethod对象,获取不到则通过构造器创建,而MapperMethod也只是创建了两个属性对象,一个是存储了执行sql相关的信息,一个则是包含了返回类型相关的信息。下面我们简单看下这个对象的构造:

public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
      //获取方法名和类名
      final String methodName = method.getName();
      final Class<?> declaringClass = method.getDeclaringClass();
      //根据方法名类型信息获取要执行sql的信息
      MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
          configuration);
      // 如果当前代理方法找不到对应的sql信息则抛出异常
      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 {
        // 找到当前方法在xml对应的sql信息后,获取sql的唯一名称以及类型信息,并赋值到SqlCommand对象属性中,以便后续使用
        name = ms.getId();
        type = ms.getSqlCommandType();
        if (type == SqlCommandType.UNKNOWN) {
          throw new BindingException("Unknown execution method for: " + name);
        }
      }
    }


private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
        Class<?> declaringClass, Configuration configuration) {
      //根据类信息和方法名构建key
      String statementId = mapperInterface.getName() + "." + methodName;
      // 如果配置文件中包含当前方法对应的sql信息,则直接返回。(这块可以结合前面一篇文章看,在上一篇中讲到xml文件解析后会将每条sql相关的信息存储到一个集合中,这个集合的key由包名类名方法名组成,value则是sql相关的信息)
      if (configuration.hasStatement(statementId)) {
        return configuration.getMappedStatement(statementId);
      } else if (mapperInterface.equals(declaringClass)) {
        return null;
      }
      // 如果当前类和方法生成的key找不到对应的sql信息,则遍历其父接口信息(未深究)
      for (Class<?> superInterface : mapperInterface.getInterfaces()) {
        if (declaringClass.isAssignableFrom(superInterface)) {
          MappedStatement ms = resolveMappedStatement(superInterface, methodName,
              declaringClass, configuration);
          if (ms != null) {
            return ms;
          }
        }
      }
      return null;
    }

SqlCommand的生成仔细品一品,是不是我们第三问的答案出来了。我来简单说下为什么第三问解决了。上篇文章中解析xml时将sql信息存储到了集合中,其key是由包名类型方法名组成,不过这时的名称都是从xml中解析得到的。而到了具体执行的时候,则根据实际的全路径类名(包含包名)加上方法名组成key,此时再去集合中查找对应的sql信息。所以如果配置文件中的namespace、id信息和接口类、方法名不同,则会抛出异常。所以到这里可以认为第三问的答案已经获得了。

下面我们接着讲:

 MapperMethod方法中除了构造SqlCommand对象外,还创建了MethodSignature对象,这个对象主要包含了一些返回类型信息,主要和返回结果的解析相关,这和我们的问题关联不大,所以这里不深究。MapperMethod构建完后则进入正式的执行阶段,这也对应我们最后一个问题,该追踪链路也比较长,这里就放到下一篇文章中讲。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值