使用mybatis查詢數據庫時,若日志級別為debug時,自動打印sql語句,參數值以及結果集數目,類似這樣
==> Preparing: select id, description from demo where id = ? ==> Parameters: 1(Integer) <== Total: 1
若是使用jdbc,打印類似日志,原有的jdbc邏輯里,我們需要插入日志打印邏輯
1 String sql = "select id, description from demo where id = ?"; 2 System.out.println("==> Preparing: " + sql); 3 PreparedStatement psmt = conn.prepareStatement(sql); 4 System.out.println("==> Parameters: 1(Integer)"); 5 psmt.setInt(1,1); 6 ResultSet rs = psmt.executeQuery(); 7 int count = 0; 8 while(rs.next()) { 9 count++; 10 } 11 System.out.println("<== Total:" + count);
這樣做是因為我們無法改變jdbc代碼,不能讓數據庫連接獲取PreparedStatement對象時,告訴它你把傳給你的sql語句打印出來吧。這時候就在想如果prepareStatement有一個日志打印的功能就好了,還要可以傳入日志對象和日志格式參數就更好了,可惜它沒有這樣的功能。
我們只能額外在獲取PreparedStatement對象時,PreparedStatement對象設置參數時和ResultSet處理返回結果集時這些邏輯之上加上日志打印邏輯,這是很讓人沮喪的代碼。其實很容易想到,這種受限於原有功能實現,需要增強其能力,正是代理模式適用的場景,我們只需要創建對應的代理類去重寫我們想要增強的方法就好。
回到mybatis。mybatis查詢數據庫時也是使用的jdbc,mybatis作為一種持久層框架,使用了動態代理來增強jdbc原有邏輯,代碼在org.apache.ibatis.logging.jdbc包下,下面從getMapper來逐步分析mybatis如何實現打印sql語句。
mybatis有兩種接口調用方式,一種是基於默認方法傳入statementID,另一種是基於Mapper接口,其實兩種方式是一樣的,一會就可以知道,先介紹getMapper。
getMapper是mybatis頂層API SqlSession的一個方法,默認實現
public class DefaultSqlSession implements SqlSession {
// mybatis配置信息對象 private final Configuration configuration; @Override public <T> T getMapper(Class<T> type) { return configuration.<T>getMapper(type, this); } }
Configuration 保存解析后的配置信息,繼續往下走
public class Configuration {
protected final MapperRegistry mapperRegistry = new MapperRegistry(this); public <T> T getMapper(Class<T> type, SqlSession sqlSession) { return mapperRegistry.getMapper(type, sqlSession); } }
MapperRegistry 保存着Mapper接口與對應代理工廠的映射關系
public class MapperRegistry { private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>(); public <T> T getMapper(Class<T> type, SqlSession sqlSession) { final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); if (mapperProxyFactory == null) { throw new BindingException("Type " + type + " is not known to the MapperRegistry."); } try { return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException("Error getting mapper instance. Cause: " + e, e); } } }
MapperProxyFactory是一個生成Mapper代理類的工廠,使用動態代理去生成具體的mapper接口代理類
public class MapperProxyFactory<T> {
// 構造器中初始化 private final Class<T> mapperInterface; protected T newInstance(MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); } public T newInstance(SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } }
分析MapperProxy定義的代理行為,調用mapper接口里面的方法時,都會走這里,這里完成了mapper接口方法與Mapper.xml中sql語句的綁定,相關參數已在MapperMethod構造器中初始化,這里邏輯較為復雜,簡單來說,就是讓接口中的方法指向具體的sql語句
public class MapperProxy<T> implements InvocationHandler, Serializable { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else if (isDefaultMethod(method)) { return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); }
// 這里關聯mapper中接口方法與Mapper.xml中的sql語句 final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); } }
下一步就是MapperMethod具體的執行邏輯了,這里內容較多,主要是對執行sql的類型進行判斷,簡單截取select部分
case SELECT: if (method.returnsVoid() && method.hasResultHandler()) { executeWithResultHandler(sqlSession, args); result = null; } else if (method.returnsMany()) { result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { result = executeForMap(sqlSession, args); } else if (method.returnsCursor()) { result = executeForCursor(sqlSession, args); } else { Object param = method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(command.getName(), param); } break;
看到這里就可以發現原來當我們使用getMapper生成的代理類型時,調用內部自定義方法,仍然是基於mybatis默認方法的。不過這好像和打印sql語句沒啥關系,重點在類似 sqlSession.selectOne(command.getName(), param)方法,依舊去查看其默認實現DefaultSqlSession,可以發現是調用內部的執行器去執行查詢方法的
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { try { MappedStatement ms = configuration.getMappedStatement(statement);
// mybatis存在3種執行器,批處理、緩存和標准執行器 return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
這里的執行器是在構造方法內賦值的,默認情況下使用的是SimpleExecutor,這里省略父類query方法中的緩存相關代碼,重點是子類去實現的doQuery方法
@Override public <E> List<E> 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.<E>query(stmt, resultHandler); } finally { closeStatement(stmt); } } 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; }
這里獲取Connection對象時,猜測mybatis一定是對Connection進行了增強,不然無法在獲取Statement 之前打印sql語句
protected Connection getConnection(Log statementLog) throws SQLException { Connection connection = transaction.getConnection(); if (statementLog.isDebugEnabled()) { return ConnectionLogger.newInstance(connection, statementLog, queryStack); } else { return connection; } }
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); }
代碼很清楚的,如果你的當前系統支持打印debug日志,那么就動態幫你生成一個連接對象,這里傳入的代理行為是這個類本身,那么只需要分析其的invoke方法就好了,這里依舊只分析一部分必要的
public Object invoke(Object proxy, Method method, Object[] params) throws Throwable { 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; } }
在這里找到了打印的sql信息,還可以發現下面的PreparedStatementLogger繼續增強,邏輯都是一樣的,就不分析了。有一點需要注意的,這里是mapper接口的動態代理類,所以這里的日志級別是受接口所在包的日志級別控制的,只需要配置mapper接口的日志級別是debug,就可以看見打印的sql語句了。
到這里,已經知道了,sql信息是如何打印出來的了,可是,還有一個問題需要解決,這里的日志對象是怎么來的,mybatis本身是沒有日志打印能力的。
mybatis本身並沒有內嵌日志框架,而是考慮了用戶本身的日志框架的選擇。簡而言之,就是用戶用啥日志框架,它就用什么框架,當用戶存在多種選擇時,它也有自己的偏好設計(可以指定)。這樣做,就需要mybatis兼容市面上常見的日志框架,同時自己也要有一套日志接口,mybatis定義日志輸出級別控制,兼容日志框架提供的具體實現類。這是不是又和一種設計模式很像了,適配器模式,轉換不同接口,實現統一調用,具體的代碼在org.apache.ibatis.logging包下。
org.apache.ibatis.logging.Log 是mybatis自己定義的日志接口,org.apache.ibatis.logging.LogFactory 是mybatis用於加載合適的日志實現類,其下的眾多包,均是日志框架的適配器類,主要做日志級別的轉換和具體log對象的創建,那么這些適配器類怎么加載的,LogFactory有一個靜態代碼塊去嘗試加載合適的日志框架,然后創建正確的log對象。

public final class LogFactory { private static Constructor<? extends Log> logConstructor; static { tryImplementation(new Runnable() { @Override public void run() { useSlf4jLogging(); } }); tryImplementation(new Runnable() { @Override public void run() { useCommonsLogging(); } }); ... } 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); } } private static void tryImplementation(Runnable runnable) { if (logConstructor == null) { try { runnable.run(); } catch (Throwable t) { // ignore } } } 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); } } }
這里會按順序去嘗試加載不同的日志框架,若當前系統中存在對應的日志框架,才可以加載成功,這樣logConstructor就有值了,下面則不會再嘗試加載,在getLog里面則是實例化具體的日志對象。
這樣就完成了mybatis如何打印sql語句的整體流程解析,主要有兩點,創建Log對象和通過動態代理給jdbc增加日志打印能力。