MyBatis设计思想(2)——日志模块
一. 痛点分析
- 作为一个成熟的中间件,日志功能是必不可少的。那么,MyBatis是要自己实现日志功能,还是集成第三方的日志呢?MyBatis选择了集成第三方日志框架。
- 第三方的日志框架种类繁多,且级别定义、实现方式都不一样,每个使用MyBatis的业务都可能采用不同的日志组件,那MyBatis如何进行兼容?如果业务方引入了多个日志框架,MyBatis按照什么优先级进行选择?
- MyBatis的核心流程,包括SQL拼接、SQL执行、结果集映射等关键步骤,都是需要打印日志的,那在核心流程中显式log.info(“xxx”)有点不太合适,如何将日志打印优雅地嵌入到核心流程中?
二. 适配器模式
适配器模式的作用:将一个接口转换成满足客户端期望的另一个接口,使得接口不兼容的那些类可以一起工作。
角色:
- Target:目标接口,定义了客户端所需要的接口。
- Adaptee:被适配者,它自身有满足客户端需求的功能,但是接口定义与Target并不兼容,需要进行适配。
- Adapter:适配器,对Adaptee进行适配,使其满足Target的定义,供客户端使用。
三. MyBatis集成第三方日志框架
-
MyBatis定义了Log接口,并给出了debug、trace、error、warn四种日志级别:
/** * @author Clinton Begin * * 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为大部分主流的日志框架,都实现了Adapter。以Log4j为例:
/** * @author Eduardo Macarron * * MyBatis为Log4j实现的Adapter */ public class Log4jImpl implements Log { private static final String FQCN = Log4jImpl.class.getName(); //内部维护log4j的Logger实例 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); } } -
日志模块实现了适配器模式
-
Log = Target
-
Logger(log4j) = Adaptee
-
Log4jImpl = Adapter
-
日志实现类的选择
/** * @author Clinton Begin * @author Eduardo Macarron * * 日志工厂,通过getLog()方法获取日志实现类 */ 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 { //按照顺序,依次尝试加载Log实现类 //优先级为:slf4j -> commons-logging -> log4j2 -> log4j -> jdk-logging -> no-logging 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<?> clazz) { return getLog(clazz.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 = candidate; } catch (Throwable t) { throw new LogException("Error setting Log implementation. Cause: " + t, t); } } } -
这里还有一个点,NoLoggingImpl是一种Null Object Pattern(空对象模式),也实现了目标接口,内部就是Do Nothing,这样客户端可以减少很多判空操作。
/** * @author Clinton Begin * * 空Log实现, Null Object Pattern */ public class NoLoggingImpl implements Log { public NoLoggingImpl(String clazz) { // Do Nothing } @Override public boolean isDebugEnabled() { return false; } @Override public boolean isTraceEnabled() { return false; } @Override public void error(String s, Throwable e) { // Do Nothing } @Override public void error(String s) { // Do Nothing } @Override public void debug(String s) { // Do Nothing } @Override public void trace(String s) { // Do Nothing } @Override public void warn(String s) { // Do Nothing } }
四. 优雅地打印日志
- 代理模式:给某一个对象提供一个代理,并由代理对象控制对原对象的访问引用。代理对象可以在原对象的基础上,进行一些功能上的增强,而这些增强对客户端来说是无感知的。
-
MyBatis内部需要打印日志的地方
- 创建PrepareStatement时,打印待执行的 SQL 语句。
- 访问数据库时,打印参数的类型和值。
- 查询出结果集后,打印结果数据条数。
-
MyBatis的日志增强器
-
BaseJdbcLogger:所有日志增强器的抽象父类,用于记录JDBC那些需要增强的方法,并保存运行期间的SQL参数信息。
/** * Base class for proxies to do logging. * * @author Clinton Begin * @author Eduardo Macarron * * 所有日志增强器的抽象父类,用于记录JDBC那些需要增强的方法,并保存运行期间的SQL参数信息 */ public abstract class BaseJdbcLogger { protected static final Set<String> SET_METHODS; protected static final Set<String> EXECUTE_METHODS = new HashSet<>(); private final Map<Object, Object> columnMap = new HashMap<>(); private final List<Object> columnNames = new ArrayList<>(); private final List<Object> columnValues = new ArrayList<>(); protected final Log statementLog; protected final int queryStack; /* * Default constructor */ public BaseJdbcLogger(Log log, int queryStack) { this.statementLog = log; if (queryStack == 0) { this.queryStack = 1; } else { this.queryStack = queryStack; } } static { //记录PreparedStatement中的setXXX()方法 SET_METHODS = Arrays.stream(PreparedStatement.class.getDeclaredMethods()) .filter(method -> method.getName().startsWith("set")) .filter(method -> method.getParameterCount() > 1) .map(Method::getName) .collect(Collectors.toSet()); //记录executeXXX()方法 EXECUTE_METHODS.add("execute"); EXECUTE_METHODS.add("executeUpdate"); EXECUTE_METHODS.add("executeQuery"); EXECUTE_METHODS.add("addBatch"); } protected void setColumn(Object key, Object value) { columnMap.put(key, value); columnNames.add(key); columnValues.add(value); } protected Object getColumn(Object key) { return columnMap.get(key); } protected String getParameterValueString() { List<Object> typeList = new ArrayList<>(columnValues.size()); for (Object value : columnValues) { if (value == null) { typeList.add("null"); } else { typeList.add(objectValueString(value) + "(" + value.getClass().getSimpleName() + ")"); } } final String parameters = typeList.toString(); return parameters.substring(1, parameters.length() - 1); } protected String objectValueString(Object value) { if (value instanceof Array) { try { return ArrayUtil.toString(((Array) value).getArray()); } catch (SQLException e) { return value.toString(); } } return value.toString(); } protected String getColumnString() { return columnNames.toString(); } protected void clearColumnInfo() { columnMap.clear(); columnNames.clear(); columnValues.clear(); } protected String removeExtraWhitespace(String original) { return SqlSourceBuilder.removeExtraWhitespaces(original); } protected boolean isDebugEnabled() { return statementLog.isDebugEnabled(); } protected boolean isTraceEnabled() { return statementLog.isTraceEnabled(); } protected void debug(String text, boolean input) { if (statementLog.isDebugEnabled()) { statementLog.debug(prefix(input) + text); } } protected void trace(String text, boolean input) { if (statementLog.isTraceEnabled()) { statementLog.trace(prefix(input) + text); } } private String prefix(boolean isInput) { char[] buffer = new char[queryStack * 2 + 2]; Arrays.fill(buffer, '='); buffer[queryStack * 2 + 1] = ' '; if (isInput) { buffer[queryStack * 2] = '>'; } else { buffer[0] = '<'; } return new String(buffer); } } -
ConnectionLogger:数据库连接的日志增强器,打印PreparedStatement信息,并通过动态代理方式,创建具有打印日志功能的PreparedStatement、Statement等。
/** * Connection proxy to add logging. * * @author Clinton Begin * @author Eduardo Macarron * * 数据库连接的日志增强器,打印PreparedStatement信息,并通过动态代理方式,创建具有打印日志功能的PreparedStatement、Statement等 */ 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 { //1. 对于Object中定义的方法,不进行拦截 if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, params); } //2. 拦截prepareStatement()、prepareCall()方法,打印SQL信息,并返回PreparedStatementLogger增强器 if ("prepareStatement".equals(method.getName()) || "prepareCall".equals(method.getName())) { if (isDebugEnabled()) { debug(" Preparing: " + removeExtraWhitespace((String) params[0]), true); } PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params); stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack); return stmt; } //3. 拦截createStatement()方法,返回StatementLogger增强器 else if ("createStatement".equals(method.getName())) { Statement stmt = (Statement) method.invoke(connection, params); stmt = StatementLogger.newInstance(stmt, statementLog, queryStack); return stmt; } //4. 对于普通的Connection中的方法,直接调用 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 * @param statementLog * the statement log * @param queryStack * the query stack * @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(); return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler); } /** * return the wrapped connection. * * @return the connection */ public Connection getConnection() { return connection; } } -
PreparedStatementLogger:PreparedStatement日志增强器,主要功能包括
- 打印PreparedStatement中的动态参数信息。
- 拦截setXXX()方法,记录封装的参数。
- 创建ResultSetLogger增强器,使得对于结果集的操作具备日志打印的功能。
/** * PreparedStatement proxy to add logging. * * @author Clinton Begin * @author Eduardo Macarron * * PreparedStatement日志增强器,主要功能包括: * 1. 打印PreparedStatement中的动态参数信息 * 2. 拦截setXXX()方法,记录封装的参数 * 3. 创建ResultSetLogger增强器,使得对于结果集的操作具备日志打印的功能 */ public final class PreparedStatementLogger extends BaseJdbcLogger implements InvocationHandler { //内部维护PreparedStatement对象 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 { //1. Object中定义的方法不拦截 if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, params); } //2. 拦截executeXXX()方法,打印参数信息 if (EXECUTE_METHODS.contains(method.getName())) { if (isDebugEnabled()) { debug("Parameters: " + getParameterValueString(), true); } clearColumnInfo(); if ("executeQuery".equals(method.getName())) { ResultSet rs = (ResultSet) method.invoke(statement, params); return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack); } else { return method.invoke(statement, params); } } //3. 拦截setXXX()方法,记录动态参数 else if (SET_METHODS.contains(method.getName())) { if ("setNull".equals(method.getName())) { setColumn(params[0], null); } else { setColumn(params[0], params[1]); } return method.invoke(statement, params); } //4. 拦截getResultSet()方法,返回ResultSetLogger增强器 else if ("getResultSet".equals(method.getName())) { ResultSet rs = (ResultSet) method.invoke(statement, params); return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack); } //5. 拦截getUpdateCount()方法,打印update操作影响的记录行数 else if ("getUpdateCount".equals(method.getName())) { int updateCount = (Integer) method.invoke(statement, params); if (updateCount != -1) { debug(" Updates: " + updateCount, false); } return updateCount; } //6. 普通方法,直接调用PreparedStatement else { return method.invoke(statement, params); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } } /** * Creates a logging version of a PreparedStatement. * * @param stmt - the statement * @param statementLog - the statement log * @param queryStack - the query stack * @return - the proxy */ public static PreparedStatement newInstance(PreparedStatement stmt, Log statementLog, int queryStack) { InvocationHandler handler = new PreparedStatementLogger(stmt, statementLog, queryStack); ClassLoader cl = PreparedStatement.class.getClassLoader(); return (PreparedStatement) Proxy.newProxyInstance(cl, new Class[]{PreparedStatement.class, CallableStatement.class}, handler); } /** * Return the wrapped prepared statement. * * @return the PreparedStatement */ public PreparedStatement getPreparedStatement() { return statement; } } -
ResultSetLogger:结果集日志增强器,主要用于打印结果集的总记录数。
/** * ResultSet proxy to add logging. * * @author Clinton Begin * @author Eduardo Macarron * * 结果集日志增强器,主要用于打印结果集的总记录数 */ 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 { //1. Object中定义的方法不拦截 if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, params); } //2. 拦截next()方法,记录总记录数,并打印 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); } } private void printColumnHeaders(ResultSetMetaData rsmd, int columnCount) throws SQLException { StringJoiner row = new StringJoiner(", ", " Columns: ", ""); for (int i = 1; i <= columnCount; i++) { if (BLOB_TYPES.contains(rsmd.getColumnType(i))) { blobColumns.add(i); } row.add(rsmd.getColumnLabel(i)); } trace(row.toString(), false); } private void printColumnValues(int columnCount) { StringJoiner row = new StringJoiner(", ", " Row: ", ""); for (int i = 1; i <= columnCount; i++) { try { if (blobColumns.contains(i)) { row.add("<<BLOB>>"); } else { row.add(rs.getString(i)); } } catch (SQLException e) { // generally can't call getString() on a BLOB column row.add("<<Cannot Display>>"); } } trace(row.toString(), false); } /** * Creates a logging version of a ResultSet. * * @param rs * the ResultSet to proxy * @param statementLog * the statement log * @param queryStack * the query stack * @return the ResultSet with logging */ public static ResultSet newInstance(ResultSet rs, Log statementLog, int queryStack) { InvocationHandler handler = new ResultSetLogger(rs, statementLog, queryStack); ClassLoader cl = ResultSet.class.getClassLoader(); return (ResultSet) Proxy.newProxyInstance(cl, new Class[]{ResultSet.class}, handler); } /** * Get the wrapped result set. * * @return the resultSet */ public ResultSet getRs() { return rs; } } -
日志功能的优雅嵌入:MyBatis有个核心的组件Executor,主要的处理逻辑都是在Executor中实现的,日志的打印也是在这里,具体可见org.apache.ibatis.executor.BaseExecutor#getConnection()方法:
//获取数据库连接 protected Connection getConnection(Log statementLog) throws SQLException { //1. 通过事务获取JDBC Connection Connection connection = transaction.getConnection(); //2. 如果开启了日志,则返回ConnectionLogger增强器 if (statementLog.isDebugEnabled()) { return ConnectionLogger.newInstance(connection, statementLog, queryStack); } else { return connection; } }这里获取了ConnectionLogger后,后续的PreparedStatement、ResultSet也就会具备日志打印的功能了。