深入剖析 mybatis 原理(二)

# 前言

在上篇文章中我们分析了 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 !!!!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,826评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,968评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,234评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,562评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,611评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,482评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,271评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,166评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,608评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,814评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,926评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,644评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,249评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,866评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,991评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,063评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,871评论 2 354

推荐阅读更多精彩内容