mybatis作为当前主流的ORM框架之一,其流行程度远超过了JPA,Hibernate,Bee等其它三方ORM框架,尤其是在与Spring无缝黏合之后。最近相当一段时间,对mybatis的源码(v3.5.6)和设计进行了一些研究,接下来会分章节给大家分享。

整体设计架构

image.png

核心门面接口

SqlSession:作为访问数据库的门面(或外观),其对外屏蔽了通过mybatis数据库访问复杂度,大大降低了外部程序对mybatis的内部代码依赖符合单一职责和迪米特法则,同时又提供了统一的访问入口和能力。

SqlSessionFactory:SqlSession的工厂模式封装, 其默认实现为DefaultSqlSessionFactory类;

SqlSessionFactoryBuilder: 全局的SqlSessionFactory工厂创建,内部通过build()方法创建出SqlSessionFactory的具体工厂,其唯一的职责就是对build的各种重载,支持以各种外部形式创建SqlSessionFactory对象。

核心处理层

配置文件解析:主要负责解析mybatis-config.xml全局配置文件,其中包括:properties, settings, typeAliases,typeHandlers,plugins,environments,mappers等主要部分。解析的结果分块缓存之Configuration全局对象,后面此对象讲贯穿整个框架始终。

参数映射:通过ParamNameResolver, 解析mapper接口参数包括参数名,参数值并缓存参数的顺序关系。

SQL解析:解析SQL语句, 预编译SQL PrepareStatement分两阶段解析SQL, 第一阶段在mybatis-config.xml配置文件加载时,将SQL相关的信息解析到MappedStatement对象中(预编译语句,将#{} 替换成 ?),最终缓存进Configuration对象中;第二阶段发生SQL执行阶段,将预编译语句中问号替换成最终的SQL语句。

SQL执行:通过Executor执行组件作为入口,内部调用RoutingStatementHandler路由接口,将请求最终路由到特定的StatementHandler中,最终调用原生jdbc PrepareStatement完成数据库读写。

结果集映射: 对原生的ResultSet的解析和转换成POJO对象的过程。

插件:mybatis中的插件,是继承其内置的Interceptor接口,开发对这些接口对象的方法进行拦截增强:Executor,StatementHandler,ParameterHandler,ResultSetHandler。

基础支撑层

数据源组件:对池化的数据库连接进行创建和管理。

事务管理:mybatis的事务功能比较弱,基本都是基于jdbc的隔离、commit或rollback的简单包装,这块一般和Spring的事务结合使用。

缓存组件:mybatis默认开启一级缓存(也可以在mapper.xml中关闭特定语句的一级缓存),它是基于SqlSession级的,并且是线程安全的。而二级缓存默认是不开启的,需要在特定的mapper.xml中开启,二级缓存的生命周期是应用程序级的,二级缓存的单位是namespace级别(也可以在多个namespace中共享同一个缓存)。

binding组件:主要用于构建mybatis的Mapper编程模型(后面会详细介绍)。

反射组件:在底层提供动态反射完整封装,支撑反射对象及对象属性、设置对象值等能力。关键部件:ObjectFactory,ObjectWrapper,ObjectWrapperFactory,ReflectorFactory,Reflector,

MetaClass,MetaObject等。

类型转换:主要提供内置的jdbc数据库类型和java程序数据类型,以及自定义的数据类型处理进行映射和转换。

日志组件:提供在mybatis中日志打印能力。解决和集成众多三方日志组件包括:slf4j, commonslog, log4j2, log4j, jdklog等。并且按优先级(前面的优先级)动态扫描加载本地的日志组件。

资源加载:提供对mybatis-config.xml、properties, mapper.xml,class等外部资源的扫描、缓存和加载能力。

解析器:在底层提供对配置文件、参数属性、sql语句等提供解析能力。

今天首先分析一下mybatis的日志组件。对于日志组件我这边是带着如下问题去看源码的:

  1. mybatis是否支持主流的日志组件?从设计上,它是如何做到的?

  2. 如何兼容主流日志组件,又能支撑mybatis自身的业务能力?

  3. 如何方便的提供日志访问、优先级和扫描机制 ?

  4. mybatis其它业务组件,是如何集成日志组件开展业务的?

兼容三方日志组件设计

mybatis本身不提供原生的日志打印和存储功能,它是靠适配其它第三方日志组件来实现的。

image.png

如上图所示,我们可以看出mybatis是通过适配其它三方的日志组件实现它自身的Log业务接口的,接着看下面

image.png

image.png

从以上的代码段截图,我们可以清楚地得知,mybatis是采用了适配器设计模式,同时适配了slf4j, commonslog, log4j2, log4j, jdk等日志组件。这些日志组件本身所提供的接口和日志级别都各不相同,而mybatis中需要的日志业务能力是通过定义的Log接口来支持的。所以这里需要将其它日志的接口转换成mybatis内置的业务和日志定义,而同时又不能去改动第三方的日志接口实现细节。那么此时适配器模式就是最佳的选择。适配的本质就是转换。

image.png

提供统一访问入口

那么既然mybatis运用适配器模式,适配了这么多三方日志组件的接口。那么它内部使用的时候,如何知道该使用那个组件来支撑业务 ? 根据什么规则来选择?又如何能知道使用的组件在我们的应用程序中是否存在?显然要解决这些问题mybatis需要单独的设计。

image.png

它内部使用LogFactory工厂模式,在内部构建日志对象。那它内部如何实现的呢?

image.png

在static构造器中对三方日志适配接口进行按优先级选择,优先级为:slf4j ---> commonslog ----> log4j2 ----> log4j ---->jdklog , 采用这个优先级自动对工程中依赖的包进行扫描,发现有可用的包就采用,其它的日志包忽略。

image.png

image.png

仔细看看以上代码的截图,大家是否就明白,它巧妙地采用了JDK8的特性,通过顺序调用多个静态方法来达到按优先级自动扫描确定使用哪个组件的设计目的

日志组件的业务集成

OK, 既然组件和组件的入口的设计好了,那么接下大家是不是更关注的问题是,mybatis设计的日志功能究竟用在框架的哪些地方,是如何设计集成的,各个地方的作用是什么?还有就是看看是不是和我们平时在业务模块中使用日志是一样呢 ?

首先要解决第一个问题,我们要知道mybatis主要在哪些地方会打日志。看下面,一个正常的mapper接口调用过程中,日志是如何输出的。

image.png

从以上我们可以得知,mybatis在运行的时候,会在这三个关键的点打印各种日志信息。OK, 那我们就找到对应的源码

image.png

先来看看ConnectionLogger,通过SQL连接数据库成功时打印日志的实现代码

image.pngimage.png

上面截图,大家都看得很清楚了吧?我就不用多说了。那么我们再来看看PreparestatementLogger代理实现过程代码

image.pngimage.png

OK, 如上图一切尽在不言中。最后我们来看看ResultSetLogger对结果集是如何增强的

image.pngimage.png

怎么样,是不是So easy? 从以上的结果我们可以看出,mybatis是采用了Proxy代理技术,分别增强了Connection、Preparestatement、Statement以及ResultSet等对象,让它具有了在特定位置打印个性化日志的能力。看到了这里,大家会不会想这些分散的Logger是不是该有一个总的调用入口呢?在mybatis中答案是肯定的。看看下面这个位置

image.png

image.png

从MappedStatement赋予日志能力到以上各个Logger代理增强的实际业务点,还会经过比较长封装过程,后面会一一解析。现在暂时不展开这个点。


总结

从mybatis源码中我们可以学到很多优秀的设计经验、经典的设计模式和原则、设计衔接处理点巧妙运用的技巧。今天只谈到了mybatis的日志组件, 下次我们将分享mybatis其它重要的基础组件,请继续关注!