Mybatis源碼初探——優雅精良的骨架
@
前言
Mybatis是一款半自動的ORM框架,是目前國內Java web開發的主流ORM框架,因此作為一名開發者非常有必要掌握其實現原理,才能更好的解決我們開發中遇到的問題;同時,Mybatis的架構和源碼也是很優雅的,使用了大量的設計模式實現解耦以及高擴展性,所以對其設計思想,我們也非常有必要好好理解掌握。(PS:本系列文章基於3.5.0版本分析)
精良的Mybatis骨架
宏觀設計
Mybatsi的源碼相較於Spring源碼無論是架構還是實現都簡單了很多,它所有的代碼都在一個工程裏面,在這個工程下分了很多包,每個包分工都很明確:
別看模塊有這麼多,實際上只需要分為三層:
這樣分層後,是不是就很清晰了,基礎支撐層是一些通用組件的封裝,如日誌、緩存、反射、數據源等等,這些模塊支撐着核心業務邏輯的實現,並且如果需要我們可以將其直接用於到我們項目中,像反射模塊就是對JDK的反射進行了封裝,使其更加方便易用;核心處理層就是Mybatis的核心業務的實現了,通過底層支撐模塊,實現了配置文件和SQL解析、參數映射和綁定、SQL執行和返回結果的映射以及擴展插件的執行等等;最後接口層則是對外提供的服務,我們使用Mybatis時只需要通過該接口進行操作,對底層的實現無需關注。這樣分層的好處不用多說,讓我們的代碼更加簡潔易讀,同時可維護性和可擴展性也大大提高,另外從整個架構設計中我們可以看到一個設計模式的體現——門面模式,因為門面模式的設計思想就是對外提供一個統一的的接口,屏蔽掉內部系統實現的複雜性,使得用戶無需關注內部實現就能輕鬆使用所有功能,而這裡的架構設計就是採用的這樣一個思想。舉一反三,再想想看其它的開源框架是不是都是這樣的設計?
基礎支撐
在了解了Mybatis的宏觀架構設計後,下面就是對源碼的詳細分析,首先先來看幾個重點的基礎支撐模塊:
- 日誌
- 數據源
- 緩存
- 反射
日誌
日誌的加載
Mybatis本身是沒有實現日誌功能的,而是引入第三方日誌,但第三方日誌都有自己的log級別,Mybatis需要解決的就是如何兼容這些日誌組件。如何兼容呢?Mybatis使用了適配器模式來解決,在logging模塊下提供了一個統一的日誌接口Log接口:
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);
}
可以看到在這個接口中統一定義了各個日誌級別,引入的第三方日誌組件只需要實現該接口,在各個級別接口中調用各組件自身對應的API即可。從下面的類圖我們可以看到Mybatis支持了哪些三方日誌組件:
看到這裡你是否會有疑問,這些第三方日誌組件是怎麼加載的?加載順序又是怎樣的呢?難道是在需要用的地方才實例化么?當然不是,Mybatis這裡又使用了一個設計模式——工廠模式。在日誌模塊下有一個類LogFactory,日誌的加載就是由該類實現的,通過這個類解耦了日誌的實例化和日誌的使用:
public final class LogFactory {
public static final String MARKER = "MYBATIS";
//被選定的第三方日誌組件適配器的構造方法
private static Constructor<? extends Log> logConstructor;
//自動掃描日誌實現,並且第三方日誌插件加載優先級如下:slf4J → commonsLoging → Log4J2 → Log4J → JdkLog
static {
tryImplementation(LogFactory::useSlf4jLogging);
tryImplementation(LogFactory::useCommonsLogging);
tryImplementation(LogFactory::useLog4J2Logging);
tryImplementation(LogFactory::useLog4JLogging);
tryImplementation(LogFactory::useJdkLogging);
tryImplementation(LogFactory::useNoLogging);
}
private LogFactory() {
// disable construction
}
public static Log getLog(Class<?> aClass) {
return getLog(aClass.getName());
}
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);
}
}
public static synchronized void useCustomLogging(Class<? extends Log> clazz) {
setImplementation(clazz);
}
public static synchronized void useSlf4jLogging() {
setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
}
public static synchronized void useCommonsLogging() {
setImplementation(org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl.class);
}
public static synchronized void useLog4JLogging() {
setImplementation(org.apache.ibatis.logging.log4j.Log4jImpl.class);
}
public static synchronized void useLog4J2Logging() {
setImplementation(org.apache.ibatis.logging.log4j2.Log4j2Impl.class);
}
public static synchronized void useJdkLogging() {
setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class);
}
public static synchronized void useStdOutLogging() {
setImplementation(org.apache.ibatis.logging.stdout.StdOutImpl.class);
}
public static synchronized void useNoLogging() {
setImplementation(org.apache.ibatis.logging.nologging.NoLoggingImpl.class);
}
private static void tryImplementation(Runnable runnable) {
if (logConstructor == null) {//當構造方法不為空才執行方法
try {
runnable.run();
} catch (Throwable t) {
// ignore
}
}
}
//通過指定的log類來初始化構造方法
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);
}
}
}
通過上面的代碼我們可以清楚的看到日誌的加載順序是怎樣的,並且只要加載成功了任何一個日誌組件,其它的日誌組件就不會被加載。
日誌的使用
日誌加載完成後,自然而然的我們就該思考的是哪些地方需要打印日誌?Mybatis本身是對JDK原生的JDBC的包裝和增強,所以在以下幾個關鍵地方都應該打印日誌:
- 創建PreparedStatement和Statement時打印SQL語句和參數信息
- 獲取到查詢結果後打印結果信息
問題是應該怎麼優雅地增強這些方法呢?Mybatis使用了動態代理來實現。在日誌模塊下的JDBC包就是代理類的實現,先來看看類圖:
見名知義,看到這些類名我們應該就能清楚這些類的作用,它們就是對原生的JDBC API的增強,在調用相關的方法時,首先會進入到這些代理類的invoke方法裏面,按照執行順序,首先進入調用的肯定是ConnectionLogger:
public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler {
//真正的連接對象
private final Connection connection;
private ConnectionLogger(Connection conn, Log statementLog, int queryStack) {
super(statementLog, queryStack);
this.connection = conn;
}
@Override
//對連接的增強
public Object invoke(Object proxy, Method method, Object[] params)
throws Throwable {
try {
//如果是從Obeject繼承的方法直接忽略
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, params);
}
//如果是調用prepareStatement、prepareCall、createStatement的方法,打印要執行的sql語句
//並返回prepareStatement的代理對象,讓prepareStatement也具備日誌能力,打印參數
if ("prepareStatement".equals(method.getName())) {
if (isDebugEnabled()) {
debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);//打印sql語句
}
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);//打印sql語句
}
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);
}
}
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);
}
public Connection getConnection() {
return connection;
}
}
從invoke方法里我們可以看到主要對Connection的prepareStatement、prepareCall、createStatement方法進行了增強,打印日誌並創建了對應的代理類返回。其它幾個類實現原理都是一樣,這裡不再贅述。
但還有個問題,其它幾個類的調用都是在創建連接之後,所以對應的代理類是由上一個階段的代理類創建的,那ConnectionLogger是在哪裡創建的呢?自然是在獲取連接時,而獲取連接都是在我們的業務代碼執行階段的時候,Mybatis對執行階段又封裝了一個個Excutor執行器,詳細代碼後文分析。
數據源
數據源的創建
數據源都需要實現JDK的DataSource接口,Mybatis自己本身實現了數據源接口,同時也支持第三方的數據源。這裡主要看看Mybatis內部的實現,同樣先來看一張類圖:
從圖中我們可以看到DataSource的初始化同樣是通過工廠模式實現的,而其本身提供了三種數據源:
- PooledDataSource:帶連接池的數據源
- UnpooledDataSource:不帶連接池的數據源
- JNDI數據源
最後一種此處不分析。UnpooledDataSource就是一個普通的數據源,實現了基本的數據源接口;而PooledDataSource是基於UnpooledDataSource實現的,只是在此之上提供了連接池功能。另外還需要注意PooledConnection,該類是連接池中存放的連接對象,但其並不是真正的連接對象,只是持有了真實連接的引用,並且是對真實連接進行增強的代理類,下面就主要分析連接池的實現原理。
池化技術原理
數據結構
首先來看下PooledConnection都封裝了些什麼:
class PooledConnection implements InvocationHandler {
private static final String CLOSE = "close";
private static final Class<?>[] IFACES = new Class<?>[] { Connection.class };
private final int hashCode;
//記錄當前連接所在的數據源對象,本次連接是有這個數據源創建的,關閉後也是回到這個數據源;
private final PooledDataSource dataSource;
//真正的連接對象
private final Connection realConnection;
//連接的代理對象
private final Connection proxyConnection;
//從數據源取出來連接的時間戳
private long checkoutTimestamp;
//連接創建的的時間戳
private long createdTimestamp;
//連接最後一次使用的時間戳
private long lastUsedTimestamp;
//根據數據庫url、用戶名、密碼生成一個hash值,唯一標識一個連接池
private int connectionTypeCode;
//連接是否有效
private boolean valid;
/*
* Constructor for SimplePooledConnection that uses the Connection and PooledDataSource passed in
*
* @param connection - the connection that is to be presented as a pooled connection
* @param dataSource - the dataSource that the connection is from
*/
public PooledConnection(Connection connection, PooledDataSource dataSource) {
this.hashCode = connection.hashCode();
this.realConnection = connection;
this.dataSource = dataSource;
this.createdTimestamp = System.currentTimeMillis();
this.lastUsedTimestamp = System.currentTimeMillis();
this.valid = true;
this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this);
}
......省略
/*
* 此方法專門用來增強數據庫connect對象,使用前檢查連接是否有效,關閉時對連接進行回收
*
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {//如果是調用連接的close方法,不是真正的關閉,而是回收到連接池
dataSource.pushConnection(this);//通過pooled數據源來進行回收
return null;
} else {
try {
//使用前要檢查當前連接是否有效
if (!Object.class.equals(method.getDeclaringClass())) {
// issue #579 toString() should never fail
// throw an SQLException instead of a Runtime
checkConnection();//
}
return method.invoke(realConnection, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
}
private void checkConnection() throws SQLException {
if (!valid) {
throw new SQLException("Error accessing PooledConnection. Connection is invalid.");
}
}
}
屬性和方法上都已經有了詳細的注釋,主要關注realConnection真實連接的引用和invoke方法增強。接着再看連接池的實現,這個類包含了很多屬性:
private final PoolState state = new PoolState(this);
//真正用於創建連接的數據源
private final UnpooledDataSource dataSource;
// OPTIONAL CONFIGURATION FIELDS
//最大活躍連接數
protected int poolMaximumActiveConnections = 10;
//最大閑置連接數
protected int poolMaximumIdleConnections = 5;
//最大checkout時長(最長使用時間)
protected int poolMaximumCheckoutTime = 20000;
//無法取得連接是最大的等待時間
protected int poolTimeToWait = 20000;
//最多允許幾次無效連接
protected int poolMaximumLocalBadConnectionTolerance = 3;
//測試連接是否有效的sql語句
protected String poolPingQuery = "NO PING QUERY SET";
//是否允許測試連接
protected boolean poolPingEnabled;
//配置一段時間,當連接在這段時間內沒有被使用,才允許測試連接是否有效
protected int poolPingConnectionsNotUsedFor;
//根據數據庫url、用戶名、密碼生成一個hash值,唯一標識一個連接池,由這個連接池生成的連接都會帶上這個值
private int expectedConnectionTypeCode;
相信上面大部分屬性讀者們都不會陌生,在進行開發時應該都有配置過。其中有一個關鍵的屬性PoolState,這個是對象主要保存了空閑連接和活躍連接,也就是連接池用來管理資源的,它包含了以下屬性:
protected PooledDataSource dataSource;
//空閑的連接池資源集合
protected final List<PooledConnection> idleConnections = new ArrayList<>();
//活躍的連接池資源集合
protected final List<PooledConnection> activeConnections = new ArrayList<>();
//請求的次數
protected long requestCount = 0;
//累計的獲得連接的時間
protected long accumulatedRequestTime = 0;
//累計的使用連接的時間。從連接取出到歸還,算一次使用的時間;
protected long accumulatedCheckoutTime = 0;
//使用連接超時的次數
protected long claimedOverdueConnectionCount = 0;
//累計超時時間
protected long accumulatedCheckoutTimeOfOverdueConnections = 0;
//累計等待時間
protected long accumulatedWaitTime = 0;
//等待次數
protected long hadToWaitCount = 0;
//無效的連接次數
protected long badConnectionCount = 0;
獲取連接
了解了這些關鍵的屬性後,再來看看如何從連接池獲取連接,在PooledDataSource中有一個popConnection用於獲取連接:
private PooledConnection popConnection(String username, String password) throws SQLException {
boolean countedWait = false;
PooledConnection conn = null;
long t = System.currentTimeMillis();//記錄嘗試獲取連接的起始時間戳
int localBadConnectionCount = 0;//初始化獲取到無效連接的次數
while (conn == null) {
synchronized (state) {//獲取連接必須是同步的
if (!state.idleConnections.isEmpty()) {//檢測是否有空閑連接
// Pool has available connection
//有空閑連接直接使用
conn = state.idleConnections.remove(0);
if (log.isDebugEnabled()) {
log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
}
} else {// 沒有空閑連接
if (state.activeConnections.size() < poolMaximumActiveConnections) {//判斷活躍連接池中的數量是否大於最大連接數
// 沒有則可創建新的連接
conn = new PooledConnection(dataSource.getConnection(), this);
if (log.isDebugEnabled()) {
log.debug("Created connection " + conn.getRealHashCode() + ".");
}
} else {// 如果已經等於最大連接數,則不能創建新連接
//獲取最早創建的連接
PooledConnection oldestActiveConnection = state.activeConnections.get(0);
long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
if (longestCheckoutTime > poolMaximumCheckoutTime) {//檢測是否已經以及超過最長使用時間
// 如果超時,對超時連接的信息進行統計
state.claimedOverdueConnectionCount++;//超時連接次數+1
state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;//累計超時時間增加
state.accumulatedCheckoutTime += longestCheckoutTime;//累計的使用連接的時間增加
state.activeConnections.remove(oldestActiveConnection);//從活躍隊列中刪除
if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {//如果超時連接未提交,則手動回滾
try {
oldestActiveConnection.getRealConnection().rollback();
} catch (SQLException e) {//發生異常僅僅記錄日誌
/*
Just log a message for debug and continue to execute the following
statement like nothing happend.
Wrap the bad connection with a new PooledConnection, this will help
to not intterupt current executing thread and give current thread a
chance to join the next competion for another valid/good database
connection. At the end of this loop, bad {@link @conn} will be set as null.
*/
log.debug("Bad connection. Could not roll back");
}
}
//在連接池中創建新的連接,注意對於數據庫來說,並沒有創建新連接;
conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
//讓老連接失效
oldestActiveConnection.invalidate();
if (log.isDebugEnabled()) {
log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
}
} else {
// 無空閑連接,最早創建的連接沒有失效,無法創建新連接,只能阻塞
try {
if (!countedWait) {
state.hadToWaitCount++;//連接池累計等待次數加1
countedWait = true;
}
if (log.isDebugEnabled()) {
log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
}
long wt = System.currentTimeMillis();
state.wait(poolTimeToWait);//阻塞等待指定時間
state.accumulatedWaitTime += System.currentTimeMillis() - wt;//累計等待時間增加
} catch (InterruptedException e) {
break;
}
}
}
}
if (conn != null) {//獲取連接成功的,要測試連接是否有效,同時更新統計數據
// ping to server and check the connection is valid or not
if (conn.isValid()) {//檢測連接是否有效
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();//如果遺留歷史的事務,回滾
}
//連接池相關統計信息更新
conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
conn.setCheckoutTimestamp(System.currentTimeMillis());
conn.setLastUsedTimestamp(System.currentTimeMillis());
state.activeConnections.add(conn);
state.requestCount++;
state.accumulatedRequestTime += System.currentTimeMillis() - t;
} else {//如果連接無效
if (log.isDebugEnabled()) {
log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
}
state.badConnectionCount++;//累計的獲取無效連接次數+1
localBadConnectionCount++;//當前獲取無效連接次數+1
conn = null;
//拿到無效連接,但如果沒有超過重試的次數,允許再次嘗試獲取連接,否則拋出異常
if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) {
if (log.isDebugEnabled()) {
log.debug("PooledDataSource: Could not get a good connection to the database.");
}
throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
}
}
}
}
}
if (conn == null) {
if (log.isDebugEnabled()) {
log.debug("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
}
throw new SQLException("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
}
return conn;
}
這裡的邏輯相對比較複雜,我總結了整個步驟並畫了一張圖幫助理解:循環獲取連接,首先判斷是否還存在空閑連接,如果存在,則直接使用,並刪除一個空閑連接;如果不存在,優先判斷是否已經達到最大活躍連接數量。如果沒有則直接創建一個新的連接;如果已經達到最大活躍連接數,則從活躍連接池中取出最早的連接,判斷是否超時。如果沒有超時,則調用wait方法阻塞;如果超時,則統計超時連接信息,並根據超時連接的真實連接創建新的連接,同時讓舊連接失效。經過以上步驟後,如果獲取到一個連接,則還需要判斷連接是否有效,有效連接需要回滾之前未提交的事務並添加到活躍連接池,無效連接則統計信息並判斷是否已經超過重試次數,若沒有則繼續循環下一次獲取連接,否則拋出異常。循環完成後返回獲取到的連接。
回收連接
普通的連接是直接關閉,需要用的時候重新創建,而連接池則需要將連接回收到池中復用,避免重複創建連接提高效率,在PooledDataSource中的pushConnection就是用於回收連接的:
protected void pushConnection(PooledConnection conn) throws SQLException {
synchronized (state) {//回收連接必須是同步的
state.activeConnections.remove(conn);//從活躍連接池中刪除此連接
if (conn.isValid()) {
//判斷閑置連接池資源是否已經達到上限
if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {
//沒有達到上限,進行回收
state.accumulatedCheckoutTime += conn.getCheckoutTime();
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();//如果還有事務沒有提交,進行回滾操作
}
//基於該連接,創建一個新的連接資源,並刷新連接狀態
PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
state.idleConnections.add(newConn);
newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
//老連接失效
conn.invalidate();
if (log.isDebugEnabled()) {
log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");
}
//喚醒其他被阻塞的線程
state.notifyAll();
} else {//如果閑置連接池已經達到上限了,將連接真實關閉
state.accumulatedCheckoutTime += conn.getCheckoutTime();
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();
}
//關閉真的數據庫連接
conn.getRealConnection().close();
if (log.isDebugEnabled()) {
log.debug("Closed connection " + conn.getRealHashCode() + ".");
}
//將連接對象設置為無效
conn.invalidate();
}
} else {
if (log.isDebugEnabled()) {
log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
}
state.badConnectionCount++;
}
}
}
回收連接的邏輯就比較簡單了,不過還是有幾個地方需要注意:首先從活躍連接池移除掉該連接,然後判斷是否是有效連接以及空閑連接池是否還有位置,如果是有效連接且空閑連接池還有位置的話,則需要基於當前回收連接的真實連接並創建新的連接放入到空閑連接中,然後喚醒等待的線程;如果沒有則直接關閉真實連接。這兩個分支都需要將回收的連接中未提交的事務回滾並將連接置為無效。如果本來就是無效連接則只需要記錄獲取無效連接的次數。
以上就是Mybatis數據源以及連接池的實現原理,其中池化技術是非常重要的。
緩存
緩存的實現
Mybatis有一級緩存和二級緩存,一級緩存是SqlSession級別的,只能存在於同一個SqlSession生命周期中;二級緩存則是跨SqlSession,以namespace為單位的。但實際上Mybatis的二級緩存非常雞肋,有可能出現臟讀的情況,一般不會使用。
但Mybatis對緩存做了大量的擴展,提供了防止緩存擊穿、緩存清空策略、序列化、定時清空、日誌等功能,設計非常優雅,所以此處主要領略這一模塊的設計思想。先來看看包的結構:
從上圖中我們可以看到,Mybatis提供了統一的緩存接口,impl和decorators包中都是它的實現類,從包的名字我們可以想到緩存這裡又是使用了一個設計模式——裝飾者模式,利用該模式動態得為緩存添加功能。真正的實現就是impl包 下的PerpetualCache,通過HashMap來緩存數據的(會不會出現並發安全問題?),key是CacheKey對象,value是緩存的數據,為什麼key是CacheKey對象,而不是一個字符串呢?讀者可以想想,怎樣才能確定不會讀取到錯誤的緩存,這個類最後來分析。而decorators包下的都是進行功能增強的裝飾者類,這裡主要來看看BlockingCache是如何防止緩存擊穿的。
public class BlockingCache implements Cache {
//阻塞的超時時長
private long timeout;
//被裝飾的底層對象,一般是PerpetualCache
private final Cache delegate;
//鎖對象集,粒度到key值
private final ConcurrentHashMap<Object, ReentrantLock> locks;
public BlockingCache(Cache delegate) {
this.delegate = delegate;
this.locks = new ConcurrentHashMap<>();
}
@Override
public void putObject(Object key, Object value) {
try {
delegate.putObject(key, value);
} finally {
releaseLock(key);
}
}
@Override
public Object getObject(Object key) {
acquireLock(key);//根據key獲得鎖對象,獲取鎖成功加鎖,獲取鎖失敗阻塞一段時間重試
Object value = delegate.getObject(key);
if (value != null) {//獲取數據成功的,要釋放鎖
releaseLock(key);
}
return value;
}
@Override
public Object removeObject(Object key) {
// despite of its name, this method is called only to release locks
releaseLock(key);
return null;
}
private ReentrantLock getLockForKey(Object key) {
ReentrantLock lock = new ReentrantLock();//創建鎖
ReentrantLock previous = locks.putIfAbsent(key, lock);//把新鎖添加到locks集合中,如果添加成功使用新鎖,如果添加失敗則使用locks集合中的鎖
return previous == null ? lock : previous;
}
//根據key獲得鎖對象,獲取鎖成功加鎖,獲取鎖失敗阻塞一段時間重試
private void acquireLock(Object key) {
//獲得鎖對象
Lock lock = getLockForKey(key);
if (timeout > 0) {//使用帶超時時間的鎖
try {
boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);
if (!acquired) {//如果超時拋出異常
throw new CacheException("Couldn't get a lock in " + timeout + " for the key " + key + " at the cache " + delegate.getId());
}
} catch (InterruptedException e) {
throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e);
}
} else {//使用不帶超時時間的鎖
lock.lock();
}
}
private void releaseLock(Object key) {
ReentrantLock lock = locks.get(key);
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
在調用getObject獲取數據時,首先調用acquireLock根據key獲取鎖,如果獲取到鎖,則從PerpetualCache緩存中獲取數據,如果沒有則去數據庫查詢數據,返回結果後添加到緩存中並釋放鎖,注意去數據庫查詢數據時是根據key加了鎖的,因此相同key只會有一個線程到達數據庫查詢,也就不會出現緩存擊穿的問題,這個思路也可以用到我們的項目中去。
以上就是Mybatis解決緩存擊穿的思路,另外再來看一個裝飾者SynchronizedCache,提供同步的功能,該裝飾器就是在對緩存的增刪API上加上了synchronized關鍵字,這個裝飾器就是用來防止二級緩存出現並發安全問題的,而一級緩存根本不存在並發安全問題。其餘的裝飾者這裡就不贅述了,感興趣的讀者可自行分析。
CacheKey
因為Mybatis中存在動態SQL,所以緩存的key沒法僅用一個字符串來表示,所以通過CacheKey來封裝所有可能影響緩存的因素,那麼哪些因素會影響到緩存呢?
- namespace + id
- 查詢的sql
- 查詢的參數
- 分頁信息
而在CacheKey中有以下屬性:
private static final int DEFAULT_MULTIPLYER = 37;
private static final int DEFAULT_HASHCODE = 17;
private final int multiplier; //參與hash計算的乘數
private int hashcode; //CacheKey的hash值,在update函數中實時運算出來的
private long checksum; //校驗和,hash值的和
private int count; //updateList的中元素個數
private List<Object> updateList; //該集合中的元素決定兩個CacheKey是否相等
其中updateList就是用來存儲所有可能影響緩存的因素,其它幾個則是根據該屬性中的對象計算出來的值,每次構造CacheKey對象時都會調用update方法:
public void update(Object object) {
//獲取object的hash值
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
//更新count、checksum以及hashcode的值
count++;
checksum += baseHashCode;
baseHashCode *= count;
hashcode = multiplier * hashcode + baseHashCode;
//將對象添加到updateList中
updateList.add(object);
}
而判斷兩個CacheKey對象是否相同則是通過equals方法:
public boolean equals(Object object) {
if (this == object) {//比較是不是同一個對象
return true;
}
if (!(object instanceof CacheKey)) {//是否類型相同
return false;
}
final CacheKey cacheKey = (CacheKey) object;
if (hashcode != cacheKey.hashcode) {//hashcode是否相同
return false;
}
if (checksum != cacheKey.checksum) {//checksum是否相同
return false;
}
if (count != cacheKey.count) {//count是否相同
return false;
}
//以上都不相同,才按順序比較updateList中元素的hash值是否一致
for (int i = 0; i < updateList.size(); i++) {
Object thisObject = updateList.get(i);
Object thatObject = cacheKey.updateList.get(i);
if (!ArrayUtil.equals(thisObject, thatObject)) {
return false;
}
}
return true;
}
可以看到這裡比較相等的方法是非常嚴格的,並且效率極高,我們在項目中重寫equals方法時也可以參照該方法的實現。
反射
反射是Mybatis的重中之重,通過反射Mybatis才能實現對象的實例化和屬性的賦值,並且Mybatis的反射是對JDK的封裝和增強,使其更易於使用,性能更高。其中關鍵的幾個類如下:
- ObjectFactory:通過該對象創建POJO類的實例。
- ReflectorFactory:創建Reflector的工廠類。
- Reflector:MyBatis反射模塊的基礎,每個Reflector對象都對應一個類,在其中緩存了反射操作所需要的類元信息。
- ObjectWrapper:對象的包裝,抽象了對象的屬性信息,他定義了一系列查詢對象屬性信息的方法,以及更新屬性的方法。
- ObjectWrapperFactory:創建ObjectWrapper的工廠類。
- MetaObject:包含了原始對象、ObjectWrapper、ObjectFactory、ObjectWrapperFactory、ReflectorFactory的引用,通過該類可以進行核心反射類的所有操作,也是門面模式的實現。
由於該模塊只是對JDK的封裝,雖然代碼和類非常多,但並不是很複雜,這裡就不詳細闡述了。
總結
本篇講解了Mybatis最核心的四大模塊,可以看到使用了大量的設計模式使得代碼優雅簡潔,可讀性高,同時便於擴展,這也是我們在做項目時首先需要考慮的,代碼都是給人讀的,如何降低閱讀代碼的成本,提高代碼的質量,減少BUG的數量,只有多學習優秀代碼的設計思想才能提高我們自身的水平。