MYBATIS日志模块解析 前言 mybatis 没有提供日志的实现类,需要接入第三方的日志组件,但第三方日志组件都有各自的Log级别,且各不相同。而Mybatis统一提供了trace/ debug/ warn/ error四个级别,mybatis使用适配器模式进行日志加载,我们来欣赏下mybatis源码的魅力。
日志接口Log
mybatis 封装了统一的日志接口,其他日志接口接入需要实现该日志接口。
该接口只提供了trace/ debug/ warn/ error四个级别的日志输出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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) ; }
日志工厂LogFactory
在这里定义了日志框架的加载顺序
slf4j -> commonsLoging -> Log4J2 -> Log4J -> JdkLog
使得日志框架优雅的嵌入到mybatis中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 public final class LogFactory { public static final String MARKER = "MYBATIS" ; private static Constructor<? extends Log > logConstructor; static { tryImplementation(LogFactory::useSlf4jLogging); tryImplementation(LogFactory::useCommonsLogging); tryImplementation(LogFactory::useLog4J2Logging); tryImplementation(LogFactory::useLog4JLogging); tryImplementation(LogFactory::useJdkLogging); tryImplementation(LogFactory::useNoLogging); } private LogFactory () { } 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) { } } } 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); } } }
到这里是整个日志的加载顺序,尝试找到一个可用的构造方法,找到后返回该日志框架的实例。
这里面用到了Runable接口的钩子方法,也可以叫做接口回调,并没有使用多线程编程。
日志系统转换
这里采用了很多日志框架,使用了适配器模式进行日志的转换,装饰着模式可以查看我的设计模式一节
这里我们就拿比较负责的sl4j来查看源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 public class Slf4jImpl implements Log { private Log log; public Slf4jImpl (String clazz) { Logger logger = LoggerFactory.getLogger(clazz); if (logger instanceof LocationAwareLogger) { try { logger.getClass().getMethod("log" , Marker.class, String.class, int .class, String.class, Object[].class, Throwable.class); log = new Slf4jLocationAwareLoggerImpl ((LocationAwareLogger) logger); return ; } catch (SecurityException | NoSuchMethodException e) { } } log = new Slf4jLoggerImpl (logger); } @Override public boolean isDebugEnabled () { return log.isDebugEnabled(); } @Override public boolean isTraceEnabled () { return log.isTraceEnabled(); } @Override public void error (String s, Throwable e) { log.error(s, e); } @Override public void error (String s) { log.error(s); } @Override public void debug (String s) { log.debug(s); } @Override public void trace (String s) { log.trace(s); } @Override public void warn (String s) { log.warn(s); } }
这里注意下Slf4j版本控制,如果Slf4j版本>=1.6 使用 Slf4jLocationAwareLoggerImpl 否则使用Slf4jLoggerImpl
我们再拿Slf4jLoggerImpl看一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 class Slf4jLoggerImpl implements Log { private final Logger log; public Slf4jLoggerImpl (Logger logger) { log = logger; } @Override public boolean isDebugEnabled () { return log.isDebugEnabled(); } @Override public boolean isTraceEnabled () { return log.isTraceEnabled(); } @Override public void error (String s, Throwable e) { log.error(s, e); } @Override public void error (String s) { log.error(s); } @Override public void debug (String s) { log.debug(s); } @Override public void trace (String s) { log.trace(s); } @Override public void warn (String s) { log.warn(s); } }
到这里就是mybatis日志框架的优雅封装实现,通过依次尝试加载,使用适配器模式进行日志接口的统一
JDBC日志增强 JDBC模板类BaseJdbcLogger
BaseJdbcLogger初始化了日志框架Log,SET_METHODS和jdbc执行方法列表EXECUTE_METHODS
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 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; public BaseJdbcLogger (Log log, int queryStack) { this .statementLog = log; if (queryStack == 0 ) { this .queryStack = 1 ; } else { this .queryStack = queryStack; } } static { SET_METHODS = Arrays.stream(PreparedStatement.class.getDeclaredMethods()) .filter(method -> method.getName().startsWith("set" )) .filter(method -> method.getParameterCount() > 1 ) .map(Method::getName) .collect(Collectors.toSet()); 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 removeBreakingWhitespace (String original) { StringTokenizer whitespaceStripper = new StringTokenizer (original); StringBuilder builder = new StringBuilder (); while (whitespaceStripper.hasMoreTokens()) { builder.append(whitespaceStripper.nextToken()); builder.append(" " ); } return builder.toString(); } 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); } }
Connection日志增强
Connection日志增加是基于动态代理实现的,具有很高的参考价值,他创建预处理等对象的时候并返回预处理对象的代理方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 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 { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this , params); } 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); } } 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); } public Connection getConnection () { return connection; } }
PreparedStatementLogger预处理日志增强
预处理日志增强,对其中一些方法进行增强,对于查询以及获取结果集,返回结果集的日志动态代理增强。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 public final class PreparedStatementLogger extends BaseJdbcLogger implements InvocationHandler { 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 { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this , params); } 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); } } 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); } else if ("getResultSet" .equals(method.getName())) { ResultSet rs = (ResultSet) method.invoke(statement, params); return rs = = null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack); } else if ("getUpdateCount" .equals(method.getName())) { int updateCount = (Integer) method.invoke(statement, params); if (updateCount != -1 ) { debug(" Updates: " + updateCount, false ); } return updateCount; } else { return method.invoke(statement, params); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } } 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); } public PreparedStatement getPreparedStatement () { return statement; } }
StatementLogger日志增加和PreparedStatementLogger这个类基本上一样,大家可以看一下。
ResultSetLogger结果集日志增强
结果集打印没有什么可说的了,基本上就是判断是否是blob类型,特殊处理,其他情况进行字符串拼接
这里使用了JDK1.8的StringJoiner 进行字符串拼接,可以简单的方式进行包含有分隔符的字符串拼接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 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 { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this , params); } 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) { row.add("<<Cannot Display>>" ); } } trace(row.toString(), false ); } 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); } public ResultSet getRs () { return rs; } }
总结 mybatis 日志模块使用适配器模式进行不同类型的日志框架的统一,使用动态代理对连接执行sql,结果集进行日志的增强,使其无缝的与mybatis结合起来,你们的日志是怎么打印的呢?