MyBatis日志模块源码分析
MyBatis源码的logging包下是日志模块的相关实现,Mybatis日志模块通过适配器模式和代理模式优雅的实现了SQL日志的输出功能。
一. 适配器模式实现了MyBatis对第三方日志框架的适配
Mybatis内部没有提供日志实现类,需要接入第三方的日志组件,但第三方组件都有自己的log级别,并且各不相同,Mybatis 在内部定义了Log
接口统一提供了trace debug warn error四个日志级别。
/**
* 由于不同的日志框架,拥有不同的日志级别,所以MyBatis在内部定义了Log接口,
* 用于让其他日志框架适配MyBatis内部规定的日志级别
*/
public interface Log {
boolean isDebugEnabled();
boolean isTraceEnabled();
/**
* -----------规定的日志级别-----------
*/
void error(String s, Throwable e);
void error(String s);
void debug(String s);
void trace(String s);
void warn(String s);
}
Mybatis 使用适配器模式,在每个第三方日志厂商和自己的log之间都存在一个适配器,将第三方的日志级别适配为自己的log级别。
其中日志适配器的继承体系如下:
我们以Jdk14LoggingImpl
类(JDK日志框架的适配器)源码为例,分析适配器所做的事情。可以看到在JDK自带的日志框架中包含SEVERE
、WARNING
、INFO
、CONFIG
、FINE
等8个日志级别,而Mybatis只提供了error
、debug
、trace
、warn
四种日志日志级别,这就需要认为将两者的日志级别对应起来。
/**
* JDK内置日志框架的适配器,用于适配MyBatis内部约定的Log接口
* @author Clinton Begin
*/
public class Jdk14LoggingImpl implements Log {
private final Logger log;
public Jdk14LoggingImpl(String clazz) {
log = Logger.getLogger(clazz);
}
@Override
public boolean isDebugEnabled() {
return log.isLoggable(Level.FINE);
}
@Override
public boolean isTraceEnabled() {
return log.isLoggable(Level.FINER);
}
@Override
public void error(String s, Throwable e) {
log.log(Level.SEVERE, s, e);//JDK中SEVERE级别日志对应MyBatis中error
}
@Override
public void error(String s) {
log.log(Level.SEVERE, s);
}
@Override
public void debug(String s) {
log.log(Level.FINE, s);
}
@Override
public void trace(String s) {
log.log(Level.FINER, s);
}
@Override
public void warn(String s) {
log.log(Level.WARNING, s);
}
}
可以看到使用适配器模式,我们解决第三方日志框架内部实现不一致的问题,但是这些日志适配器的使用就需要借助Mybatis提供的日志工厂类了(org.apache.ibatis.logging.LogFactory
)。在该类拥有一个静态代码块,用于动态加载项目中依赖的日志框架:
static {
//按照顺序扫描当前项目中配置的日志框架。注:双冒号是JDK8的语法,代表方法引用(语法糖)
tryImplementation(LogFactory::useSlf4jLogging);
tryImplementation(LogFactory::useCommonsLogging);
tryImplementation(LogFactory::useLog4J2Logging);
tryImplementation(LogFactory::useLog4JLogging);
tryImplementation(LogFactory::useJdkLogging);
tryImplementation(LogFactory::useNoLogging);
}
该类会按照slf4J → commonsLoging → Log4J2 → Log4J → JdkLog顺序加载日志框架,如果加载成功一个,就不会加载后面的日志框架了。
private static void tryImplementation(Runnable runnable) {
//如果前面的适配器都没有创建成功,则试图创建当前类型的适配器
if (logConstructor == null) {
try {
runnable.run();
} catch (Throwable t) {
// ignore
}
}
}
public static synchronized void useSlf4jLogging() {
setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
}
/**
* 设置具体的日志实现类构造器
*
* @param implClass
*/
private static void setImplementation(Class<? extends Log> implClass) {
try {
Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
//这里调用目标日志框架的适配器中的构造器,试图创建适配器对象,如果没有该框架的依赖则会抛出异常
Log log = candidate.newInstance(LogFactory.class.getName());
if (log.isDebugEnabled()) {
log.debug("Logging initialized using '" + implClass + "' adapter.");
}
logConstructor = candidate;
} catch (Throwable t) {
throw new LogException("Error setting Log implementation. Cause: " + t, t);
}
}
二. 代理模式实现了SQL日志输出
Mybatis 日志的使用优雅的嵌入到主体功能中(动态代理增强),接下来我们看一下是如何进行日志增强的。
2.1日志输出的体系结构
org.apache.ibatis.logging.jdbc
包中存放就是JDBC相关对象的增强类,它们就是MyBatis实现日志打印的核心:
其中BaseJdbcLogger
类保存着日志打印所需要的信息:
/**
* MyBatis用于输出执行器SQL日志记录的基类。
*
* @author Clinton Begin
* @author Eduardo Macarron
*/
public abstract class BaseJdbcLogger {
//保存PreparedStatement中常用的set方法(用于占位符设值的方法)
protected static final Set<String> SET_METHODS;
//保存PreparedStatement中常用的执行SQL语句的方法
protected static final Set<String> EXECUTE_METHODS = new HashSet<>();
//保存PreparedStatement中set方法的键值对
private final Map<Object, Object> columnMap = new HashMap<>();
//保存PreparedStatement中set方法的key值
private final List<Object> columnNames = new ArrayList<>();
//保存PreparedStatement中set方法的value值
private final List<Object> columnValues = new ArrayList<>();
//用于打印日志的对象
protected final Log statementLog;
而ConnectionLogger
、PreparedStatementLogger
、ResultSetLogger
、StatementLogger
这几个类都实现了InvocationHandler
接口,这也证明了它们分别是对Conneciton
、PreparedStatement
、ResultSet
、Statemtent
类的加强。
2.2 探寻入口
org.apache.ibatis.executor.BaseExecutor
是MyBatis中的执行器,是Mybatis实际操作数据库的类,该类中的getConnection()
方法返回的就是经过加强的连接对象,也就是:ConnectionLogger
:
protected Connection getConnection(Log statementLog) throws SQLException {
Connection connection = transaction.getConnection();
if (statementLog.isDebugEnabled()) {
return ConnectionLogger.newInstance(connection, statementLog, queryStack);
} else {
return connection;
}
}
2.2.1 ConnectionLogger
其中ConnectionLogger
负责在调用prepareStatement()
方法时打印SQL语句日志,并返回经过加强后的PreparedStatement
,也就是PreparedStatementLogger
。
/**
* 负责打印数据库连接相关信息和SQL语句,并创建PreparedStatementLogger
*
* @author Clinton Begin
* @author Eduardo Macarron
*/
public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler {
//真正的连接对象
private final Connection connection;
private ConnectionLogger(Connection conn, Log statementLog, int queryStack) {
super(statementLog, queryStack);
this.connection = conn;
}
@Override
public Object invoke(Object proxy, Method method, Object[] params)
throws Throwable {
try {
//如果调用的是Object的方法则跳过代理
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, params);
}
//如果是调用prepareStatement或者prepareCall,则打印SQL语句
if ("prepareStatement".equals(method.getName()) || "prepareCall".equals(method.getName())) {
if (isDebugEnabled()) {
debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
}
PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
/**
* 创建PreparedStatement的代理类PreparedStatementLogger对象,并返回这个对象
*/
stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
return stmt;
} else if ("createStatement".equals(method.getName())) {
Statement stmt = (Statement) method.invoke(connection, params);
stmt = StatementLogger.newInstance(stmt, statementLog, queryStack);
return stmt;
} else {
return method.invoke(connection, params);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
.....
}
2.2.2 PreparedStatementLogger
PreparedStatementLogger
增强类是为了在调用目标对象的executeQuery
方时输出将要执行的SQL语句,并将返回的ResultSet
加强为ResultSetLogger
:
public final class PreparedStatementLogger extends BaseJdbcLogger implements InvocationHandler {
private final PreparedStatement statement;
private PreparedStatementLogger(PreparedStatement stmt, Log statementLog, int queryStack) {
super(statementLog, queryStack);
this.statement = stmt;
}
@Override
public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
try {
//判断调用的是不是Object的方法
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, params);
}
//如果执行的是SQL执行相关的方法,则打印参数
if (EXECUTE_METHODS.contains(method.getName())) {
if (isDebugEnabled()) {
debug("Parameters: " + getParameterValueString(), true);
}
//清除父类中保存的SQL参数
clearColumnInfo();
if ("executeQuery".equals(method.getName())) {
//执行executeQuery方法,获取查询结果集
ResultSet rs = (ResultSet) method.invoke(statement, params);
//将查询结果集封装成ResultSetLogger代理对象
return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
} else {
//如果不是查询语句,则不生成代理对象,也就不会打印日志
return method.invoke(statement, params);
}
} else if (SET_METHODS.contains(method.getName())) {
/**
* 如果执行的是PreparedStatement中的Set方法,则将对应的参数值放入父类专门保存SQL参数值的容器中
*/
if ("setNull".equals(method.getName())) {
setColumn(params[0], null);
} else {
setColumn(params[0], params[1]);
}
return method.invoke(statement, params);
} else if ("getResultSet".equals(method.getName())) {
ResultSet rs = (ResultSet) method.invoke(statement, params);
return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
} else if ("getUpdateCount".equals(method.getName())) {
int updateCount = (Integer) method.invoke(statement, params);
if (updateCount != -1) {
debug(" Updates: " + updateCount, false);
}
return updateCount;
} else {
return method.invoke(statement, params);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
2.2.3 ResultSetLogger
ResultSetLogger
增强类是为了在获取结果集时打印结果集信息。
public final class ResultSetLogger extends BaseJdbcLogger implements InvocationHandler {
private static final Set<Integer> BLOB_TYPES = new HashSet<>();
private boolean first = true;
private int rows;
private final ResultSet rs;
private final Set<Integer> blobColumns = new HashSet<>();
static {
BLOB_TYPES.add(Types.BINARY);
BLOB_TYPES.add(Types.BLOB);
BLOB_TYPES.add(Types.CLOB);
BLOB_TYPES.add(Types.LONGNVARCHAR);
BLOB_TYPES.add(Types.LONGVARBINARY);
BLOB_TYPES.add(Types.LONGVARCHAR);
BLOB_TYPES.add(Types.NCLOB);
BLOB_TYPES.add(Types.VARBINARY);
}
private ResultSetLogger(ResultSet rs, Log statementLog, int queryStack) {
super(statementLog, queryStack);
this.rs = rs;
}
@Override
public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, params);
}
Object o = method.invoke(rs, params);
if ("next".equals(method.getName())) {
if ((Boolean) o) {
rows++;
if (isTraceEnabled()) {
ResultSetMetaData rsmd = rs.getMetaData();
final int columnCount = rsmd.getColumnCount();
if (first) {
first = false;
printColumnHeaders(rsmd, columnCount);
}
printColumnValues(columnCount);
}
} else {
debug(" Total: " + rows, false);
}
}
clearColumnInfo();
return o;
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
}
注:上文中并没有分析StatementLogger
类,具体的分析方式与上文一致。
详细的代码注释请移步至:http://github.com/tianjindong/mybatis-source-annotation
三. 总结
MyBatis中使用适配器模式对第三方日志框架输出级别进行统一,使用JDK动态代理技术分别加强Connection
、PreparedStatement
、ResultSet
类使之能在自己的声明周期中打印SQL执行的相关信息。
MyBatis日志模块源码分析的更多相关文章
- Fabric2.2中的Raft共识模块源码分析
引言 Hyperledger Fabric是当前比较流行的一种联盟链系统,它隶属于Linux基金会在2015年创建的超级账本项目且是这个项目最重要的一个子项目.目前,与Hyperledger的另外几个 ...
- nginx健康检查模块源码分析
nginx健康检查模块 本文所说的nginx健康检查模块是指nginx_upstream_check_module模块.nginx_upstream_check_module模块是Taobao定制的用 ...
- Spark Scheduler模块源码分析之TaskScheduler和SchedulerBackend
本文是Scheduler模块源码分析的第二篇,第一篇Spark Scheduler模块源码分析之DAGScheduler主要分析了DAGScheduler.本文接下来结合Spark-1.6.0的源码继 ...
- Spark Scheduler模块源码分析之DAGScheduler
本文主要结合Spark-1.6.0的源码,对Spark中任务调度模块的执行过程进行分析.Spark Application在遇到Action操作时才会真正的提交任务并进行计算.这时Spark会根据Ac ...
- gorm的日志模块源码解析
gorm的日志模块源码解析 如何让gorm的日志按照我的格式进行输出 这个问题是<如何为gorm日志加traceId>之后,一个群里的朋友问我的.如何让gorm的sql日志不打印到控制台, ...
- Zepto事件模块源码分析
Zepto事件模块源码分析 一.保存事件数据的handlers 我们知道js原生api中要移除事件,需要传入绑定时的回调函数.而Zepto则可以不传入回调函数,直接移除对应类型的所有事件.原因就在于Z ...
- Django(51)drf渲染模块源码分析
前言 渲染模块的原理和解析模块是一样,drf默认的渲染有2种方式,一种是json格式,另一种是模板方式. 渲染模块源码入口 入口:APIView类中dispatch方法中的:self.response ...
- Springboot中mybatis执行逻辑源码分析
Springboot中mybatis执行逻辑源码分析 在上一篇springboot整合mybatis源码分析已经讲了我们的Mapper接口,userMapper是通过MapperProxy实现的一个动 ...
- MyBatis 之 SqlSessionManager 源码分析
MyBatis 的 4 个基本构成: SqlSessionFactoryBuilder(构造器): 根据配置信息或者代码来生成 SqlSessionFactory(工厂接口) SqlSessionFa ...
- Log4j2异步情况下怎么防止丢日志的源码分析以及队列等待和拒绝策略分析
org.apache.logging.log4j.core.async.AsyncLoggerConfigDisruptor以下所有源码均在此类中首先我们看下log4j2异步队列的初始化 从这里面我们 ...
随机推荐
- GAN的实现和一些问题
GAN的学习是一个二人博弈问题,最终目标是达到纳什平衡.对抗指的是生成网络和判别网络的互相对抗.生成网络尽可能生成逼真样本,判别网络则尽可能去判别该样本是真实样本,还是生成的假样本.示意图如下: 生成 ...
- js es6系列——map函数
正文 map,必要解释就是map不是地图的意思,而是映射的意思. 这里就简单的介绍了这个map了. array.map(callback,[ thisObject]); 看下这个案例后,我们发现了就发 ...
- uniapp中实现简易计算器
uniapp中实现简易计算器主要问题:在计算器的实现过程中会遇到小数点计算精度:此计算器是依赖了uni-popup的弹出层插件,可在uniapp官方组件中查找扩展插件popup弹窗层下载,也可直接点击 ...
- vue登录3D效果
实现的效果 登录动态效果很炫酷,话不多说直接上代码: 组件template <template> <div class="entrance"> <di ...
- HBuilderX 连接网易mumu手机模拟器进行App开发
1.下载安装手机模拟器 常见的安卓手机模拟器: 手机模拟器名称 对应端口号 夜神模拟器 62001 天天模拟器 6555 海马玩模拟器 26944 逍遥模拟器 21503 网易mumu模拟器 7555 ...
- JavaScript中字符串小知识
1. 字符串是不可变的 字符串一旦创建就是不可变的,后续的修改都是新建一个新的字符串而不是在原有的字符串上修改 // 在内存中开辟 可以存放五个字母的空间 str指向该空间 let str = 'fi ...
- 剑指 Offer II 018(Java). 有效的回文(简单)
题目: 给定一个字符串 s ,验证 s 是否是 回文串 ,只考虑字母和数字字符,可以忽略字母的大小写. 本题中,将空字符串定义为有效的 回文串 . 示例 1: 输入: s = "A man, ...
- 力扣596(MySQL)-超过5名学生的课(简单)
题目: 表: Courses 编写一个SQL查询来报告 至少有5个学生 的所有班级. 以 任意顺序 返回结果表. 查询结果格式如下所示 示例1: 解题思路: 使用group by按 班级 进行分组后 ...
- Apsara Stack 同行者专刊 | 政企混合云技术架构的演进和发展
简介: 现在,政企客户已进入到用云计算全面替换传统IT基础架构的攻坚阶段,混合云与传统架构的技术产品能力也正在经历全面的比较与评估.阿里云混合云平台首席架构师张晓丹分享IT架构技术深刻洞察,并对政企混 ...
- KubeVela 1.3 发布:开箱即用的可视化应用交付平台,引入插件生态、权限认证、版本化等企业级新特性
简介:得益于 KubeVela 社区上百位开发者的参与和 30 多位核心贡献者的 500 多次代码提交, KubeVela 1.3 版本正式发布.相较于三个月前发布的 v1.2 版本[1],新版本在 ...