日志模块是mybatis中很重要的一个模块,在我们查询问题和分析sql的时候,经常会将mybatis打印sql的功能打开,但是mybatis并没有给我们提供打印日志的能力,那它是如何使用我们系统支持的日志模块进行日志打印的呢?今天我们来一探究竟。
一.日志模块需求分析
Mybatis的日志模块具备如下特点:
- MyBatis没有提供日志的实现类,需要接入第三方的日志组件,但第三方日志组件都有各自的Log级别,且各不相同,而MyBatis统一提供了trace、debug、warn、error四个级别;
- 自动扫描日志实现,并且第三方日志插件加载优先级如下:slf4J → commonsLoging → Log4J2 → Log4J → JdkLog;
- 日志优雅的嵌入到主体功能中,不需要额外改动代码;
二.mybatis日志模块实现分析
2.1.第三方日志组件兼容的实现
从需求上分析,日志模块的第一个需求是一个典型的使用适配器模式的场景。
2.1.1.适配器模式含义
适配器模式(Adapter Pattern) 是作为两个不兼容的接口之间的桥梁,将一个类的接口转换成客户希望的另外一个接口。适 配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作;类图如下:
角色代表含义:
Target:目标角色,期待 得到的接口.
Adaptee:适配者角色, 被适配的接口.
Adapter:适配器角色, 将源接口转换成目标 接口.
适用场景:当调用双方都不太容易修改的时候,为了复用现有组件可以使用适配器模式;在 系统中接入第三方组件的时候经常被使用到;
PS:如果系统中存在过多的适配器,会增加 系统的复杂性,设计人员应考虑对系统进行重构;
2.1.2.MyBatis 日志模块适配器的使用
mybatis的日志模块的主要实现如下:
- Target:mybatis中org.apache.ibatis.logging.Log扮演者target角色,用于对内提供统一的日志接口;
- Adaptee:其他日志组件组件如 slf4J 、commonsLoging 、 Log4J2 等被包含在适配器中,用于被适配。
- Adapter:针对每个日志组件都提供了适配器, 每 个 适 配 器 都 对 特 定 的 日 志 组 件 进 行 封 装 和 转 换 ,保证对外提供一同的使用标准。; 如 Slf4jLoggerImpl JakartaCommonsLoggingImpl 等;
mybatis的日志模块实现采用适配器模式,日志组件(Target)、适配器以及统一接口(Log 接口) 定义清晰明确符合单一职责原则;同时,客户端在使用日志时,面向 Log 接口编程,不需要 关心底层日志模块的实现,符合依赖倒转原则;最为重要的是,如果需要加入其他第三方日 志框架,只需要扩展新的模块满足新需求,而不需要修改原有代码,这又符合了开闭原则。
2.2.日志组件顺序加载的实现
日志组件的顺序加载实现比较简单,mybatis加载之后在静态代码块中完成的加载。不在过多讲解。
2.3.优雅的使用日志模块的实现
我们在打开mybatis日志的时候,无需任何多余的操作,只需将配置打开即可将看到执行日志。那么我们要实现优雅日志,首先我们得搞清楚,在什么情况下要打日志,通过我们对日志的使用,一般有如下几个位置需要进行日志打印
- 在创建 prepareStatement 时,打印执行的 SQL 语句;
- 访问数据库时,打印参数的类型和值
- 查询出结构后,打印结果数据条数
在mbatis的日志模块中有 BaseJdbcLogger、ConnectionLogger、PreparedStatementLogger 和 ResultSetLogge 通过动态代理负责在不同的位置打印日志;
说到动态代理那么很显然,mybatis的日子增强是通过代理模式进行实现的。那么我们了解下什么是代理模式。
2.3.1.代理模式
代理模式定义:给目标对象提供一个代理对象,并由代理对象控制对目标对象的引用;
例如小王要买个化妆品,但是他不知道渠道,只能通过小张这个海外代购渠道去购买化妆品店。 因此小王不需要和化妆品店直接交互,由小张代理帮他购买即可。
主要目的:
- 通过引入代理对象的方式来间接访问目标对象,防止直接访问目标对象给系统带来的 不必要复杂性;
- 通过代理对象对原有的业务增强;
代理模式有静态代理和动态代理两种实现方式。
静态代理
这种代理方式需要代理对象和目标对象实现一样的接口。
优点:可以在不修改目标对象的前提下扩展目标对象的功能。
缺点:由于代理对象要实现与目标对象一致的接口,会产生过多的代理类。不易维护。一旦接口增加方法,目标对象与代理对象都要进行修改。
动态代理
动态代理利用了 JDK API,动态地在内存中构建代理对象,从而实现对目标对象的代理功能。
动态代理又被称为 JDK 代理或接口代理。
静态代理与动态代理的区别主要:
1. 静态代理在编译时就已经实现,编译完成后代理类是一个实际的 class 文件
2. 动态代理是在运行时动态生成的,即编译完成后没有实际的 class 文件,而是在运行时 动态生成类字节码,并加载到 JVM 中 注意:动态代理对象不需要实现接口,但是要求目标对象必须实现接口,否则不能使用动态 代理。
JDK 中生成代理对象主要涉及两个类,第一个类为 java.lang.reflect.Proxy,通过静态方法 newProxyInstance 生成代理对象,第二个为 java.lang.reflect.InvocationHandler 接口,通过 invoke 方法对业务进行增强;
2.4.Mybatis日志模块实现分析
2.4.1.基础日志组件
刚才我们讲到mybatis的日志模块中有 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;如果是更新,直接打印影响的行数
ResultSetLogge:负责打印数据结果信息;
2.4.2.日志模块初始化时机
既然在 Mybatis中 Executor 才是访问数据库的组件,日志功能则肯定是在 Executor中被嵌入的, 具体代码在 org.apache.ibatis.executor.SimpleExecutor.prepareStatement(StatementHandler, Log) 方法中,如下图所示:
学习Mybatis源码是学习设计模式最好的方式,对于以后业务开发模块设计有很大的帮助~