深入剖析 mybatis 原理(二)

本贴最后更新于 2562 天前,其中的信息可能已经天翻地覆

# 前言

在上篇文章中我们分析了 sqlSession.selectOne("org.apache.ibatis.mybatis.UserInfoMapper.selectById", parameter) 代码的执行过程,我们说,这种方式虽然更接近 mybaits 的底层,但不够面向对象,也不利于 IDEA 工具的编译期排错。

而 mybatis 还有另一种写法,我们在测试代码也写过,如下:

   UserInfoMapper userInfoMapper = sqlSession.getMapper(UserInfoMapper.class);
   UserInfo userInfo2 = userInfoMapper.selectById(1);

这段代码非常的面向对象,也非常的利于 IDE 工具在编译期间排错。实际上这是 mybait 为我们做的工作,是对第一种的方式的一种更抽象的封装。同时,这种方式也是我们现在开发常用的一种方式,所以,我们必须剖析的原理,看看他到底是如何实现的。想必有经验的大佬都能猜测到,肯定用动态代理的技术。不过,我们还是从源码看个究竟吧!

1. 从 getMapper 方法进入源码

实际上调用了 configuration 的 getMapper 方法:

configuration 实际上调用了 mapperRegistry.getMapper:

注意,要停下来,看看 mapperRegistry 是什么,从名字上看出来,该对象是 Mapper 映射器注册容器,我们看看该对象中有什么?

这是该类的属性,有一个 Configuration 对象,有一个 Map,Map 存放什么数据呢,key 是 class 类型, value 是 MapperProxyFactory 类型,MapperProxyFactory 又是什么呢,看名字是映射器代理对象工厂,我们看看该类:

这是该类的结构图,有一个 Class 对象,表示 映射器的接口,有一个 Map 表示映射方法的缓存。并且由 2 个 newInstance 方法,看名字肯定是创建代理对象啦。我们看看这 2 个方法:

调用了动态的技术,根据给定的接口和 SqlSession 和方法缓存,创建一个代理对象 MapperProxy ,该类实现了 InvocationHandler 接口,因此我们需要看看他的 invoke 方法:

这是 MapperProxy 的 invoke 方法,该方法首先判断方法的 class 是否继承 Object ,如果是,就不使用代理,直接执行,如果该方法是默认的,那么就执行默认方法(该方法是针对 Java7 以上版本对动态类型语言的支持,不进行详述)。我们这里肯定不是,执行下面的 cachedMapperMethod 方法 并调用返回对象 MapperMethod 的 execute 方法。

我们看看 cachedMapperMethod 方法,该方法应该是跟缓存相关,我们看看实现:

该方法首先从缓存中取出,如果没有,便创建一个,并放入缓存并返回。我们关注一下 MapperMethod 的构造方法:

该构造方法拿着这三个参数创建了两个对象,一个是 SQL 命令对象,一个方法签名对象,这两个类都是 MapperMethod 的静态内部类。我们来看看这两个类。

SqlCommand 类:

该类由 2 个属性,一个是 name,表示 sql 语句的名称,一个是 type 自动,表示 sql 语句的类型,sql 语句的名称是怎么来的呢?我们从代码中看到是从 resolveMappedStatement 方法返回的 MappedStatement 对象中得到的。而 SqlCommandType 也是从该方法中得到的。那么我重点关注该方法。

我们刚刚说,name 是怎么来的,是调用了 MappedStatement 对象的 getId 方法来的,而 id 是怎么来的呢?是 mapperInterface.getName() + "." + methodName 拼起来的,也就是接口名称和方法名称成为了一个 id,素以注意,该方法不能重载。因为他不关注参数。根据 id 从 configuration 获取解析好的 MappedStatement(存放在 hashmap 中)。如果没有这个 id 的话,注意,该方法最后还会递归调用该接口的所有父接口的 resolveMappedStatement ,确保找到给定 id 的 MappedStatement。

那么 SqlCommandType 是什么呢?是个枚举,我们看看该枚举:

该枚举定义我们在 xml 中的标签。是不是很亲切?

那么 MethodSignature 是什么呢?我们看看该类由哪些属性:

看该类名字知道是方法的签名,因此包含方法的很多信息,是否返回多值,是否返回 map,是否返回游标,返回值类型等等,这些属性都是在构造方法中注入的:

看完了内部类,回到 MapperProxy 的 invoke 方法,现在有了 MapperMethod 对象,就要执行该对象的 execute 方法,该方法是如何执行的呢?

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    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()) {
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          result = executeForCursor(sqlSession, args);
        } else {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);
        }
        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;
  }

该方法主要是判断方法的类型,也就是我们的 insert select 标签,根据不同的标签执行不同的方法,如果是 SELECT 标签就还要再判断他的返回值,根据不同的返回值类型执行不同的方法。默认执行 sqlSession 的 selectOne 方法,看到这里,是不是一目了然了呢?也就是说 getMapper 最终还是调用 SqlSession 的 selectOne 方法,只不过通过动态代理封装了一遍,让 mybatis 来管理这些字符串样式的 key,而不是让用户来手动管理。

我们回到 MapperRegistry 的 getMapper 方法:

首先根据接口类型从缓存中取出,如果没有,则抛出异常,因为这些缓存都是在解析配置文件的时候放入的。根据返回的映射代理工厂,调用该工厂的方法,传入 SqlSession 返回一个接口代理:

这段代码其实我们已经看过了,创建一个实现类 InvocationHandler 接口的对象,然后使用 JDK 动态代理创建实例返回,而 InvocationHandler 的实现类 MapperProxy 的代码我们刚刚也看过了。主要逻辑在 invoke 中,在该方法中调用 SqlSession 的 selectOne 方法。后面的我们就不说了,和上一篇文章的逻辑一样,就不赘述了。

2. 总结

大家可能注意到了,这篇文章不长,因为主要逻辑在上篇文章中,这里只不过将 Mybatis 如何封装代理的过程解析了一遍。主要实现这个功能是的是 mybatis 的 binding 包下的几个类:

这几个类完成了对代理的封装和对目标方法的调用。当然还有 SqlSession,可以看出,mybatis 的模块化做的非常好。

我们也来看看这几个类的 UML 图:

可以看到,所有的类都关联着 SqlSession,由此可以看出 SqlSession 的重要性。而我们今天解析的代码只不过在 SqlSession 外面封装了一层,便于开发者使用,否则,配置这些字符串,就太难以维护了。

好了,今天的 Mybatis 分析就到这里了。我们通过一个 demo 知道了 mybatis 的运行原理,由此,在以后的开发中,遇到错误时,再也不是黑盒操作了。可以深入源码去找真正的原因。当然,阅读源码带来的好处肯定不止这些。

good luck !!!!

  • MyBatis

    MyBatis 本是 Apache 软件基金会 的一个开源项目 iBatis,2010 年这个项目由 Apache 软件基金会迁移到了 google code,并且改名为 MyBatis ,2013 年 11 月再次迁移到了 GitHub。

    170 引用 • 414 回帖 • 387 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • someone

    哇塞
    博主你更新好快啊
    不愧是博主 相当厉害
    希望博主不停更新的同时 也要注意身体
    加油!

  • someone

    会注意身体的,谢谢关心。