上半年在進行知識儲備,下半年爭取寫一點好的部落格來記錄自己原始碼之路。在學習原始碼的路上也掌握了一些設計模式,可所謂一舉兩得。本次打算寫Mybatis的原始碼解讀。
準備工作
1. 下載mybatis原始碼
下載地址:https://github.com/mybatis/mybatis-3
2. 下載mybatis-parent原始碼
下載地址:https://github.com/mybatis/parent
3. 編譯
進入mybatis-paren所在資料夾
mvn clean install
進入mybatis所在資料夾
mvn clean mvn install -Dmaven.test.skip=true
4. 用IDEA或Eclipse開啟mybatis即可
原始碼分析-日誌模組
1. 日誌基礎包
package org.apache.ibatis.logging; // mybatis自定義介面,提供四種級別 error->debug->trace->warn 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); }
如上,mybatis提供了日誌的四種級別,error->debug->trace->warn
2. 從原始碼可以看到,mybatis提供瞭如jdk14,log4j,log4j2等日誌實現,分析常用的如log4j2原始碼
// log4j的介面卡 public class Log4j2Impl implements Log { // 真正提供日誌能力的log4j的日誌類 private Log log; // 構造方法,匯入真正的實現類 public Log4j2Impl(String clazz) { Logger logger = LogManager.getLogger(clazz); if (logger instanceof AbstractLogger) { log = new Log4j2AbstractLoggerImpl((AbstractLogger) logger); } else { log = new Log4j2LoggerImpl(logger); } } public boolean isDebugEnabled() { return log.isDebugEnabled(); } public boolean isTraceEnabled() { return log.isTraceEnabled(); } // 呼叫真正的實現方法 public void error(String s, Throwable e) { log.error(s, e); } // 呼叫真正的實現方法 public void error(String s) { log.error(s); } // 呼叫真正的實現方法 public void debug(String s) { log.debug(s); } // 呼叫真正的實現方法 public void trace(String s) { log.trace(s); } // 呼叫真正的實現方法 public void warn(String s) { log.warn(s); } }
綜上,可以看到,mybatis並沒有提供真正的日誌實現介面,只是定義了一套自己的日誌介面,其實現交給真正的具體日誌類(如log4j,log4j2)。此處用到了介面卡模式。比如log4j2,mybatis提供自定義介面,提供log4j2介面卡繼承自定義介面,在log4j2介面卡裡呼叫log4j2的真實方法來實現自己的介面。
3.各日誌預設呼叫流程
看原始碼包中有很多日誌實現,那具體的預設呼叫流程是怎樣。檢視org.apache.ibatis.logging包中的LogFactory類。看名字就知道是用到了工廠模式。
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;
// 自動掃描日誌實現,並且第三方日誌外掛載入優先順序如下
// 類載入時會預設實現靜態方法,實現順序為slf4j->commonsLoging->log4j2->log4j->jdklog
static {
// 呼叫tryImplementation方法
tryImplementation(new Runnable() {
public void run() {
useSlf4jLogging();
}
});
tryImplementation(new Runnable() {
public void run() {
useCommonsLogging();
}
});
tryImplementation(new Runnable() {
public void run() {
useLog4J2Logging();
}
});
tryImplementation(new Runnable() {
public void run() {
useLog4JLogging();
}
});
tryImplementation(new Runnable() {
public void run() {
useJdkLogging();
}
});
tryImplementation(new Runnable() {
public void run() {
useNoLogging();
}
});
}
......
LogFactory有預設的日誌載入順序,寫在靜態程式碼塊中。預設實現順序為slf4j->commonsLoging->log4j2->log4j->jdklog。接著分析tryImplementation方法
// 此方法呼叫的是執行緒的run方法,仔細看,是直接run,不是start,所以不是多執行緒 private static void tryImplementation(Runnable runnable) { // 判斷全域性的日誌構造方法是否為空,若為空,則呼叫執行緒的run方法 if (logConstructor == null) { try { runnable.run(); } catch (Throwable t) { // 對沒有具體實現的日誌類,直接忽略,不拋異常, // ignore } } }
檢視具體的run方法,此處繼續分析log4j2的run方法。
public static synchronized void useLog4J2Logging() { setImplementation(org.apache.ibatis.logging.log4j2.Log4j2Impl.class); } // 使用到了java的反射機制 private static void setImplementation(Class<? extends Log> implClass) { try { // 利用反射獲取log4j2的真實構造方法 Constructor<? extends Log> candidate = implClass.getConstructor(new Class[] { String.class }); // 獲取log4j2的例項 Log log = candidate.newInstance(new Object[] { LogFactory.class.getName() }); log.debug("Logging initialized using '" + implClass + "' adapter."); // 賦值給全域性變數logConstructor logConstructor = candidate; } catch (Throwable t) { throw new LogException("Error setting Log implementation. Cause: " + t, t); } }
以log4j2為例,分析了具體的日誌實現。但我們使用mybatis時通常會在執行sql的時候,對sql進行列印輸出。所以接下來檢視mybatis如何使用這些日誌介面。
4. 分析org.apache.ibatis.logging.jdbc下的類
在原始碼分析前,回顧下jdbc連線資料庫方式。
........ Class.forName("com.mysql.jdbc.Driver"); //2.獲得資料庫連結 Connection conn=DriverManager.getConnection(URL, USER, PASSWORD); //3.通過資料庫的連線運算元據庫,實現增刪改查(使用Statement類) Statement st=conn.createStatement(); ResultSet rs=st.executeQuery("select * from user"); .........
載入資料庫忽略,此處有Connection,Statement,ResultSet等引數,檢視org.apache.ibatis.logging.jdbc下的類,都有對應的log類,先分析BaseJdbcLogger類。
public abstract class BaseJdbcLogger { // PrepareedStatement下的所有set方法 protected static final Set<String> SET_METHODS = new HashSet<String>(); protected static final Set<String> EXECUTE_METHODS = new HashSet<String>(); // PrepareecdStatement 設定的key value pair private Map<Object, Object> columnMap = new HashMap<Object, Object>(); // PreparedStatement 設定的column private List<Object> columnNames = new ArrayList<Object>(); // PrepareedStatement 設定的value private List<Object> columnValues = new ArrayList<Object>(); protected Log statementLog; protected int queryStack; /* * Default constructor */ public BaseJdbcLogger(Log log, int queryStack) { this.statementLog = log; if (queryStack == 0) queryStack = 1; this.queryStack = queryStack; } // 初始化預設的一些set方法 static { SET_METHODS.add("setString"); SET_METHODS.add("setInt"); SET_METHODS.add("setByte"); SET_METHODS.add("setShort"); SET_METHODS.add("setLong"); SET_METHODS.add("setDouble"); SET_METHODS.add("setFloat"); SET_METHODS.add("setTimestamp"); SET_METHODS.add("setDate"); SET_METHODS.add("setTime"); SET_METHODS.add("setArray"); SET_METHODS.add("setBigDecimal"); SET_METHODS.add("setAsciiStream"); SET_METHODS.add("setBinaryStream"); SET_METHODS.add("setBlob"); SET_METHODS.add("setBoolean"); SET_METHODS.add("setBytes"); SET_METHODS.add("setCharacterStream"); SET_METHODS.add("setClob"); SET_METHODS.add("setObject"); SET_METHODS.add("setNull"); EXECUTE_METHODS.add("execute"); EXECUTE_METHODS.add("executeUpdate"); EXECUTE_METHODS.add("executeQuery"); EXECUTE_METHODS.add("addBatch"); } // setColumn方會記錄設定的column和對應的value protected void setColumn(Object key, Object value) { columnMap.put(key, value); columnNames.add(key); columnValues.add(value); }
再來分析ConnectionLogger類
// 實現InvocationHandler就知道是個代理類 public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler { // 真正的connection private Connection connection; private ConnectionLogger(Connection conn, Log statementLog, int queryStack) { super(statementLog, queryStack); this.connection = conn; } // 增強 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方法 if ("prepareStatement".equals(method.getName())) { // 列印sql引數 if (isDebugEnabled()) { debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true); } // 呼叫connection真實方法 PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params); // 將生成的PreparedStatement也構建成代理物件 stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack); return stmt; } else if ("prepareCall".equals(method.getName())) { // 若是prepareCall方法 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())) { // 若是createStatement方法 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); } } /* * Creates a logging version of a connection * * @param conn - the original connection * @return - the connection with logging */ 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); } /* * return the wrapped connection * * @return the connection */ public Connection getConnection() { return connection; } }
後續如PreparedStatementLogger,ResultSetLogger等也是一樣,都是封裝了PreparedStatement或ResultSet,在執行真實語句前後進行日誌列印,列印執行的Sql語句,此處用到了代理模式。熟悉AOP的可能對此有了解。