深入剖析-Mybatis-原理(二)
而mybatis 还有另一种写法,我们在测试代码也写过,如下:
1 | UserInfoMapper userInfoMapper = sqlSession.getMapper(UserInfoMapper.class); |
这段代码非常的面向对象,也非常的利于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 方法,该方法是如何执行的呢?
1 | public Object execute(SqlSession sqlSession, Object[] args) { |
该方法主要是判断方法的类型,也就是我们的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 !!!!