通过上篇文章《Mybatis 框架日志相关源码分析(二)》了解了 Mybatis 通过工厂模式创建 Log 接口的实现类,那么拿到实现类之后, Mybatis 是如何输出日志的呢?
本文将分析 Mybatis 框架的日志相关源码,了解 Mybatis 使用 JDBC 时,是通过何种方式输出日志。
Mybatis 执行过程也可使用 JDBC 差不多,首先是要获取 Connection 对象。而获取此对象是通过 BaseExecutor#getConnection() 方法。
protected Connection getConnection(Log statementLog) throws SQLException {
Connection connection = transaction.getConnection();
if (statementLog.isDebugEnabled()) {
return ConnectionLogger.newInstance(connection, statementLog, queryStack);
}
return connection;
}
这个方法的新参看到了熟悉的 Log 接口,假设 isDebugEnabled() 方法返回false,则返回正常的 Connection 对象;而 true 情况,则是通过 org.apache.ibatis.logging.jdbc.ConnectionLogger 类获取 Connection 对象。看来真相应该就在 ConnectionLogger#newInstance() 方法里面了。
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);
}
先分析 ConnectionLogger 类。此类继承 BaseJdbcLogger 并实现 InvocationHandler 接口。而实现 InvocationHandler 接口的类,说明这是一个动态代理类。当代理的原对象方法被调用时,代理对象会执行 invoke() 方法。所以 ConnectionLogger 类中,肯定实现了 invoke() 方法。
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
if ("prepareStatement".equals(method.getName()) || "prepareCall".equals(method.getName())) {
if (isDebugEnabled()) {
// 输出日志
debug(" Preparing: " + removeExtraWhitespace((String) params[0]), true);
}
// 返回 PreparedStatementLogger 代理对象
PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
return PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
}
// 如果方法名为:createStatement
if ("createStatement".equals(method.getName())) {
Statement stmt = (Statement) method.invoke(connection, params);
// 返回 StatementLogger 对象
return StatementLogger.newInstance(stmt, statementLog, queryStack);
} else {
// 执行被代理对象方法
return method.invoke(connection, params);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
这个时候再回顾下 JDBC 的使用方法:
...
PreparedStatement updateSales = con.prepareStatement("SELECT * FROM USER WHERE ACCOUNT = ?");
...
假设 con 是代理对象,此时调用了 prepareStatement 方法,那么程序就会走到代理对象中的 invoke 方法。
同理,Mybatis 在上面获取的 Connection 对象是 ConnectionLogger 代理对象,那么当调用 Connection 对象的 prepareStatement 方法时,也就自然就调用 ConnectionLogger 的 invoke 方法。于是,上面 Mybatis 代码中的 debug(" Preparing: " + removeExtraWhitespace((String) params[0]), true);
就可以被系统执行,从而实现输出日志。
到此,我们知道了 Mybatis 是通过动态代理的方式,来输出调用 Connection 类中的方法时的日志。而在源代码中,不仅仅只针对 Connection 使用了代理,还对 PreparedStatement 、ResultSet 、Statement 也都是使用了代理,代理类为:PreparedStatementLogger 、ResultSetLogger 、 StatementLogger 。