最近在做mych(一个MySQL到内存数据库h2的同步工具)时需要在数据操作时打印日志,主要是为了打印sql语句和参数。刚开始我是把日志打印写在各个数据处理工具类中的,主要包括insert、update和delete处理类中。后来觉得这种打印日志的方式对代码的侵入性太高,不仅仅是可读性变差了,更重要的是无法重用这些代码,代码分散在各个数据处理类中,如果新增了数据处理类也需要打印,则需要重新添加代码。
于是我就想怎么对此做一些优化,突然想到在mybatis中也有类似的需求,何不看看mybatis是如何处理的呢?凭着记忆首先找到了SimpleExecutor类,在org.apache.ibatis.executor包下面,我们看一下doQuery方法,代码如下:
@Override public List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { Statement stmt = null; try { Configuration configuration = ms.getConfiguration(); StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); stmt = prepareStatement(handler, ms.getStatementLog()); return handler.query(stmt, resultHandler); } finally { closeStatement(stmt); } }
我们可以看到其中调用了prepareStatement方法来创建Statement对象,prepareStatement方法如下:
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { Statement stmt; Connection connection = getConnection(statementLog); stmt = handler.prepare(connection, transaction.getTimeout()); handler.parameterize(stmt); return stmt; }
我们可以看到这里调用了getConnection用于创建Connection对象,继续看getConnection方法,这个方法在父类BaseExecutor中,具体代码如下:
protected Connection getConnection(Log statementLog) throws SQLException { Connection connection = transaction.getConnection(); if (statementLog.isDebugEnabled()) { return ConnectionLogger.newInstance(connection, statementLog, queryStack); } else { return connection; } }
这里先从transaction对象获取数据库连接对象,然后判断了日志对象是否开启debug级别,如果开启了则会调用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); }
这个方法使用jdk的动态代理功能,生成了Connection类的一个代理对象,根据动态代理规则,最终对该对象的操作都将会调用ConnectionLogger类的invoke方法,动态代理对象可以在此方法中实现我们需要的额外功能,我们看看invoke方法的实现:
@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); } }
在这个方法中我们能够清晰的看到,在我们调用prepareStatement方法时,如果日志对象开启了debug级别,则会打印出我们的sql语句,而后生成一个PreparedStatement的代理对象。调用其他方法也是类似的逻辑。除了prepareStatement方法外,调用prepareCall和createStatement方法同样会生成相关的代理类。这里为什么要生成代理类,而不是直接返回被代理对象呢?其实也是为了实现日志打印的相关操作,我们就看一下PreparedStatementLogger的invoke方法具体实现:
@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); } }
具体的代码就不一一解释了,大家可以自行去看源码。
通过jdk提供的动态代理功能,我们可以将非业务处理逻辑从当前对象中移除,保证代码的可读、可维护性,同时可以在动态代理类中植入我们想要的其他功能,而这些功能对于被代理对象来说是透明的,尤其适合用来处理框架层面一些业务逻辑。