Mybatis Log 源码分析

警告
本文最后更新于 2020-12-20,文中内容可能已过时。

mybatis源码系列文章,示例中带有中文注释的源码 copy 自 https://gitee.com/wlizhi/mybatis-3

链接中源码是作者从 github 下载,并以自身理解对核心流程及主要节点做了详细的中文注释。


1 适配器模式

适配器模式(Adapter Patter)是作为两个不兼容接口之间的桥梁,将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不 兼容而不能一起工作的那些类可以一起工作。

适用场景:当调用双方都不太容易修改的时候,为了复用现有组件可以使用适配器模式;在系统中接入第三方组件的时候经常被使用到。

注意:如果系统中存在过多的适配器,会增加系统的复杂性,设计人员应考虑对其进行重构。

2 代理模式

定义:给目标对象提供一个代理对象,并由代理对象控制对目标对象的引用。

目的:

  1. 通过引入代理对象的方式来间接访问目标对象,防止直接访问目标对象给系统带来不必要的复杂性。
  2. 通过代理对象对原有的业务增强。

3 Mybatis Log 模块核心类结构图

mybatis-log-class-structure.png

4 Mybatis Log 模块的核心类

  • LogFactory log对象工厂
  • Log mybatis 中自定义的一个 log 接口。使用了适配器模式,所有的第三方日志组件引入后,通过适配类,最终都会被转换为 Log 类型,mybatis 源码中的日志,统一使用 Log 接口。
  • BaseJdbcLogger jdbc包中所有日志增强的抽象基类,使用动态代理对目标进行日志增强。
    • ConnectionLogger 对 Connection 的日志增强类。
    • PreparedStatementLogger 对 PreparedStatement 的日志增强类。
    • ResultSetLogger 对 ResultSet 的日志增强类。
    • StatementLogger 对 Statement 的日志增强类。(一般使用 PreparedStatementLogger,且与前者原理相同,不列举。)

5 Log工厂类 LogFactory

在类加载器加载 LogFactory 时,通过静态代码块,按照一定顺序,加载第三方日志组件,只要有一个加载成功,下面的就不再加载。

第三方日志组件加载优先级:slf4j -> commonsLogging -> log4J2 -> log4j -> JdkLog

加载后将对应的日志组件的构造函数缓存到静态成员中。最终通过 LogFactory.getLog(Class<?> clazz) 进行日志对象的获取。

源码如下:

 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
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 {
    // 注意这里实在静态代码块中执行,在类加载器,第一次加载 LogFactory 时,就会执行这块代码,进行日志具体实现的初始化。
    // 按照优先级尝试加载日志实现类。第三方日志插件加载优先级: slf4j -> commonsLogging -> log4J2 -> log4j -> JdkLog
    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<?> clazz) {
    return getLog(clazz.getName());
  }

  public static Log getLog(String logger) {
    try {// 获取log对象,通过静态块中加载log实现类时,缓存的log实现构造函数,以反射的方式创建log对象。
      return logConstructor.newInstance(logger);
    } catch (Throwable t) {
      throw new LogException("Error creating logger for logger " + logger + ".  Cause: " + t, t);
    }
  }
  // 省略部分方法...

  private static void tryImplementation(Runnable runnable) {
    // 这个方法是加载log的方法。当构造方法为空,才执行方法。如果加载不到,则会忽略异常。
    if (logConstructor == null) {
      try {
        runnable.run();
      } catch (Throwable t) {
        // 注意这里忽略了异常,即当加载一个log实现,加载不到时,就会尝试加载另一种实现。
        // 而加载不到的实现会抛出 java.lang.NoClassDefFoundError 到这里,最终会忽略掉异常。
        // ignore
      }
    }
  }

  // 通过指定的 log 类来初始化构造方法
  private static void setImplementation(Class<? extends Log> implClass) {
    try {
      // 反射创建 log 实例。
      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);
    }
  }

}

tryImplementation() 中调用了具体加载日志组件的方法,如果项目中未配置对象的组件,则会在 run() 中抛出异常 java.lang.NoClassDefFoundError,并在此方法中捕获,这里会忽略掉加载的异常。

注意判断条件 logConstructor == null 也就是说,只有加载失败时,才会向下执行继续加载下一个组件,如果加载成功了,则 logConstructor 是有值的。

setImplementation() 是具体加载日志组件的方法,如果加载成功,会对 logConstructor 进行赋值,并将具体使用的日志实现类进行日志打印。

6 Mybatis Log 规范 Log

mybatis将不同组件的日志,统一封装成了四种日志级别。分别是 trace、debug、warn、error。

这四种日志级别,在 Log 接口中进行了定义。

源码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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);
}

7 日志增强基 BaseJdbcLogger

BaseJdbcLogger 是所有 jdbc 相关日志增强的基类,在这里定义了一些变量,用于缓存与数据库交互中传递的一些参数信息。

BaseJdbcLogger 数据结构如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public abstract class BaseJdbcLogger {

	/** 保存preparestatment中常用的set方法(占位符赋值) */
	protected static final Set<String> SET_METHODS;
	/** 保存preparestatment中常用的执行sql语句的方法 */
	protected static final Set<String> EXECUTE_METHODS = new HashSet<>();

	/** 保存preparestatment中set方法的键值对 */
	private final Map<Object, Object> columnMap = new HashMap<>();

	/** 保存preparestatment中set方法的key值 */
	private final List<Object> columnNames = new ArrayList<>();
	/** 保存preparestatment中set方法的value值 */
	private final List<Object> columnValues = new ArrayList<>();
	/** 日志对象 */
	protected final Log statementLog;
	protected final int queryStack;
}

在静态块中,对 SET_METHODS 以及 EXECUTE_METHODS 进行了初始化。

初始化源码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public abstract class BaseJdbcLogger {

	static {
		// 反射获取 PreparedStatement 中所有 set 开头且有参数的方法名称
		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");
	}
}

并提供了一些设置参数以及获取参数值的字符串表示的方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public abstract class BaseJdbcLogger {
	
	protected void setColumn(Object key, Object value) {
		columnMap.put(key, value);
		columnNames.add(key);
		columnValues.add(value);
	}
	
	protected String getParameterValueString() {
		// 将参数的值、参数类型,封装成string返回。
		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);
	}
}

还有一些其他方法,不再列举。

8 连接日志增强 ConnectionLogger

ConnectionLogger 实现了 InvocationHandler,且内部持有了 Connection 实例。它是 Connection 的日志增强类,从这个实现接口可以看出,使用的是 jdk 动态代理。

它的核心在 invoke 方法,源码如下:

 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
public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler {
	/** 使用代理模式对连接进行日志增强。这里封装了连接对象 */
	private final Connection connection;

	@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 方法,则参数列表中,第一个参数一定是 sql 语句,这里打印 sql 语句。
			if ("prepareStatement".equals(method.getName()) || "prepareCall".equals(method.getName())) {
				if (isDebugEnabled()) {
					// 在打印sql语句的时候,将sql中的多余空白去除,替换为单个空格。
					debug(" Preparing: " + removeExtraWhitespace((String) params[0]), true);
				}
				// 调用业务方法
				PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
				// 创建 PreparedStatement 的代理对象,即对其进行日志增强,与此类相似。使用 PreparedStatementLogger 作为增强类。
				stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
				return stmt;
			} else if ("createStatement".equals(method.getName())) {
				// 如果是 createStatement 方法,执行业务方法
				Statement stmt = (Statement) method.invoke(connection, params);
				// 对 Statement 创建代理,使用 StatementLogger 作为增强类。
				stmt = StatementLogger.newInstance(stmt, statementLog, queryStack);
				return stmt;
			} else { // 如果以上方法都不是,则直接执行业务方法。即只对上面两个 if 分支中判断的方法进行日志增强。
				return method.invoke(connection, params);
			}
		} catch (Throwable t) {
			throw ExceptionUtil.unwrapThrowable(t);
		}
	}
}

在 invoke() 中,对 Connection 进行了日志增强,针对 prepareStatement()、prepareCall()、createStatement() 进行日志打印,并对其返回值生成代理。

还有一个生成 Connection 代理的方法:

1
2
3
4
5
6
7
8
public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler {
    
	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);
	}
}

9 PreparedStatementLogger

与 ConnectionLogger 类似,它也是一个 InvocationHandler 实现类。内部持有了 PreparedStatement 对象。对一些特定方法添加了日志增强, 并对方法的返回值 ResultSet 生成代理对象。

源码如下:

 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
public final class PreparedStatementLogger extends BaseJdbcLogger implements InvocationHandler {

	private final PreparedStatement statement;

	@Override
	public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
		try {
			// 这个方法的增强内容,都是打印 debug 日志,在打印之前,判断 isDebugEnabled(),为了提高性能,
			// 因为打印的内容可能存在字符串拼装,相对判断一个布尔值,相对要消耗性能许多,而 debug 一般在生产环境并不会激活。

			// 如果是 Object 中声明的方法,则直接跳过,执行业务方法。
			if (Object.class.equals(method.getDeclaringClass())) {
				return method.invoke(this, params);
			}
			// BaseJdbcLogger 中静态块中声明的方法集合。如果当前方法是集合中的方法,则打印日志。
			if (EXECUTE_METHODS.contains(method.getName())) {
				// 如果 debug 级别是开启的,则打印日志。打印内容是参数,比如一个 insert 语句,则这里会打印 mapper 方法中传递的参数及类型。
				if (isDebugEnabled()) {
					debug("Parameters: " + getParameterValueString(), true);
				}
				// 清除字段信息
				clearColumnInfo();
				// 如果是 executeQuery 方法,则直接调用方法。并对 ResultSet 生成代理对象。
				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);
				}
				// 如果是 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);
				// 如果是 getResultSet 方法,则执行方法后,将 ResultSet 生成代理,进行日志增强。
			} else if ("getResultSet".equals(method.getName())) {
				ResultSet rs = (ResultSet) method.invoke(statement, params);
				return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
				// 如果是 getUpdateCount 方法,则在执行方法后,打印 debug 日志。
			} 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);
		}
	}
}

创建 PreparedStatement 代理的方法:

1
2
3
4
5
6
7
8
9
public final class PreparedStatementLogger extends BaseJdbcLogger implements InvocationHandler {
	public static PreparedStatement newInstance(PreparedStatement stmt, Log statementLog, int queryStack) {
		// 使用当前类作为增强类,创建 PreparedStatement 的代理对象。
		// 符合单一职责原则,创建目标代理对象的工作应该交给与之相关的类来做,而本类就是目标类的增强。
		InvocationHandler handler = new PreparedStatementLogger(stmt, statementLog, queryStack);
		ClassLoader cl = PreparedStatement.class.getClassLoader();
		return (PreparedStatement) Proxy.newProxyInstance(cl, new Class[]{PreparedStatement.class, CallableStatement.class}, handler);
	}
}

10 结果集日志增强 ResultSetLogger

ResultSetLogger 实现了 InvocationHandler,是对 ResultSet 的日志增强。

invoke() 源码如下:

 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
public final class ResultSetLogger extends BaseJdbcLogger implements InvocationHandler {

	public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
		try {
			// 如果是 Object 中声明的方法,则直接跳过,执行业务方法。
			if (Object.class.equals(method.getDeclaringClass())) {
				return method.invoke(this, params);
			}
			Object o = method.invoke(rs, params);
			// 执行 next 方法,判断是否还有数据。
			if ("next".equals(method.getName())) {
				// 如果当前方法是 next,返回值为 true,对变量自增,如果启用了 trace(比debug更低的级别) 级别,则打印 ResultSet 中的值。
				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);
		}
	}
}

创建 ResultSet 代理对象的方法源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public final class ResultSetLogger extends BaseJdbcLogger implements InvocationHandler {

	public static ResultSet newInstance(ResultSet rs, Log statementLog, int queryStack) {
		// 使用当前类作为增强类,创建 ResultSet 的代理对象。
		// 符合单一职责原则,创建目标代理对象的工作应该交给与之相关的类来做,而本类就是目标类的增强。
		InvocationHandler handler = new ResultSetLogger(rs, statementLog, queryStack);
		ClassLoader cl = ResultSet.class.getClassLoader();
		return (ResultSet) Proxy.newProxyInstance(cl, new Class[]{ResultSet.class}, handler);
	}
}

11 小结

对第三方日志组件的支出,通过适配器模式,对不同的日志组件,提供不同的适配器类,进行转换支持。

对不同节点的日志的打印支持,通过动态代理的方式,进行日志信息的增强。这样的好处是对原有业务方法是无侵入的,与业务功能解耦。

相较于 spring 中 aop 的实现原理是不同的,spring 中如果定义了许多增强方法,会将这些增强统一封装成一个 Advisor 列表。执行的时候, 会生成 MethodInterceptor 执行链。而代理对象,最终只生成一个,只是在代理对象的增强方法中,封装了这个执行链。

而 mybatis 中相对简单暴力一些,只是生成代理,就像使用 ConnectionLogger 增强创建 Connection 代理对象时,其内部封装的其实也是一个代理对象, 并非是原始链接。也就是说,在 mybatis 中对一个实例的多次增强,最终每一个增强都会创建代理对象。即对代理对象进行代理。