Mybatis源码阅读-日志模块(适配器模式+代理模式)
#最近在参与大数据云平台建设,需要处理各种类型的数据,其中结构化数据占多数,使用的是一个简单好用的结构化数据映射处理框架Mybatis。Mybatis中有许多常见的设计模式,正好通过博客的方式进行记录,方便学习和回顾。
Part1 谈到设计模式,我们首先想到的就是基本的设计六原则
单一职责原则:一个类或者一个接口只负责唯一项职责,尽量设计出功能单一的接口;
依赖倒转原则:高层模块不应该依赖低层模块具体实现,解耦高层与低层。既面向接口编程,当实现发生变化时,只需提供新的实现类,不需要修改高层模块代码;
开放-封闭原则:程序对外扩展开放,对修改关闭;换句话说,当需求发生变化时,我们可以通过添加新模块来满足新需求,而不是通过修改原来的实现代码来满足新需求;
迪米特法则:一个对象应该对其他对象保持最少的了解,尽量降低类与类之间的耦合度;
里氏代换原则:所有引用基类(父类)的地方必须能透明地使用其子类的对象;
接口隔离原则:客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上;
Part2 日志模块-适配器模式
1 首先MyBatis作为一个开源工具,它在实现的时候,就需要考虑这个工具的基本需求:
2 1.MyBatis没有提供日志的实现类,需要接入第三方的日志组件,但第三方日志组件都有各自的Log级别,且各不相同,而MyBatis统一提供了trace、debug、warn、error四个级别;
3 2.自动扫描日志实现,并且第三方日志插件加载优先级如下:slf4J → commonsLoging → Log4J2 → Log4J → JdkLog;
4 3.日志的使用要优雅的嵌入到主体功能中;
5 1)我们通过mybatis的基本功能需求,就可以了解到,要实现上面的需求,有一种设计模式就是很适合这种需求-----》**适配器模式**
6 2)适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁,将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作;

Target:目标角色,期待得到的接口
Adaptee:适配者角色,被适配的接口
Adapter:适配器角色,将源接口转换成目标接口

通过适配器基本的原理和应用场景,可以知道和我们的需求契合度很高,接下来看一下Mybatis的具体实现是不是这样实现的
首先Mybatis有对外统一的Log接口也就是:Target

另外也有适配者角色Adaptee,被适配的接口。其他日志组件组件如 slf4J 、commonsLoging 、Log4J2 等被包含在适配器中。
这里用org.apache.commons.logging.Log为例:
引入其他组件的jar包,在JakartaCommonsLoggingImpl中实现org.apache.ibatis.logging.Log默认的接口,但是这些Log接口的具体实现则由org.apache.commons.logging.Log进行实现。

最后还有适配器Adapter,将源接口转换成目标接口。针对每个日志组件都提供了适配器,每个适配器都对特定的日志组件进行封装和转换;如Slf4jLoggerImpl,JakartaCommonsLoggingImpl等。

所以通过源码我们可以得到整个日志模块的类图:

这里还要讲一下这个LogFactory,看名字就知道这个是一个创建Log对象的,由于我们系统可能会引用多种日志组件,那我们的mybatis最后到底使用的哪一个第第三方日志组件,这个选择就是在LogFactory实现的,在这个类里面有一个静态代码块来实现加载的优先级:slf4J → commonsLoging → Log4J2 → Log4J → JdkLog;



不同的log实现通过一个指定的构造方法进行实例化:

Part3 日志模块-代理模式
1 上面讲了日志模块对于第三方日志组件的适配过程,但是日志模块适配了,我们是如何优雅的运用在我们的sql执行的打印功能呢?比如会打印执行的sql,传入的参数,sql执行结果等信息。
2 这里要优雅的嵌入mybatis的执行过程中,肯定首先要考虑基本的设计原则,所以在这里mybatis就使用了新的模式:代理模式。
代理模式定义:给目标对象提供一个代理对象,并由代理对象控制对目标对象的引用;目的:
(1)通过引入代理对象的方式来间接访问目标对象,防止直接访问目标对象给系统带来的不必要复杂性;
(2)通过代理对象对原有的业务增强;
代理模式有主要有两种类型:
3.1 静态代理
这种代理方式需要代理对象和目标对象实现一样的接口。
优点:可以在不修改目标对象的前提下扩展目标对象的功能。
缺点:
冗余。由于代理对象要实现与目标对象一致的接口,会产生过多的代理类。
不易维护。一旦接口增加方法,目标对象与代理对象都要进行修改。
3.2 动态代理
动态代理利用了 JDK API,动态地在内存中构建代理对象,从而实现对目标对象的代理功能。动态代理又被称为 JDK 代理或接口代理。静态代理与动态代理的区别主要在:
- 静态代理在编译时就已经实现,编译完成后代理类是一个实际的 class 文件
- 动态代理是在运行时动态生成的,即编译完成后没有实际的 class 文件,而是在运行时
动态生成类字节码,并加载到 JVM 中
注意:动态代理对象不需要实现接口,但是要求目标对象必须实现接口,否则不能使用动态代理。
JDK 中生成代理对象主要涉及两个类,第一个类为 java.lang.reflect.Proxy,通过静态方法
newProxyInstance 生成代理对象,第二个为 java.lang.reflect.InvocationHandler 接口,通过invoke 方法对业务进行增强;
类图如下

- 了解了代理模式的基本原理,现在就要弄清楚mybatis哪些地方会有日志打印的功能:
- 在创建 prepareStatement 时,打印执行的 SQL 语句;
- 访问数据库时,打印参数的类型和值
- 查询出结构后,打印结果数据条数
因此在日志模块中有 BaseJdbcLogger、ConnectionLogger、PreparedStatementLogger 和ResultSetLogge 通过动态代理负责在不同的位置打印日志;几个相关类的类图如下

BaseJdbcLogger:所有日志增强的抽象基类,用于记录 JDBC 那些方法需要增强,保存运
行期间 sql 参数信息;

ConnectionLogger:负责打印连接信息和 SQL 语句。通过动态代理,对 connection 进行
增强,如果是调用 prepareStatement、prepareCall、createStatement 的方法,打印要执
行的 sql 语句并返回 prepareStatement 的代理对象(PreparedStatementLogger),让
prepareStatement 也具备日志能力,打印参数;

PreparedStatementLogger:对 prepareStatement 对象增强,增强的点如下:
增强 PreparedStatement 的 setxxx 方法将参数设置到 columnMap、columnNames、
columnValues,为打印参数做好准备;
增强 PreparedStatement 的 execute 相关方法,当方法执行时,通过动态代理打印
参数,返回动态代理能力的 resultSet;
如果是查询,增强 PreparedStatement 的 getResultSet 方法,返回动态代理能力的
resultSet;如果是更新,直接打印影响的行数


ResultSetLogger:负责打印数据结果信息:

前面讲这么多,都是日志功能的实现,那日志功能是怎么加入主体功能的?
既然在 Mybatis中 Executor 才是访问数据库的组件,所以日志功能是在 Executor 中被嵌入的:

在ReuseExecutor的父类BaseExecutor中的getConnection方法中,我们可以看到使用反射完成了ConnectionLogger对象的实例化。
参考 https://blog.csdn.net/xqqTechnology/article/details/106616297
浙公网安备 33010602011771号