通过上篇文章《Mybatis 框架日志相关源码分析(二)》了解了 Mybatis 通过工厂模式创建 Log 接口的实现类,那么拿到实现类之后, Mybatis 是如何输出日志的呢?
本文将分析 Mybatis 框架的日志相关源码,了解 Mybatis 使用 JDBC 时,是通过何种方式输出日志。Mybatis 执行过程也可使用 JDBC 差不多,首先是要获取 Connection 对象。而获取此对象是通过 [BaseExecutor#getConnection()](https://github.com/mybatis/mybatis-3/blob/1ff4a865e77acfa51e8eaaf7bf605f5cf098093f/src/main/java/org/apache/ibatis/executor/BaseExecutor.java#L344) 方法。
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 。