介绍
mybatis没有提供日志的实现类,需要接入第三方的日志组件
从LogFactory类的静态代码块中可以看到第三方日志加载优先级如下 slf4j > apache commons log > log4j2 > log4j > jul
static {
tryImplementation(new Runnable() {
@Override
public void run() {
useSlf4jLogging();
}
});
tryImplementation(new Runnable() {
@Override
public void run() {
useCommonsLogging();
}
});
tryImplementation(new Runnable() {
@Override
public void run() {
useLog4J2Logging();
}
});
// 省略了其他模块的加载逻辑
}
从LogFactory类名可以看出用到了工厂模式,对工厂模式不了解的可以看如下文章
但是第三方日志都有各自的log级别,mybatis用适配器模式统一提供了trace,debug,warn,error四个级别,
org.apache.ibatis.logging.Log接口有多个实现类,实现类即Mybatis提供的适配器
,例如Log4jImpl,Log4j2Impl等,一种实现类提供一个适配器,UML类图如下
上图中Logger对象是org.apache.log4j.Logger,即通过Log4jImpl的适配将对Log接口的操作转化为Logger对象的操作
适配器模式设计的几个角色如下
- 目标接口(Target):调用者能够直接使用的接口,即Log接口
- 需要适配的类(Adaptee):Adaptee类中有真正的逻辑,但是不能被调用者直接使用,即Logger对象
- 适配器(Adapter):实现了Target接口,包装了Adaptee对象
当然我们在项目中一般也不配置Mybatis适配后的Logger对象,因为级别实在是太少了,INFO级别都没有
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
public static final Log log = LogFactory.getLog(Test.class);
直接配置slf4j的即可
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public static final Logger logger = LoggerFactory.getLogger(Test.class);
之所有要适配,是因为JDBC模块也会打印日志,但是它没有和特定的日志框架绑定,用的是Mybatis适配后的Log对象
JDBC调试,打印不同类型的日志
在mybatis源码logging模块下有一个jdbc包,当日志级别设置为DEBUG级别时它通过动态代理的方式输出很多实用信息,如输出SQL语句,用户传入的绑定参数,SQL语句影响的行数等信息
你可能会想,为什么要通过动态代理来打印debug级别的日志呢?用log.debug()不就行了,主要还是为了避免日志逻辑和正常逻辑耦合到一块
BaseJdbcLogger是一个抽象类,它是jdbc包下其他Logger类的父类,继承关系如下图
ConnectionLogger:负责打印连接信息和SQL语句,并创建PreparedStatementLogger
PreparedStatementLogger:负责打印参数信息,并创建ResultSetLogger
StatementLogger:负责打印参数信息,并创建ResultSetLogger
ResultSetLogger:负责打印数据结果信息
4个类实现的思路是一样的,这里只分析一下ConnectionLogger
当日志级别为debug时,返回的是被代理后的Connection对象,否则就是正常的Connection对象
protected Connection getConnection(Log statementLog) throws SQLException {
Connection connection = transaction.getConnection();
if (statementLog.isDebugEnabled()) {
return ConnectionLogger.newInstance(connection, statementLog, queryStack);
} else {
return connection;
}
}
ConnectionLogger对象实现了InvocationHandler接口,返回了代理后的Connection对象
public static Connection newInstance(Connection conn, Log statementLog, int queryStack) {
InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack);
ClassLoader cl = Connection.class.getClassLoader();
return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler);
}
具体的拦截逻辑如下
@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,createStatement的方法,打印要执行的SQL语句
// 并返回PreparedStatement的代理对象,让PreparedStatement也具备日志能力,打印参数
if ("prepareStatement".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 ("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);
}
}
可以看到对3个方法进行了增强prepareStatement,prepareCall,createStatement
如果是调用prepareStatement,prepareCall的方法,打印要执行的SQL语句
3个方法都会返回PreparedStatement的代理对象,让PreparedStatement也具备日志能力,打印参数
看看最后打印出的DEBUG日志
DEBUG 2020-02-24 18:09:13,647 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==> Preparing: select id, `name`, phone from author
DEBUG 2020-02-24 18:09:13,766 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: ==> Parameters:
DEBUG 2020-02-24 18:09:13,813 org.apache.ibatis.logging.jdbc.BaseJdbcLogger: <== Total: 1
是不是看着很熟悉,就是用动态代理实现的