文章目录
- [1. 适配器模式](#1. 适配器模式)
- [2. Log](#2. Log)
-
- [2.1 默认实现StdOutImpl](#2.1 默认实现StdOutImpl)
- [2.2 Log4jImpl](#2.2 Log4jImpl)
- [3. LogFactory](#3. LogFactory)
- [4. 解析配置和应用](#4. 解析配置和应用)
-
- [4.1 settings配置](#4.1 settings配置)
- [4.2 解析](#4.2 解析)
- [5. jdbc日志](#5. jdbc日志)
-
- [5. 1 类图](#5. 1 类图)
- [5.2 BaseJdbcLogger](#5.2 BaseJdbcLogger)
- [5.3 ConnectionLogger](#5.3 ConnectionLogger)
- [5.4 ConnectionLogger的具体应用](#5.4 ConnectionLogger的具体应用)
1. 适配器模式
适配器使接口不兼容的对象可以相互合作。
Java的日志框架有很多,Log4j,Log4j2,Apache Commons Log,java.util.logging,slf4j等,接口不尽相同,Mybatis为了统一匹配这些框架,使用到了适配器模式。
2. Log
Mybatis自定义了日志接口:org.apache.ibatis.logging.Log。
java
/**
* @author Clinton Begin
*/
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);
}
以下为其类图:
上图的实现相当于adapter。
2.1 默认实现StdOutImpl
java
public class StdOutImpl implements Log {
public StdOutImpl(String clazz) {
// Do Nothing
}
@Override
public boolean isDebugEnabled() {
return true;
}
@Override
public boolean isTraceEnabled() {
return true;
}
@Override
public void error(String s, Throwable e) {
System.err.println(s);
e.printStackTrace(System.err);
}
@Override
public void error(String s) {
System.err.println(s);
}
@Override
public void debug(String s) {
System.out.println(s);
}
@Override
public void trace(String s) {
System.out.println(s);
}
@Override
public void warn(String s) {
System.out.println(s);
}
}
这个实现很简单,就是控制台把日志打印出来。
2.2 Log4jImpl
为了匹配Log4j,该类持有org.apache.log4j.Logger,Logger就是上图adaptee,被匹配的接口。
java
/**
* @author Eduardo Macarron
*/
public class Log4jImpl implements Log {
private static final String FQCN = Log4jImpl.class.getName();
private final Logger log;
public Log4jImpl(String clazz) {
log = Logger.getLogger(clazz);
}
@Override
public boolean isDebugEnabled() {
return log.isDebugEnabled();
}
@Override
public boolean isTraceEnabled() {
return log.isTraceEnabled();
}
@Override
public void error(String s, Throwable e) {
log.log(FQCN, Level.ERROR, s, e);
}
@Override
public void error(String s) {
log.log(FQCN, Level.ERROR, s, null);
}
@Override
public void debug(String s) {
log.log(FQCN, Level.DEBUG, s, null);
}
@Override
public void trace(String s) {
log.log(FQCN, Level.TRACE, s, null);
}
@Override
public void warn(String s) {
log.log(FQCN, Level.WARN, s, null);
}
}
3. LogFactory
LogFactory使用工厂模式,创建各类适配器。 该类用final修饰,不可继承,同时构造方法是私有的,不能通过new方法创建,像是一个工具类。
java
/**
* @author Clinton Begin
* @author Eduardo Macarron
*/
public final class LogFactory {
/**
* Marker to be used by logging implementations that support markers.
*/
public static final String MARKER = "MYBATIS";
private static Constructor<? extends Log> logConstructor;
static {
// 按序加载对应的日志组件,从上往下加载,上面的成功了,下面的就不会在加载了
/**
* tryImplementation(LogFactory::useSlf4jLogging); 等价于
* tryImplementation(new Runnable(){
* void run(){
* useSlf4jLogging();
* }
* })
*/
tryImplementation(LogFactory::useSlf4jLogging);
tryImplementation(LogFactory::useCommonsLogging);
tryImplementation(LogFactory::useLog4J2Logging);
tryImplementation(LogFactory::useLog4JLogging);
tryImplementation(LogFactory::useJdkLogging);
tryImplementation(LogFactory::useNoLogging);
}
private LogFactory() {
// disable construction
}
public static Log getLog(Class<?> aClass) {
return getLog(aClass.getName());
}
public static Log getLog(String logger) {
try {
return logConstructor.newInstance(logger);
} catch (Throwable t) {
throw new LogException("Error creating logger for logger " + logger + ". Cause: " + t, t);
}
}
public static synchronized void useCustomLogging(Class<? extends Log> clazz) {
setImplementation(clazz);
}
public static synchronized void useSlf4jLogging() {
setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
}
public static synchronized void useCommonsLogging() {
setImplementation(org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl.class);
}
public static synchronized void useLog4JLogging() {
setImplementation(org.apache.ibatis.logging.log4j.Log4jImpl.class);
}
public static synchronized void useLog4J2Logging() {
setImplementation(org.apache.ibatis.logging.log4j2.Log4j2Impl.class);
}
public static synchronized void useJdkLogging() {
setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class);
}
public static synchronized void useStdOutLogging() {
setImplementation(org.apache.ibatis.logging.stdout.StdOutImpl.class);
}
public static synchronized void useNoLogging() {
setImplementation(org.apache.ibatis.logging.nologging.NoLoggingImpl.class);
}
private static void tryImplementation(Runnable runnable) {
if (logConstructor == null) {
try {
runnable.run();
} catch (Throwable t) {
// ignore
}
}
}
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);
}
}
}
4. 解析配置和应用
问题:mybatis是如何选择日志框架的。
4.1 settings配置
配置值包括:SLF4J , LOG4J(deprecated since 3.5.9) , LOG4J2 , JDK_LOGGING |,COMMONS_LOGGING , STDOUT_LOGGING , NO_LOGGING。
配置的值是如何来的?
在配置对象Configuration的构造方法里面。
4.2 解析
XMLConfigBuilder的方法:loadCustomLogImpl().
java
private void loadCustomLogImpl(Properties props) {
// 获取 logImpl设置的 日志 类型
Class<? extends Log> logImpl = resolveClass(props.getProperty("logImpl"));
// 设置日志 这块代码是我们后面分析 日志 模块的 关键代码
configuration.setLogImpl(logImpl);
}
继续:
java
public void setLogImpl(Class<? extends Log> logImpl) {
if (logImpl != null) {
this.logImpl = logImpl; // 记录日志的类型
// 设置 适配选择
LogFactory.useCustomLogging(this.logImpl);
}
}
继续:
java
public static synchronized void useCustomLogging(Class<? extends Log> clazz) {
setImplementation(clazz);
}
继续:
java
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);
}
}
5. jdbc日志
有了以上的类,那mybatis是如何打印日志的?
Mybatis通过JDK动态代理的方式,将JDBC操作通过指定的日志框架打印出来。
5. 1 类图
5.2 BaseJdbcLogger
BaseJdbcLogger是一个基础的抽象类。基本的属性:
java
// 记录 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
5.3 ConnectionLogger
java
/**
* Connection proxy to add logging.
*
* @author Clinton Begin
* @author Eduardo Macarron
*
*/
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;
}
/**
* Connection 是一个数据库连接对象
* 通过 Connection 的下一步是创建 Statement 对象
* Statement包含对应的子类 PreparedStatement
*
* 日志记录 Connection 创建 Statement 的过程
* 同时会创建 Statement 的代理对象类增强 Statement
*
* @param proxy
* @param method
* @param params
* @return
* @throws Throwable
*/
@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;
}
}
5.4 ConnectionLogger的具体应用
在实际处理的时候,日志模块是如何工作的?
- 执行sql之前先要获取Statement.
SimpleExecutor的方法doQuery
java
/**
* 到了 具体的数据库操作的步骤了 JDBC
* Connection
* Statement
* PreparedStatement
* @param ms
* @param parameter
* @param rowBounds
* @param resultHandler
* @param boundSql
* @param <E>
* @return
* @throws SQLException
*/
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
// 注意,已经来到SQL处理的关键对象 StatementHandler >> 同时会完成 parameterHandler和resultSetHandler的实例化
// 默认创建的是 PreparedStatementHandler
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 获取一个 Statement对象 对占位符处理
stmt = prepareStatement(handler, ms.getStatementLog());
// 执行查询
return handler.query(stmt, resultHandler);
} finally {
// 用完就关闭
closeStatement(stmt);
}
}
- 继续看prepareStatement
java
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
// 放开了日志 会创建 Connection 的代理对象
Connection connection = getConnection(statementLog);
// 获取 Statement 对象 ==》 PreparedStatement对象 Statement 如果放开了日志 则会创建 Statement 对应的代理对象
stmt = handler.prepare(connection, transaction.getTimeout());
// 为 Statement 设置参数 如果是PreparedStatement处理则会对 对应的占位符赋值
handler.parameterize(stmt);
return stmt;
}
- 继续看getConnection方法,
java
protected Connection getConnection(Log statementLog) throws SQLException {
// 获取到了真正的 Connection 对象 ? 如果有连接池管理 在此处获取的是PooledConnection 是Connection的代理对象
Connection connection = transaction.getConnection();
if (statementLog.isDebugEnabled()) {
// 创建Connection的日志jdk代理对象
return ConnectionLogger.newInstance(connection, statementLog, queryStack);
} else {
// 返回的是真正的Connection 没有走代理的方式
return connection;
}
}
- 再看 handler.prepare(connection, transaction.getTimeout()), 进入到instantiateStatement方法
java
@Override
protected Statement instantiateStatement(Connection connection) throws SQLException {
String sql = boundSql.getSql();
if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
String[] keyColumnNames = mappedStatement.getKeyColumns();
if (keyColumnNames == null) {
// connection 是 日志的代理对象
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);
}
}
- 执行查询handler.query(stmt, resultHandler);