mybaits源码分析--日志模块(四)
一.日志模块
首先日志在我们开发过程中占据了一个非常重要的地位,是开发和运维管理之间的桥梁,在Java中的日志框架也非常多,Log4j,Log4j2,Apache Commons Log,java.util.logging,slf4j等,这些工具对外的接口也都不尽相同,为了统一这些工具,MyBatis定义了一套统一的日志接口供上层使用。如果要看懂首先对于适配器模式要了解下
1.1 Log
Log接口中定义了四种日志级别,相比较其他的日志框架的多种日志级别显得非常的精简,但也能够满足大多数常见的使用了
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); }
1.2 LogFactory
LogFactory工厂类负责创建日志组件适配器,通过这玩意能找到具体的实现
在LogFactory类加载时会执行其静态代码块,其逻辑是按序加载并实例化对应日志组件的适配器,然后使用LogFactory.logConstructor这个静态字段,记录当前使用的第三方日志组件的适配器。具体代码如下
1.3 日志应用
如果想知道在MyBatis系统启动的时候日志框架是如何选择的,那么首先要在全局配置文件中我们可以设置对应的日志类型选择
在Configuration的构造方法中其实是设置的各个日志实现的别名的,其中STDOUT_LOGGING这个也可以在里面找到
然后在解析全局配置文件的时候就会处理日志的设置
进入方法
private void loadCustomLogImpl(Properties props) {
// 获取 logImpl设置的 日志 类型
Class<? extends Log> logImpl = resolveClass(props.getProperty("logImpl"));
// 设置日志
configuration.setLogImpl(logImpl);
}
进入setLogImpl方法中
public void setLogImpl(Class<? extends Log> logImpl) {
if (logImpl != null) {
this.logImpl = logImpl; // 记录日志的类型
// 设置 适配选择
LogFactory.useCustomLogging(this.logImpl);
}
}
再进入useCustomLogging方法
public static synchronized void useCustomLogging(Class<? extends Log> clazz) {
setImplementation(clazz);
}
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 字段
logConstructor = candidate;
} catch (Throwable t) {
throw new LogException("Error setting Log implementation. Cause: " + t, t);
}
}
这就关联上了前面在LogFactory中看到的代码,启动测试方法看到的日志也和源码中的对应上来了,还有就是我们自己设置的会覆盖掉默认的sl4j日志框架的配置
1.4 JDBC 日志
当开启了 STDOUT的日志管理后,当执行SQL操作时会发现在控制台中可以打印出相关的日志信息
那这些日志信息是怎么打印出来的呢?其实在MyBatis中的日志模块中包含了一个jdbc包,它并不是将日志信息通过jdbc操作保存到数据库中,而是通过JDK动态代理的方式,将JDBC操作通过指定的日志框架打印出来。下面就来看看它是如何实现的。
1.4.1 BaseJdbcLogger
BaseJdbcLogger是一个抽象类,它是jdbc包下其他Logger的父类。继承关系如下
从图中也可以看到4个实现都实现了InvocationHandler接口。属性含义如下
// 记录 PreparedStatement 接口中定义的常用的set*() 方法
protected static final Set<String> SET_METHODS;
// 记录了 Statement 接口和 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;// 用于日志输出的Log对象
protected final int queryStack; // 记录了SQL的层数,用于格式化输出SQL
1.4.2 ConnectionLogger
ConnectionLogger的作用是记录数据库连接相关的日志信息,在实现中是创建了一个Connection的代理对象,在每次Connection操作的前后都可以实现日志的操作。
public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler { // 真正的Connection对象
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继承过来的方法,就直接调用 toString,hashCode,equals等
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, params);
}
// 如果调用的是 prepareStatement方法
if ("prepareStatement".equals(method.getName())) {
if (isDebugEnabled()) {
debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
}
// 创建 PreparedStatement
PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
// 然后创建 PreparedStatement 的代理对象 增强
stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
return stmt;
// 同上
} else if ("prepareCall".equals(method.getName())) {
if (isDebugEnabled()) {
debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
}
PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
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);
}
} /**
* Creates a logging version of a connection.
*
* @param conn - the original connection
* @return - the connection with logging
*/
public static Connection newInstance(Connection conn, Log statementLog, int queryStack) {
InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack);
ClassLoader cl = Connection.class.getClassLoader();
// 创建了 Connection的 代理对象 目的是 增强 Connection对象 给他添加了日志功能
return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler);
} /**
* return the wrapped connection.
*
* @return the connection
*/
public Connection getConnection() {
return connection;
} }
其他几个xxxxLogger的实现和ConnectionLogger几乎是一样的就不重复说了,看懂一个就可以看懂所有
1.4.3 应用实现
在实际处理的时候,看下日志模块是如何工作的,在我们要执行SQL语句前需要获取Statement对象,而Statement对象是通过Connection获取的,所以在SimpleExecutor中就可以看到相关的代码
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
// 获取 Statement 对象
stmt = handler.prepare(connection, transaction.getTimeout());
// 为 Statement 设置参数
handler.parameterize(stmt);
return stmt;
}
先进入如到getConnection方法中
protected Connection getConnection(Log statementLog) throws SQLException {
Connection connection = transaction.getConnection();
if (statementLog.isDebugEnabled()) {
// 创建Connection的日志代理对象
return ConnectionLogger.newInstance(connection, statementLog, queryStack);
} else {
return connection;
}
}
再进入到handler.prepare方法中
@Override
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
ErrorContext.instance().sql(boundSql.getSql());
Statement statement = null;
try {
statement = instantiateStatement(connection);
setStatementTimeout(statement, transactionTimeout);
setFetchSize(statement);
return statement;
} catch (SQLException e) {
closeStatement(statement);
throw e;
} catch (Exception e) {
closeStatement(statement);
throw new ExecutorException("Error preparing statement. Cause: " + e, e);
}
}
@Override
protected Statement instantiateStatement(Connection connection) throws SQLException {
String sql = boundSql.getSql();
if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
String[] keyColumnNames = mappedStatement.getKeyColumns();
if (keyColumnNames == null) {
return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
} else {
// 在执行 prepareStatement 方法的时候会进入进入到ConnectionLogger的invoker方法中
return connection.prepareStatement(sql, keyColumnNames);
}
} else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
return connection.prepareStatement(sql);
} else {
return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
}
}
在执行sql语句的时候
如果是查询操作,后面的ResultSet结果集操作,其他是也通过ResultSetLogger来处理的
mybaits源码分析--日志模块(四)的更多相关文章
- mybaits源码分析--缓存模块(六)
一.缓存模块 MyBatis作为一个强大的持久层框架,缓存是其必不可少的功能之一,Mybatis中的缓存分为一级缓存和二级缓存.但本质上是一样的,都是使用Cache接口实现的.缓存位于 org.apa ...
- mybaits源码分析--binding模块(五)
一.binding模块 接下来我们看看在org.apache.ibatis.binding包下提供的Binding模块 ,binding其实在执行sqlSession.getMapper(UserMa ...
- MyBatis 源码篇-日志模块2
上一章的案例,配置日志级别为 debug,执行一个简单的查询操作,会将 JDBC 操作打印出来.本章通过 MyBatis 日志部分源码分析它是如何实现日志打印的. 在 MyBatis 的日志模块中有一 ...
- 手机自动化测试:appium源码分析之bootstrap四
手机自动化测试:appium源码分析之bootstrap四 Orientation是调整屏幕方向的操作 package io.appium.android.bootstrap.handler; i ...
- MyBatis 源码篇-日志模块1
在 Java 开发中常用的日志框架有 Log4j.Log4j2.Apache Common Log.java.util.logging.slf4j 等,这些日志框架对外提供的接口各不相同.本章详细描述 ...
- mybaits源码分析(一)
一.源码下载 1.手动编译源码 为了方便在看源码的过程中能够方便的添加注释,可以从官网下载源码编译生成对应的Jar包,然后上传到本地maven仓库,再引用这个Jar. 首先需要编译打包parent项目 ...
- [Abp vNext 源码分析] - 2. 模块系统的变化
一.简要说明 本篇文章主要分析 Abp vNext 当中的模块系统,从类型构造层面上来看,Abp vNext 当中不再只是单纯的通过 AbpModuleManager 来管理其他的模块,它现在则是 I ...
- Vue.js 源码分析(二十四) 高级应用 自定义指令详解
除了核心功能默认内置的指令 (v-model 和 v-show),Vue 也允许注册自定义指令. 官网介绍的比较抽象,显得很高大上,我个人对自定义指令的理解是:当自定义指令作用在一些DOM元素或组件上 ...
- 精尽Spring Boot源码分析 - 日志系统
该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...
随机推荐
- 二本,拿腾讯,阿里 offer 了
我的春招 Hello,首先自我介绍一下,我是一所普普通通的二本院校的大三学生,坐标江苏. 今年三月份拿到了腾讯实习的offer,人生中第一次面试是腾讯,部门是 TEG 的云架构,并且顺利签约,说实话内 ...
- VUP虚拟直播与光学动作捕捉技术
虚拟直播将虚拟场景.虚拟形象实时显示在观众面前,虚拟场景与人物替代了原有的耗费较大搭建成本的实景场景与真人出镜,为观众带来全新的视觉体验,同时新技术降低了原有场景搭建成本,是近些年继AI.VR.动作捕 ...
- noip模拟27[妹子图·腿·腰](fengwu半仙的妹子们)
\(noip模拟27\;solutions\) 这次吧,我本来以为我能切掉两个题,结果呢??只切掉了一个 不过,隔壁Varuxn也以为能切两个,可惜了,他一个都没切...... 确实他分比我高一点,但 ...
- JavaScript关于对象的一些小知识
javascript的对象 javascriot(之后用js代替)中基本数据类型之一,是复合类型的数据. 对象在js中应用十分广泛,几乎所有的的事物都是做对象,js的对象拥有自己的属性和方法的数据 在 ...
- 第二章 Vant的v-cell的v-for使用
遍历加载list数据,类似el-table 一.问题描述 由于VantUI中没有类似于el-table的table组件,看官网的范例,只找到了cell组件有v-for
- Python3中的List列表的相关操作
列表对象内建函数 1. append(obj) 在列表的末尾添加新元素obj.例: >>> a = ['a', 'b', 'c'] >>> a.append('d' ...
- IP地址,InetAddress类的使用
IP地址 IP地址:InetAddress(没有构造器,通过静态方法返回) java.net包下 唯一定位一台网络上的计算机 127.0.0.1:本机localhost ip地址的分类 IPV4/IP ...
- SpringCloud升级之路2020.0.x版-12.UnderTow 简介与内部原理
本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford 在我们的项目中,我 ...
- 跟我一起写 Makefile(六)
书写命令 ---- 每条规则中的命令和操作系统Shell的命令行是一致的.make会一按顺序一条一条的执行命令,每条命令的开头必须以[Tab]键开头,除非,命令是紧跟在依赖规则后面的分号后的.在命令行 ...
- IP网络地址的计算
IP地址与子网掩码进行"与"运算,得到网络地址