文章目录
- 1. 适配器模式
- 2. Log
- 2.1 默认实现StdOutImpl
- 2.2 Log4jImpl
- 3. LogFactory
- 4. 解析配置和应用
- 4.1 settings配置
- 4.2 解析
- 5. jdbc日志
- 5. 1 类图
- 5.2 BaseJdbcLogger
- 5.3 ConnectionLogger
- 5.4 ConnectionLogger的具体应用
1. 适配器模式
适配器使接口不兼容的对象可以相互合作。
Java的日志框架有很多,Log4j,Log4j2,Apache Commons Log,java.util.logging,slf4j等,接口不尽相同,Mybatis为了统一匹配这些框架,使用到了适配器模式。
2. Log
Mybatis自定义了日志接口:org.apache.ibatis.logging.Log。
/*** @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
public class StdOutImpl implements Log {public StdOutImpl(String clazz) {// Do Nothing}@Overridepublic boolean isDebugEnabled() {return true;}@Overridepublic boolean isTraceEnabled() {return true;}@Overridepublic void error(String s, Throwable e) {System.err.println(s);e.printStackTrace(System.err);}@Overridepublic void error(String s) {System.err.println(s);}@Overridepublic void debug(String s) {System.out.println(s);}@Overridepublic void trace(String s) {System.out.println(s);}@Overridepublic void warn(String s) {System.out.println(s);}
}
这个实现很简单,就是控制台把日志打印出来。
2.2 Log4jImpl
为了匹配Log4j,该类持有org.apache.log4j.Logger,Logger就是上图adaptee,被匹配的接口。
/*** @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);}@Overridepublic boolean isDebugEnabled() {return log.isDebugEnabled();}@Overridepublic boolean isTraceEnabled() {return log.isTraceEnabled();}@Overridepublic void error(String s, Throwable e) {log.log(FQCN, Level.ERROR, s, e);}@Overridepublic void error(String s) {log.log(FQCN, Level.ERROR, s, null);}@Overridepublic void debug(String s) {log.log(FQCN, Level.DEBUG, s, null);}@Overridepublic void trace(String s) {log.log(FQCN, Level.TRACE, s, null);}@Overridepublic void warn(String s) {log.log(FQCN, Level.WARN, s, null);}}
3. LogFactory
LogFactory使用工厂模式,创建各类适配器。 该类用final修饰,不可继承,同时构造方法是私有的,不能通过new方法创建,像是一个工具类。
/*** @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().
private void loadCustomLogImpl(Properties props) {// 获取 logImpl设置的 日志 类型Class<? extends Log> logImpl = resolveClass(props.getProperty("logImpl"));// 设置日志 这块代码是我们后面分析 日志 模块的 关键代码configuration.setLogImpl(logImpl);}
继续:
public void setLogImpl(Class<? extends Log> logImpl) {if (logImpl != null) {this.logImpl = logImpl; // 记录日志的类型// 设置 适配选择LogFactory.useCustomLogging(this.logImpl);}}
继续:
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);}}
5. jdbc日志
有了以上的类,那mybatis是如何打印日志的?
Mybatis通过JDK动态代理的方式,将JDBC操作通过指定的日志框架打印出来。
5. 1 类图
5.2 BaseJdbcLogger
BaseJdbcLogger是一个基础的抽象类。基本的属性:
// 记录 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*() 方法设置的键 keyprivate final List<Object> columnNames = new ArrayList<>();// 记录了PreparedStatement.set*() 方法设置的值 Valueprivate final List<Object> columnValues = new ArrayList<>();protected final Log statementLog;// 用于日志输出的Log对象protected final int queryStack; // 记录了SQL的层数,用于格式化输出SQL
5.3 ConnectionLogger
/*** 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*/@Overridepublic 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);}// 创建 PreparedStatementPreparedStatement 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
/*** 到了 具体的数据库操作的步骤了 JDBC* Connection* Statement* PreparedStatement* @param ms* @param parameter* @param rowBounds* @param resultHandler* @param boundSql* @param <E>* @return* @throws SQLException*/@Overridepublic <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的实例化// 默认创建的是 PreparedStatementHandlerStatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);// 获取一个 Statement对象 对占位符处理stmt = prepareStatement(handler, ms.getStatementLog());// 执行查询return handler.query(stmt, resultHandler);} finally {// 用完就关闭closeStatement(stmt);}}
- 继续看prepareStatement
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方法,
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方法
@Overrideprotected 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);